Updates from August, 2024 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:21 am on 13 August 2024 Permalink | Reply
    Tags:   

    Teaser 2586: Powerful dice 

    From The Sunday Times, 15th April 2012 [link] [link]

    George and Martha threw a die, noted the number of spots on the uppermost face, and entered that number in one of the boxes of a 3-by-3 grid of nine squares. They repeated the exercise eight more times resulting in a digit in each box.

    Then George looked at the three 3-digit numbers read across the rows and commented that each was a square or a cube. Martha looked at the three 3-digit numbers read down the columns and said the same was true for her numbers. Furthermore, their two sets of three numbers had only one in common.

    What (in increasing order) were the five different 3-digit numbers?

    [teaser2586]

     
    • Jim Randell's avatar

      Jim Randell 8:22 am on 13 August 2024 Permalink | Reply

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this puzzle.

      The following run file executes in 76ms. (Internal runtime of the generated program is 2.5ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C
      #  D E F
      #  G H I
      
      --digits="1-6"
      --distinct=""
      
      --code="check = cached(lambda n: is_square(n) or is_cube(n))"
      
      # check rows and columns
      "check(ABC)"
      "check(DEF)"
      "check(GHI)"
      "check(ADG)"
      "check(BEH)"
      "check(CFI)"
      
      # there is exactly one number shared between the rows and columns
      "len(intersect([{ABC, DEF, GHI}, {ADG, BEH, CFI}])) = 1"
      
      # and there are five different numbers in total
      "len({ABC, DEF, GHI, ADG, BEH, CFI}) = 5"
      
      --answer="call(ordered, {ABC, DEF, GHI, ADG, BEH, CFI})"
      --template="rows = [ ABC DEF GHI ]; cols = [ ADG BEH CFI ]"
      --solution=""
      --verbose="1-H"
      

      Solution: The numbers entered in the box were: 121, 125, 216, 256, 512.

      We have:

      121 = 11^2
      125 = 5^3
      216 = 6^3
      256 = 16^2
      512 = 8^3

      512 appears as both a row and a column.

      The most likely scenario is:

      Then George’s rows would consist of both squares and cubes, and Martha could say the same about her columns even though they are actually all cubes.

      Swapping the rows and columns yields a second viable layout, but it seems unlikely George would note that his each of his rows was a square or a cube, when they are in fact all cubes.

      Like

      • ruudvanderham's avatar

        ruudvanderham 11:50 am on 13 August 2024 Permalink | Reply

        from istr import istr
        istr.repr_mode('int')
        squares=[i*i for i in range(10,32)]
        cubes=[i*i*i for i in range (5,10)]
        squares_cubes=istr(squares+cubes)
        
        solutions=set()
        for abc in squares_cubes:
            for def_ in squares_cubes:
                for ghi in squares_cubes:
                    horizontals={abc,def_,ghi}
                    verticals={abc[i]|def_[i]|ghi[i] for i in range(3)}
                    if all(vertical in squares_cubes for vertical in verticals):
                        horizontals_verticals= horizontals | verticals
                        if len(horizontals_verticals)==5:
                            solutions.add(tuple(sorted(horizontals_verticals)))
        print(solutions)
        

        Like

        • Ruud's avatar

          Ruud 7:07 pm on 13 August 2024 Permalink | Reply

          I forgot to test whether all digits were between 1 and 6.
          Here’s an improved version (same output):

          from istr import istr
          
          istr.repr_mode("int")
          squares = [i * i for i in range(10, 32)]
          cubes = [i * i * i for i in range(5, 10)]
          squares_cubes = [i for i in istr(squares + cubes) if all(j in range(1, 7) for j in i)]
          
          solutions = set()
          for abc in squares_cubes:
              for def_ in squares_cubes:
                  for ghi in squares_cubes:
                      horizontals = {abc, def_, ghi}
                      verticals = {abc[i] | def_[i] | ghi[i] for i in range(3)}
                      if all(vertical in squares_cubes for vertical in verticals):
                          horizontals_verticals = horizontals | verticals
                          if len(horizontals_verticals) == 5:
                              solutions.add(tuple(sorted(horizontals_verticals)))
          print(solutions)
          

          Like

          • Jim Randell's avatar

            Jim Randell 5:54 pm on 14 August 2024 Permalink | Reply

            @Ruud: I think you also need to add a test for “their two sets of three numbers had only one in common”. Your code would allow a repeated row or a repeated column.

            Like

            • Ruud's avatar

              Ruud 7:07 pm on 14 August 2024 Permalink | Reply

              @Jim
              Thanks for pointing this out. You are right.
              My updated code:

              from istr import istr
              
              istr.repr_mode("int")
              squares = [i * i for i in range(10, 32)]
              cubes = [i * i * i for i in range(5, 10)]
              squares_cubes = [i for i in istr(squares + cubes) if all(j in range(1,7) for j in i)]
              
              solutions = set()
              for abc in squares_cubes:
                  for def_ in squares_cubes:
                      for ghi in squares_cubes:
                          horizontals = {abc, def_, ghi}
                          verticals = {abc[i] | def_[i] | ghi[i] for i in range(3)}
                          if all(vertical in squares_cubes for vertical in verticals):
                              if len(horizontals & verticals) == 1:
                                  solutions.add(tuple(sorted(horizontals | verticals)))
              print(solutions)
              

              Like

  • Unknown's avatar

    Jim Randell 9:50 am on 7 August 2024 Permalink | Reply
    Tags:   

    Teaser 2587: Hobson’s choice 

    From The Sunday Times, 22nd April 2012 [link] [link]

    In the diagram the letters represent the numbers 1 to 16 in some order. They form a magic square, where the sum of each row, each column and each main diagonal is the same. The letters of the word “CHOICE” and some others (including all the letters not used in the magic square) have a value equal to their position in the alphabet (C=3, H=8, etc).

    What is the value of H+O+B+S+O+N?

    [teaser2587]

     
    • Jim Randell's avatar

      Jim Randell 9:52 am on 7 August 2024 Permalink | Reply

      See also: Enigma 1690.

      The 16 numbers in the grid sum to tri(16) = 136, so each row and column must sum to 1/4 of this = 34.

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to find possible assignments.

      The following run file executes in 85ms. (Internal runtime of the generated code is 145µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C D
      #  E F G H
      #  I J K L
      #  M N O P
      
      --base=27
      --digits="1-16"
      
      # rows
      "A + B + C + D == 34"
      "E + F + G + H == 34"
      "I + J + K + L == 34"
      "M + N + O + P == 34"
      # columns
      "A + E + I + M == 34"
      "B + F + J + N == 34"
      "C + G + K + O == 34"
      "D + H + L + P == 34"
      # diagonals
      "A + F + K + P == 34"
      "D + G + J + M == 34"
      
      # given values
      --assign="C,3"
      --assign="E,5"
      --assign="H,8"
      --assign="I,9"
      --assign="O,15"
      --assign="S,19" # we only need S (from Q..Z)
      
      # required answer
      --answer="H + O + B + S + O + N"
      
      --template=""
      

      Solution: H + O + B + S + O + N = 73.

      The completed grid is:


      The Magic Square is a variation of the “Dürer” Magic Square seen in “Melencolia I”. [ @wikipedia ]

      If you recognise that the given values (C = 3, E = 5, H = 8, I = 9, O = 15) fit into this square you don’t need to do any more working out, as the square is associative (which means any number and its symmetric opposite add to 17).

      So we can calculate the required result using the given values as:

      H + O + B + S + O + N
      = H + O + (17 − O) + S + O + (17 − C)
      = 8 + 15 + 2 + 19 + 15 + 14
      = 73

      Like

    • ruudvanderham's avatar

      ruudvanderham 2:10 pm on 7 August 2024 Permalink | Reply

      Brute force solution:

      import itertools
      
      C = 3
      E = 5
      H = 8
      I = 9
      O = 15
      S = 19
      for A, B, D, F, G, J, K, L, M, N, P in itertools.permutations((1, 2, 4, 6, 7, 10, 11, 12, 13, 14, 16)):
          ABCD = A + B + C + D
          if E + F + G + H == ABCD:
              if I + J + K + L == ABCD:
                  if M + N + O + P == ABCD:
                      if A + E + I + M == ABCD:
                          if B + F + J + N == ABCD:
                              if C + G + K + O == ABCD:
                                  if D + H + L + P == ABCD:
                                      if A + F + K + P == ABCD:
                                          if D + G + J + M == ABCD:
                                              print(f"{H+O+B+S+O+N=}")
                                              print(f"{A=:2} {B=:2} {C=:2} {D=:2}")
                                              print(f"{E=:2} {F=:2} {G=:2} {H=:2}")
                                              print(f"{I=:2} {J=:2} {K=:2} {L=:2}")
                                              print(f"{M=:2} {N=:2} {O=:2} {P=:2}")
      
      

      Output:

      H+O+B+S+O+N=73
      A=16 B= 2 C= 3 D=13
      E= 5 F=11 G=10 H= 8
      I= 9 J= 7 K= 6 L=12
      M= 4 N=14 O=15 P= 1
      

      Like

    • GeoffR's avatar

      GeoffR 5:41 pm on 7 August 2024 Permalink | Reply

      A 4-stage permutation brings the run- time down to 55 msec using ABCD = 34 , or 177 msec not using ABCD = 34.

      
      from enigma import Timer
      timer = Timer('ST2587', timer='E')
      timer.start()
      
      from itertools import permutations
      #  A B C D
      #  E F G H
      #  I J K L
      #  M N O P
      
      # Given values
      C, E, H = 3, 5, 8
      I, O, S = 9, 15, 19
      
      # Find remaining digits to permute
      digits = set(range(1,17)).difference({3, 5,8,9,15})
      
      # 1st stage permutation
      for p1 in permutations (digits, 3):                       
        A, B, D = p1  # C given
        ABCD = A + B + C + D
        if ABCD != 34:continue
        
        # 2nd stage permutation
        q1 = digits.difference(p1)
        for p2 in permutations(q1, 2):
          F, G = p2  # E and H given
          EFGH = E + F + G + H
          if EFGH != ABCD:continue
          
          # 3rd stage permutation
          q2 = q1.difference(p2)
          for p3 in permutations(q2, 3):
            J, K, L = p3  # I given
            IJKL = I + J + K + L
            if IJKL != ABCD:continue
            
            # 4th stage permutation
            q3 = q2.difference(p3)
            for p4 in permutations(q3, 3):
              M, N, P = p4 # O given
              MNOP = M + N + O + P
              if MNOP != ABCD: continue
              
              # Check column and diagonal sums
              if A + E + I + M != ABCD:continue
              if B + F + J + N != ABCD:continue
              if C + G + K + O != ABCD:continue
              if D + H + L + P != ABCD:continue
              if A + F + K + P != ABCD:continue
              if D + G + J + M != ABCD:continue
                
              print(f"HOBSON = {H + O + B + S + O + N}")
              print(A,B,C,D)
              print(E,F,G,H)
              print(I,J,K,L)
              print(M,N,O,P)
              timer.stop()      
              timer.report()
              # [ST2587] total time: 0.1775740s (177.57ms) - without ABCD = 34
              # [ST2587] total time: 0.0553104s (55.31ms) - with ABCD = 34
      
      # HOBSON = 73
      # 16 2 3 13
      # 5 11 10 8
      # 9 7  6 12
      # 4 14 15 1
      
      
      

      Like

      • ruudvanderham's avatar

        ruudvanderham 6:48 am on 8 August 2024 Permalink | Reply

        Nice solution.
        I guess that if you first try the EFGH permutations, performance might slightly improve because there are only 2 choices there.

        Like

        • ruudvanderham's avatar

          ruudvanderham 6:54 am on 8 August 2024 Permalink | Reply

          Or even faster (maybe): go vertical vertical and first do AEIM, then CGKO (twice 2 choices only).

          Like

        • Frits's avatar

          Frits 10:00 am on 8 August 2024 Permalink | Reply

          Brian did some analysis and discovered that L must be 12.

          [ https://brg.me.uk/?page_id=3378 ]

          Like

          • Jim Randell's avatar

            Jim Randell 11:03 am on 8 August 2024 Permalink | Reply

            @Frits: By using the given values and simplifying the 10 equations we can get:

            B + N = 16

            And we know values for all the other letters in HOBSON, so the result follows directly.

            (Although it is not a constructive solution – it assumes that it is possible to make a viable Magic Square).

            Like

    • GeoffR's avatar

      GeoffR 10:07 am on 8 August 2024 Permalink | Reply

      I got about a 5 ms speed increase trying the EFGH permutation first, but the second suggestion i.e. AEIM, then CGKO did not come out easily – may have another look later.

      Like

    • GeoffR's avatar

      GeoffR 11:57 am on 9 August 2024 Permalink | Reply

      This solution uses the fact that this an associative magic square to find the value of HOBSON only. The full ten constraints for summing rows, columns and diagonals were still needed to find a unique magic square.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      %   A B C D
      %   E F G H
      %   I J K L
      %   M N O P
      
      var 1..16:A; var 1..16:B; var 1..16:C; var 1..16:D;
      var 1..16:E; var 1..16:F; var 1..16:G; var 1..16:H;
      var 1..16:I; var 1..16:J; var 1..16:K; var 1..16:L;
      var 1..16:M; var 1..16:N; var 1..16:O; var 1..16:P;
      int:S == 19;
      
      constraint all_different([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P]);
      
      % Given values
      constraint C == 3 /\ E == 5 /\ H == 8 /\ I == 9 /\ O == 15;
      
      % LB = 1+2+3+4+5+6 = 21, UB = 19 + 4*16 = 83
      var 21..83: HOBSON == H + O + B + S + O + N;
      
      % Symmetric sum = 4^2 + 1 = 17, for this associative magic square
      % So symmetrically opposite numbers add to 17 
      constraint A + P == 17 /\ B + O == 17 /\ C + N == 17 /\ D + M == 17;
      constraint E + L == 17 /\ F + K == 17 /\ G + J == 17;  % Given H + I = 17
      
       solve satisfy;
       output["HOBSON = " ++ show(HOBSON) ];
      %  HOBSON = 73
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:44 am on 1 August 2024 Permalink | Reply
    Tags:   

    Teaser 2584: Truncation 

    From The Sunday Times, 1st April 2012 [link] [link]

    Callum is learning about squares and primes. He told me that he had discovered a four-figure square that becomes a three-figure prime if you delete its last digit. I could not work out his square from this information and he informed me that, if I knew the sum of its digits, then I would be able to work it out. So I then asked whether the square had a repeated digit: his answer enabled me to work out his square.

    What was it?

    [teaser2584]

     
    • Jim Randell's avatar

      Jim Randell 9:44 am on 1 August 2024 Permalink | Reply

      This Python program runs in 81ms. (Internal runtime is 381µs).

      Run: [ @replit ]

      from enigma import (
        powers, is_prime, filter_unique, dsum,
        is_duplicate, singleton, group, printf
      )
      
      # find 4-digit squares, where the first 3 digits form a prime
      sqs = list(n for n in powers(32, 99, 2) if is_prime(n // 10))
      
      # select numbers unique by digit sum
      ns = filter_unique(sqs, f=dsum).unique
      
      # group results by whether there are repeated digits
      g = group(ns, by=is_duplicate)
      
      # look for unique values
      for (k, vs) in g.items():
        n = singleton(vs)
        if n is not None:
          printf("(duplicate = {k}) => n = {n}")
      

      Solution: Callum’s number was: 5476.

      There are 8 candidate squares that can be truncated to a prime, but only 3 have a unique digit sum:

      dsum = 10 → 2116
      dsum = 13 → 3136
      dsum = 19 → 1936, 4096
      dsum = 22 → 5476
      dsum = 25 → 5776, 7396, 8836

      Two of these 3 have repeated digits, so Callum must have answered that square did not have repeated digits, which uniquely identifies 5476 as his number.

      Like

    • GeoffR's avatar

      GeoffR 4:17 pm on 1 August 2024 Permalink | Reply

      
      from collections import defaultdict
      from enigma import is_prime, dsum
      
      ans = defaultdict(list)
      
      sq4 = [ x**2 for x in range(32, 100)]
      
      for n1 in sq4:
          flag = 0  # for repeating or non-repeating digits
          n2 = int(str(n1)[0:3])
          if is_prime(n2):
              # non rpeeating digits in squares
              if len(set(str(n1))) == len(str(n1)):
                  flag = 1
                  ans[(flag, dsum(n1))] += [n1]
              # repeating digits in squares
              if len(set(str(n1))) != len(str(n1)):
                  flag = 2
                  ans[(flag, dsum(n1))] += [n1]
      
      for k, v in ans.items():
          print(k,v)
      
      '''
      (1, 19) [1936, 4096] <<< multiple squares for digit sum
      (2, 10) [2116] <<< same number of repeating digits as square 3136
      (2, 13) [3136] <<< same number of repeating digits as square 2116
      (1, 22) [5476]  <<< Answer
      (2, 25) [5776, 8836] <<< multiple squares for digit sum
      (1, 25) [7396]  <<< same digit sum as squares 5776, 8836
      '''
      
      
      
      

      Like

    • Ruud's avatar

      Ruud 8:50 pm on 1 August 2024 Permalink | Reply

      from istr import istr
      import math
      import collections
      
      
      def is_prime(n):
          if n < 2:
              return False
          if n % 2 == 0:
              return n == 2  # return False
          k = 3
          while k * k <= n:
              if n % k == 0:
                  return False
              k += 2
          return True
      
      
      squares_per_sum_digits = collections.defaultdict(list)
      
      for i in range(round(math.sqrt(1000)), round(math.sqrt(10000))):
          square = istr(i * i)
          if is_prime(square[:-1]):
              prime = square[:-1]
              sum_digits = sum(n for n in square)
              squares_per_sum_digits[sum_digits].append(square)
      
      potential_squares = [squares[0] for squares in squares_per_sum_digits.values() if len(squares) == 1]
      has_repeated_digits = collections.defaultdict(list)
      for square in potential_squares:
          has_repeated_digits[len(set(square))!=4].append(square)
      
      solution = [square[0] for square in has_repeated_digits.values() if len(square) == 1]
      print(*solution)
      

      Like

    • GeoffR's avatar

      GeoffR 7:57 pm on 3 August 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 0..9:B; var 1..9:C; var 0..9:D;
      var 1024..9081: sq_dig4 == 1000*A + 100*B + 10*C + D;
      
      set of int: sq4 = {n * n | n in 32..99};  
      
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x)))))
       ((i < x) -> (x mod i > 0));
       
      % Square with last digit deleted is a prime
      constraint is_prime(100*A + 10*B + C);
      constraint sq_dig4 in sq4;
      
      solve satisfy;
      
      output[ "Digit sum = " ++ show(A + B + C + D) ++
      ", Square = " ++ show(sq_dig4) ];
      
      % Digit sum = 10, Square = 2116 - repeating digits - 2nd stage elimination
      % ----------
      % Digit sum = 19, Square = 1936 - not a square with a single digit sum - 1st stage elimination
      % ----------
      % Digit sum = 13, Square = 3136 - repeating digits - 2nd stage eliminations
      % ----------
      % Digit sum = 25, Square = 8836 - not a square with a single digit sum - 1st stage elimination
      % ----------
      % Digit sum = 22, Square = 5476 - *** ANSWER *** (unique digit sum, no repeating digits)
      % ----------
      % Digit sum = 25, Square = 5776 - not a square with a single digit sum - 1st stage elimination
      % ----------
      % Digit sum = 19, Square = 4096 - not a square with a single digit sum - 1st stage elimination
      % ----------
      % Digit sum = 25, Square = 7396 - not a square with a single digit sum - 1st stage elimination
      % ----------
      % ==========
      

      Like

  • Unknown's avatar

    Jim Randell 9:44 am on 23 July 2024 Permalink | Reply
    Tags:   

    Teaser 2576: Latin cube 

    From The Sunday Times, 5th February 2012 [link] [link]

    I have a cube and on each face of it there is a capital letter. I look at one face from the front, hold the top and bottom faces horizontal, and rotate the cube. In this way I can correctly read a four-letter Roman numeral. Secondly, I hold the cube with the original front face underneath and the original top face at the front, and I rotate the cube keeping the new top and bottom faces horizontal. In this way I correctly see another four-letter Roman numeral. Thirdly, I return the cube to its original position and rotate it with the two sides vertical to give another correct four-letter Roman numeral. The sum of the second and third numbers is less than a hundred.

    What was the third four-letter Roman numeral seen?

    [teaser2576]

     
    • Jim Randell's avatar

      Jim Randell 9:45 am on 23 July 2024 Permalink | Reply

      Some clarification of the puzzle text is required to find a single answer (and I assume the puzzle is meant to have a unique answer):

      (1) Although the letters used in Roman numerals (I, V, X, L, C, D, M) are all distinguishable whatever the orientation, the puzzle requires that when letters are read they are in a “sensible” orientation. So, I and X can be read correctly upside down. However this is not sufficient to solve the puzzle, we also require that X can be read correctly in any orientation (which may not be the case if a serif font is used, as is often the case with Roman numerals).

      (2) The mechanism to read the numbers is that the front face is read, the cube is then rotated through 90° about a major axis to change the front face, which is then read, and the same move is repeated until the four faces in a band around the cube have been read in order (and a final move would bring the cube back to the starting position).

      (3) The numbers read should be valid Roman numerals in the “usual” form. (I used the [[ int2roman() ]] and [[ is_roman() ]] functions in the enigma.py library to ensure this). I didn’t allow “IIII” to represent 4 (although if you do it doesn’t change anything).

      (4) Each of the three numbers read is different. (Possibly this is meant to be indicated by the use of “another”).


      We are not told what direction the cube is rotated, so this Python program considers rotations in each direction for the 3 numbers. Although note that for some numbers (e.g. VIII (= 8), where the second and fourth letters are the same), the direction of the rotation doesn’t matter.

      I used the [[ Cube() ]] class (originally from Teaser 2835) to keep track of the rotation of the cube (and orientation of the faces).

      This Python program considers possible values for the second and third numbers (which cover bands of the cube that include all faces, so leave the cube fully specified), and then reads the first number and looks for cases where this gives a valid Roman numeral.

      It runs in 78ms. (Internal runtime is 11ms).

      Run: [ @replit ]

      from enigma import (irange, int2roman, is_roman, join, seq2str, printf)
      from cube import (Cube, U, D, L, R, F, B, face_label as f2l)
      
      # valid orientations for roman digits
      valid = dict((k, {0}) for k in "IVXLCDM")
      valid['I'] = {0, 2}  # I can be upside down
      valid['X'] = {0, 1, 2, 3}  # X can be in any orientation
      
      # collect 4-letter roman numerals less than 100
      n2r = dict()
      for n in irange(1, 99):
        r = int2roman(n)
        if len(r) == 4:
          n2r[n] = r
      
      # read values from the front of cube <c>, and apply each of the specified moves <ms>
      # while match the values in <vs> (can be '?')
      # return (<updated cube>, <values read>)
      def read(c, ms, vs):
        rs = list()
        while True:
          # read the front face
          (v, vs0) = (c.faces[F], vs[0])
          # blank face?
          if v is None:
            v = vs0
            if v != '?':
              # fill out the new value
              c = c.update(faces=[(F, v)], orientations=[(F, 0)])
          else:
            # check it is in a valid orientation and a valid value
            if (c.orientations[F] not in valid[v]) or (vs0 != '?' and vs0 != v):
              return (None, None)
          rs.append(v)
          # any more moves?
          if not ms: break
          c = c.rotate(ms[0:1])
          ms = ms[1:]
          vs = vs[1:]
        return (c, join(rs))
      
      # start with an empty cube
      c0 = Cube(faces=[None] * 6)
      
      # choose a 4-letter roman for the second number
      for (n2, r2) in n2r.items():
        # choose the direction of rotation
        for d2 in [U, D]:
          # read the second number
          (c1, _) = read(c0.rotate([L]), [d2] * 3, r2)
          if c1 is None: continue
          # reset to original position
          c1 = c1.rotate([d2, R])
      
          # choose a 4-letter roman for the third number
          for (n3, r3) in n2r.items():
            # the first and second numbers sum to less than 100
            if n2 + n3 > 99: continue
            # choose a direction of rotation
            for d3 in [L, R]:
              # read the third number
              (c2, _) = read(c1, [d3] * 3, r3)
              if c2 is None: continue
              # all faces should now be filled out
              assert None not in c2.faces
              # reset to original position
              c2 = c2.rotate([d3])
      
              # choose a direction for the first number
              for d1 in [U, D]:
                # read the first number
                (_, r1) = read(c2, [d1] * 3, '????')
                if r1 is None: continue
                # is it a valid roman?
                n1 = is_roman(r1)
                if not n1: continue
                # check numbers are all different
                if len({n1, n2, n3}) != 3: continue
      
                # output solution
                printf("{r1} ({n1}) [{d1}]; {r2} ({n2}) [{d2}]; {r3} ({n3}) [{d3}]; {cube}",
                       d1=f2l[d1], d2=f2l[d2], d3=f2l[d3], cube=seq2str(c2.faces))
      

      Solution: The third Roman numeral seen was: LXII (= 62).

      The first two numbers were: LXIX (= 69) and XXIX (= 29), and the second and third sum to 91.

      Note that both of the first two numbers can be read using either direction of rotation (which is why my code finds 4 different ways to the answer), but the final number only works in one direction.

      The cube has faces (U, D, L, R, F, B) = (X, I, X, X, L, I).

      If we allow repeated numbers there is also a solution with the cube (U, D, L, R, F, B) = (X, I, X, X, X, I), which gives the numbers XXIX (= 29), XXIX (= 29), XXII (= 22).

      Like

    • Lise Andreasen's avatar

      Lise Andreasen 12:07 am on 24 July 2024 Permalink | Reply

      Especially (1) the orientation of X is interesting.

      Like

  • Unknown's avatar

    Jim Randell 5:24 pm on 9 July 2024 Permalink | Reply
    Tags:   

    Teaser 2579: Box clever 

    From The Sunday Times, 26th February 2012 [link] [link]

    I have placed the digits 1 to 9 in a three-by-three grid made up of nine boxes. For each two-by-two array within the grid, the sum of its four entries is the same. Furthermore, if you reverse the order of the digits in that common total, then you get the sum of the four corner entries from the original grid.

    Which digits are adjacent to the 6? (i.e. digits whose boxes have a common side with 6’s box?)

    [teaser2579]

     
    • Jim Randell's avatar

      Jim Randell 5:25 pm on 9 July 2024 Permalink | Reply

      Here is a run-file that uses the [[ SubstitutedExpression ]] solver from the enigma.py library to find possible grid layouts.

      It runs in 94ms. (Internal runtime of the generated program is 8.6ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C
      #  D E F
      #  G H I
      
      --distinct="ABCDEFGHI"
      --invalid="0,ABCDEFGHI"
      
      # each box of 4 digits sums to XY
      "A + B + D + E = XY"
      "B + C + E + F = XY"
      "D + E + G + H = XY"
      "E + F + H + I = XY"
      
      # the four corners sum to YX
      "A + C + G + I = YX"
      
      # [optional] remove duplicate solutions
      "A < C" "A < G" "C < G"
      
      --template=""
      

      Line 22 ensures only one version of symmetrically equivalent layouts (rotations and reflections) is produced.

      For a complete solution to the puzzle we can further process the results to find the required digits for the answer.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, grid_adjacency, chunk, printf)
      
      adj = grid_adjacency(3, 3)
      
      p = SubstitutedExpression.from_file("teaser2579.run")
      
      for s in p.solve(verbose=''):
        grid = list(s[x] for x in 'ABCDEFGHI')
        ans = sorted(grid[i] for i in adj[grid.index(6)])
        printf("{grid} -> {ans}", grid=list(chunk(grid, 3)))
      

      Solution: The digits adjacent to 6 are: 3, 4, 5.

      Here is a viable layout:

      The 2×2 grids give:

      1 + 9 + 8 + 3 = 21
      9 + 2 + 3 + 7 = 21
      8 + 3 + 4 + 6 = 21
      3 + 7 + 6 + 5 = 21

      And the corners:

      1 + 2 + 4 + 5 = 12

      There are 8 potential layouts, but they can all be rotated/reflected to give the one above.

      Like

      • Ruud's avatar

        Ruud 12:40 pm on 10 July 2024 Permalink | Reply

        """
        label boxes as
        012
        345
        678
        """
        import itertools
        
        
        def sum4(*indexes):
            return sum(digits[index] for index in indexes)
        
        
        adjacent = {0: (1, 3), 1: (0, 2, 4), 2: (1, 5), 3: (0, 4, 6), 4: (1, 3, 5, 7), 5: (2, 4, 8), 6: {3, 7}, 7: (6, 4, 8), 8: (5, 7)}
        
        for digits in itertools.permutations(range(1, 10)):
            if (s := sum4(0, 1, 3, 4)) == sum4(1, 2, 4, 5) == sum4(3, 4, 6, 7) == sum4(4, 5, 7, 8) and int(str(s)[::-1]) == sum4(0, 2, 6, 8):
                print(digits[0], digits[1], digits[2], "\n", digits[3], digits[4], digits[5], "\n", digits[6], digits[7], digits[8], sep="")
                print("     ", *sorted(digits[index] for index in adjacent[digits.index(6)]))
        

        Like

      • Frits's avatar

        Frits 9:19 pm on 10 July 2024 Permalink | Reply

        @Jim, “verbose” only seems to work in the run member.

        Like

        • Jim Randell's avatar

          Jim Randell 7:17 am on 11 July 2024 Permalink | Reply

          @Frits: Care to elaborate?

          Like

          • Frits's avatar

            Frits 8:09 am on 11 July 2024 Permalink | Reply

            @Jim,

            “for s in p.solve(verbose=256):” or “for s in p.solve(verbose=’3′):” is not working in my environment. ‘–verbose=”3″‘ works well in the run member.

            Like

            • Jim Randell's avatar

              Jim Randell 3:06 pm on 11 July 2024 Permalink | Reply

              @Frits:

              You can add an appropriate [[ --verbose ]] directive to the run file, or pass it as an argument to the [[ from_file() ]] call, so that it is active when the code is being generated.

              p = SubstitutedExpression.from_file("teaser/teaser2579.run", ["--verbose=C"])
              

              But I’ve changed the [[ solve() ]] function so that the verbose parameter takes effect before anything else, which means if the code has not already been generated (which will usually be the case the first time solve() is called), then it will be displayed before it is compiled.

              Like

    • GeoffR's avatar

      GeoffR 7:31 pm on 9 July 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      %  A B C
      %  D E F
      %  G H I
      
      var 1..9:A; var 1..9:B; var 1..9:C;
      var 1..9:D; var 1..9:E; var 1..9:F;
      var 1..9:G; var 1..9:H; var 1..9:I;
      
      var 12..98:total;
      var 12..98:rev_total;
      
      constraint all_different([A,B,C,D,E,F,G,H,I]);
      
      constraint A+B+D+E == total /\ B+C+E+F == total /\ D+E+G+H == total /\ E+F+H+I == total;
      
      constraint rev_total == 10*(total mod 10) + total div 10;
      
      constraint A + C + G + I == rev_total;
      
      % order constraint
      constraint A < C /\ A < G /\ C < G;
      
      solve satisfy; 
      
      output[  show([A,B,C]) ++ "\n" ++ show([D,E,F]) ++ "\n" ++ show([G,H,I]) ];
      
      % [1, 9, 2]
      % [8, 3, 7]
      % [4, 6, 5]
      % ----------
      % ==========
      % From grid, the digits adjacent to 6 are 3, 4, and 5.
      
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 3:34 pm on 10 July 2024 Permalink | Reply

      
      from enigma import Timer
      timer = Timer('ST2579', timer='E')
      timer.start()
      
      from itertools import permutations
      
      # a b c
      # d e f
      # g h i
      
      for a, b, c, d, e, f, g, h, i in permutations (range(1, 10)):
        # check four square totals
        tot = a + b + d + e
        if b + c + e + f == tot:
          if d + e + g + h == tot:
            if e + f + h + i == tot:
              
              # check corner digits sum for a reverse total
              x, y = tot // 10, tot % 10
              rev_tot = 10 * y + x
              if a + c + g + i == rev_tot:
                if a < c and a < g and c < g:
                  
                  # digits whose boxes have a common side with 6’s box
                  if a == 6:print(f"Adjacent digits = {b} and {d}.")
                  if b == 6:print(f"Adjacent digits = {a},{e} and {c}.")
                  if c == 6:print(f"Adjacent digits = {b} and {f}.")
                  if d == 6:print(f"Adjacent digits = {a},{e} and {g}.")
                  if e == 6:print(f"Adjacent digits = {b},{d},{f} and {h}.")
                  if f == 6:print(f"Adjacent digits = {c},{e} and {i}.")
                  if g == 6:print(f"Adjacent digits = {d} and {h}.")
                  if h == 6:print(f"Adjacent digits = {g},{e} and {i}.")
                  if i == 6:print(f"Adjacent digits = {h} and {f}.")
                  # Adjacent digits = 4,3 and 5.
                  timer.stop()      
                  timer.report()
                  # [ST2579] total time: 0.0290888s (29.09ms)
      
      
      

      Like

      • GeoffR's avatar

        GeoffR 11:51 am on 11 July 2024 Permalink | Reply

        I recoded this teaser in a 3-stage permutation for 4, 2 and 3 digits – my previous posting was a brute-force 9-digit permutation. It was much faster, albeit that this 2nd posting was on an I9 CPU as opposed to an I7 CPU for the original posting

        from enigma import Timer
        timer = Timer('ST2579', timer='E')
        timer.start()
        
        from itertools import permutations
        
        # a b c
        # d e f
        # g h i
        
        digits = set(range(1, 10))
        
        for p1 in permutations (digits, 4):
          a, b, d, e = p1
         
          # check four square totals
          tot = a + b + d + e
          q1 = digits.difference( p1)
          for p2 in permutations(q1, 2):
            c, f = p2
            if b + c + e + f == tot:
               q3 = q1.difference(p2)
               for p3 in permutations(q3):
                 g, h, i = p3
                 if d + e + g + h == tot:
                   if e + f + h + i == tot:
               
                     # check corner digits sum for a reverse total
                     x, y = tot // 10, tot % 10
                     rev_tot = 10 * y + x
                     if a + c + g + i == rev_tot:
                       if a < c and a < g and c < g:
                   
                         # digits whose boxes have a common side with 6’s box
                         if a == 6:print(f"Adjacent digits = {b} and {d}.")
                         if b == 6:print(f"Adjacent digits = {a},{e} and {c}.")
                         if c == 6:print(f"Adjacent digits = {b} and {f}.")
                         if d == 6:print(f"Adjacent digits = {a},{e} and {g}.")
                         if e == 6:print(f"Adjacent digits = {b},{d},{f} and {h}.")
                         if f == 6:print(f"Adjacent digits = {c},{e} and {i}.")
                         if g == 6:print(f"Adjacent digits = {d} and {h}.")
                         if h == 6:print(f"Adjacent digits = {g},{e} and {i}.")
                         if i == 6:print(f"Adjacent digits = {h} and {f}.")
                         # Adjacent digits = 4,3 and 5.
                         timer.stop()      
                         timer.report()
                         # [ST2579] total time: 0.0055232s (5.52ms)
        
        
        

        Like

    • Frits's avatar

      Frits 9:08 pm on 10 July 2024 Permalink | Reply

      Looping over 4 variables.

           
      from itertools import permutations
      
      row, col = lambda i: i // 3, lambda i: i % 3
      
      # adjacent fields to field <i>
      adj = {i: tuple(j for j in range(9)
                if sum(abs(tp(j) - tp(i)) for tp in (row, col)) == 1)
                for i in range(9)}
                
      #  A B C
      #  D E F
      #  G H I
      
      '''
      # each box of 4 digits sums to XY
      "A + B + D + E = XY"
      "B + C + E + F = XY"
      "D + E + G + H = XY"
      "E + F + H + I = XY"
       
      # the four corners sum to YX
      "A + C + G + I = YX"
      '''
      
      sols, set9 = set(), set(range(1, 10))
      # Y is 1 or 2 as YX < 30 and X = 2 (H formula) so XY = 21
      XY, YX = 21, 12
      # get first three numbers
      for B, D, E in permutations(range(1, 10), 3):
        # remaining numbers after permutation
        r = sorted(set9 - {B, D, E})
        A = XY - (B + D + E)
        if A in {B, D, E} or not (0 < A <= 12 - sum(r[:2])): continue
       
        for F in set(r).difference([A]):
          C = XY - B - E - F
          H = (4 * XY - YX) // 2 - (B + D + 2 * E + F)
          G = XY - D - E - H
          I =  YX - A - C - G
          if E + F + H + I != XY: continue
          
          vars = [A, B, C, D, E, F, G, H, I]
          if set(vars) != set9: continue
          # store digits adjacent to the 6
          sols.add(tuple(sorted(vars[j] for j in adj[vars.index(6)])))
      
      print("answer:", ' or '.join(str(s) for s in sols)) 
      

      Like

  • Unknown's avatar

    Jim Randell 3:59 pm on 25 June 2024 Permalink | Reply
    Tags:   

    Teaser 2578: The right money 

    From The Sunday Times, 19th February 2012 [link] [link]

    In order to conserve his change, Andrew, our local shopkeeper, likes to be given the exact money. So today I went to Andrew’s shop with a collection of coins totalling a whole number of pounds in value. I could use these coins to pay exactly any amount from one penny up to the total value of the coins. Furthermore, the number of coins equalled the number of pounds that all the coins were worth; I had chosen the minimum number of coins to have this property.

    (a) How many coins did I have?
    (b) For which denominations did I have more than one coin of that value?

    [teaser2578]

     
    • Jim Randell's avatar

      Jim Randell 3:59 pm on 25 June 2024 Permalink | Reply

      I am assuming we are using “standard” UK coinage which was in circulation at the time of the publication of the puzzle. These are: 1p, 2p, 5p, 10p, 20p, 50p, £1 (= 100p), £2 (= 200p).

      There are special commemorative 25p and £5 coins, but these are not commonly used in making purchases.

      We can get a nice compact program without doing any particular analysis (like determining there must be at least one 1p coin), and it runs in just under 1s, so it not too bad. (I also tried a custom express() routine which makes an amount using an exact number of coins, but it was only a little bit faster).

      We are going to need at least 10 coins to be able to make the required different amounts (with a set of 10 different coins there are 2^10 − 1 = 1023 subsets, which could potentially allow all values from 1p – 1000p to be made, but with fewer coins there will be insufficiently many subsets).

      This Python program runs in 860ms (using PyPy).

      Run: [ @replit ]

      from enigma import (irange, inf, subsets, multiset, printf)
      
      # coin denominations (in pence)
      coins = (1, 2, 5, 10, 20, 50, 100, 200)
      
      # consider number coins (= total value in pounds)
      for n in irange(10, inf):
        done = 0
        # choose n coins ...
        for vs in subsets(coins, size=n, select='R', fn=multiset.from_seq):
          # ... with a total value of n pounds
          if vs.sum() != 100 * n: continue
          # can we make all values between 1 and 100n using these coins?
          if not all(any(vs.express(x)) for x in irange(1, 100 * n)): continue
          # output solution
          printf("{n} coins -> {vs}", vs=vs.map2str(arr='p x', sep='; '))
          done = 1
        if done: break
      

      Solution: (a) There are 16 coins; (b) There is more than one of the 2p, 10p, £2 coins.

      The collection is:

      1× 1p
      2× 2p
      1× 5p
      2× 10p
      1× 20p
      1× 50p
      1× £1
      7× £2
      total = £16 in 16 coins


      If we additionally allow 25p and £5 coins we can achieve the answer using only 13 coins:

      1× 1p
      2× 2p
      2× 5p
      1× 10p
      1× 25p
      1× 50p
      1× £1
      3× £2
      1× £5
      total = £13 in 13 coins

      1x 1p
      2× 2p
      1× 5p
      2× 10p
      1× 20p
      1× 50p
      1× £1
      3× £2
      1× £5
      total = £13 in 13 coins

      Like

      • Frits's avatar

        Frits 1:30 pm on 1 July 2024 Permalink | Reply

        Lucky for you the coin denominations weren’t something like [1, 2, 5, 10, 20, 50, 101, 104].

         
        -------- number of coins: 182 [1, 2, 1, 1, 2, 1, 2, 172]
        

        Like

      • Jim Randell's avatar

        Jim Randell 2:21 pm on 3 July 2024 Permalink | Reply

        I’ve just realised that [[ multiset.express() ]] already supports making an amount with a specific number of coins, so my code can be a bit more compact (and faster).

        Run: [ @replit ]

        from enigma import (irange, inf, multiset, printf)
        
        # coin denominations
        coins = (1, 2, 5, 10, 20, 50, 100, 200)
        
        # consider total value in pounds
        for n in irange(10, inf):
          done = 0
          # make a total of n pounds using exactly n coins
          tot = 100 * n
          for vs in multiset.from_seq(coins, count=n).express(tot, n):
            # can we make all other values between 1 and tot using these coins?
            if not vs.expressible(irange(1, tot - 1)): continue
            # output solution
            printf("{n} coins -> {vs}", vs=vs.map2str(arr='p x', sep='; '))
            done = 1
          if done: break
        

        Like

        • Frits's avatar

          Frits 2:55 pm on 3 July 2024 Permalink | Reply

          I know you said you were not doing any particular analysis but with this update the program is faster especially for more difficult cases.

          if not all(any(vs.express(x)) for x in irange(1, max(vs) - 1)): continue     
          

          Like

      • Jim Randell's avatar

        Jim Randell 8:43 am on 5 July 2024 Permalink | Reply

        The following approach builds up increasing sets of coins that can make change for all values from 1..n, by adding a coin to the previous sets. We can expand a set by adding another instance of the largest denomination in the set, or we can add a larger denomination coin only if we can already make all values lower than it.

        This program runs (under PyPy) in 125ms (internal runtime is 37ms).

        Run: [ @replit ]

        from enigma import (multiset, printf)
        
        # coin denominations (in pence)
        coins = (1, 2, 5, 10, 20, 50, 100, 200)
        
        # build up increasing sets of coins that cover range 1..n
        assert 1 in coins
        (ss, tgt, done) = ([(0, [])], 0, 0)
        while not done:
          tgt += 100
          ss_ = list()
          # consider current sets
          for (n, vs) in ss:
            m = (vs[-1] if vs else 0)
            # and try to add in another coin
            for d in coins:
              if d < m: continue
              if d - 1 > n: break
              (n_, vs_) = (n + d, vs + [d])
              if n_ == tgt:
                printf("{k} coins -> {vs}", k=len(vs_), vs=multiset.from_seq(vs_).map2str(arr="p x", sep="; "))
                done += 1
              ss_.append((n_, vs_))
          ss = ss_
        
        printf("[{done} solutions]")
        

        Note that with the following 16 coins we can make change for all values up to 1610p:

        1× 1p
        2× 2p
        1× 5p
        1× 10p
        2× 20p
        1× 50p
        1× 100p
        7× 200p
        total = £16.10 in 16 coins

        Like

      • Frits's avatar

        Frits 2:20 pm on 5 July 2024 Permalink | Reply

        Jim’s clever latest program can use a lot of memory and become quite slow for some denominations (like “pypy3 teaser2578.py 1 2 5 10 39 71 103 131”).
        His program suggested to me there might be a minimal number of pounds to have a solution. This minimal number of pounds can be used with his earlier program (unfortunately not with my program (of which I made a similar recursive version)).

        from enigma import (irange, inf, multiset, map2str, printf)
        from sys import argv
        
        # coin denominations (in pence)
        coins = ([1, 2, 5, 10, 20, 50, 100, 200] if len(argv) == 1 else
                 list(int(x) for x in argv[1:]))
        
        # build up increasing sets of coins that cover range 1..n
        assert 1 in coins
        coins.sort()
        
        print(f"{coins = }") 
        
        # determine the minimal number of pounds for which a solution is feasible
        (tgt, ss, done, strt) = (0, [(0, [])], 0, 10)
        while not done and tgt < 1000000:
          tgt += 100
          if tgt - ss[-1][0] <= coins[-1]:   # within reach
            if tgt > coins[-1]:  
              strt = tgt // 100
              print("minimal number of pounds:", strt)
              done = 1
              continue 
          
          ss_ = list()
          # consider current sets
          for (n, vs) in ss:
            m = (vs[-1] if vs else 0)
            # and try to add in another high coin
            for d in coins[::-1]:
              if d < m: continue     # not below minimum 
              # don't choose a coin higher than <n + 1>
              # otherwise number <n + 1> can never me made
              if d - 1 > n: continue
              ss_.append((n + d, vs + [d]))
              break # we have found the highest coin
          ss = ss_
          
        
        if strt > 26:  # this can be changed to a better value
          print("Jim 1")
          # consider total value in pounds
          for n in irange(strt, inf):
            done = 0
            # make a total of n pounds using exactly n coins
            tot = 100 * n
            for vs in multiset.from_seq(coins, count=n).express(tot, n):
              # can we make all other values between 1 and tot using these coins?
              #if not vs.expressible(irange(1, tot - 1)): continue
              if not vs.expressible(irange(1, max(vs) - 1)): continue
              # output solution
              printf("{n} coins -> {vs}", vs=vs.map2str(arr='p x', sep='; '))
              done = 1
            if done: break  
        else:
          print("Jim 2")
          (tgt, ss, done) = (0, [(0, [])], 0)
          while not done:
            tgt += 100
            ss_ = list()
            # consider current sets
            for (n, vs) in ss:
              m = (vs[-1] if vs else 0)
              # and try to add in another coin
              for d in coins:
                if d < m: continue
                if d - 1 > n: break
                (n_, vs_) = (n + d, vs + [d])
                if n_ == tgt:
                  printf("{k} coins -> {vs}", k=len(vs_), vs=multiset.from_seq(vs_).map2str(arr="p x", sep="; "))
                  done += 1
                ss_.append((n_, vs_))
            ss = ss_
           
          printf("[{done} solutions]")     
        

        Like

    • Frits's avatar

      Frits 1:25 pm on 1 July 2024 Permalink | Reply

      For this program I didn’t try to keep every line within 80 bytes (it is already a bit messy).
      I used a performance enhancement I don’t understand myself and I used break statements that I theoretically are not supposed to make. So far I have not encountered a case that differs from Jim’s program. The programs supports different coin denominations and also more than 8.

      With the performance enhancement the program runs in 21ms.

      from math import ceil
      from sys import argv
      
      # coin denominations
      coins = (1, 2, 5, 10, 20, 50, 100, 200) if len(argv) == 1 else list(int(x) for x in argv[1:])
      N = len(coins)
      M = 20      # maximum quantity
      vars = "abcdefghijklmnopqrstuxwxyz"
      print(f"{coins = }, {N = }")
      mn = 1e8    # minimum number of coins
      
      # make number <n> for specific deniminations and quantities
      def make(n, t, k, denoms, qs, ns=set(), ss=[]):
        if t == 0:
          yield ns | {n}
        if k :
          v, q = denoms[0], qs[0]
          ns.add(n - t)
          for i in range(min(q, t // v) + 1):
            yield from make(n, t - i * v, k - 1, denoms[1:], qs[1:], ns, ss + [i])
         
      # make all numbers from <strt> to <n>      
      def make_all(n, denoms, qs, strt=1):
        todo = set(i for i in range(strt, n + 1))  
        ln = len(denoms)
        while todo:
          n = max(todo)
          # make number <n>
          for made in make(n, n, ln, denoms, qs): 
            break
          else: return False  
          
          todo -= made
        return True
      
      # enhance performance (don't know why this is the case)
      if N < 9:
        _ = make_all(sum(qs := [2] * N) * 100, coins, qs)
        
      exprs = ""
      
      exprs += "mn, cnt = 1e8, 0\n\n"  
      exprs += "def check(lev, qs, tv, mn, make=1):\n"
      exprs += "  global cnt\n"  
      exprs += f"  if mn < 1e8:\n"
      exprs += "    # calculate minimum last quantity\n"  
      exprs += "    h_ = ceil((100 * mn - tv) / coins[-1])\n"  
      exprs += "    if sum(qs) + h_ > mn:\n"  
      exprs += "      return 'break'\n"  
      
      exprs += "  if make:\n"  
      exprs += "    n = coins[lev] - 1\n"  
      exprs += "    if tv < n: return False \n"  
      exprs += "    strt = 1 + coins[lev - 1] if lev > 1 else 1\n"  
      exprs += "    cnt += 1\n"  
      exprs += "    # can we make numbers <strt>...<n> with these denominations?\n"  
      exprs += "    if not make_all(n, coins[:lev], qs, strt):\n"  
      exprs += "      return False\n"
      exprs += "  return True\n\n"  
      
      exprs += f"make_{vars[0]} = 1\n"
      exprs += f"# can we make numbers 1..<total pence> with denominations {coins}?\n"
      
      last_odd = [i for i, x in enumerate(coins) if x % 2][-1]
      sols = dict()
      
      for i in range(1, N + 1):
        if i < N: 
          # our worst case scenario is if we have only one odd denomination and we check
          # all combinations with only one coin of 1 with no chance of achieving an 
          # even number of pence
          if last_odd == 0 and i == 1:
            exprs += f"{'  '* (i - 1)}for {vars[i - 1]} in range({coins[1] - (coins[1] % 2)}, {max(coins[1] - (coins[1] % 2) + 15, M) if i != N - 1 else 2 * M}, 2):\n"
          elif last_odd == i - 1 and i != N:
            exprs += f"{'  '* (i - 1)}for {vars[i - 1]} in range(prod_{vars[i - 2]} % 2, max(prod_{vars[i - 2]} % 2 + 15, M) if i != N - 1 else {2 * M}, 2):\n"  
          else:  
            exprs += f"{'  '* (i - 1)}for {vars[i - 1]} in range({coins[1] - 1 if i == 1 else 0}, {max(coins[1] + 14 if i == 1 else 0, M) if i != N - 1 else 2 * M}):\n"
            
          if i == N - 1: 
            exprs += f"{'  '* i}tot = sum(lst_{vars[i - 2]}) + {vars[i - 1]}\n"
            if coins[-2] != 100:
              exprs += f"{'  '* i}# calculate last quantity so that the total is a multiple of 100\n"
              exprs += f"{'  '* i}{(last := vars[i])}, rest = divmod(prod_{vars[i - 2]} + {vars[i - 1]} * {coins[i - 1]} - "
              exprs += f"100 * tot, 100 - coins[-1])\n"
              exprs += f"{'  '* i}if rest or {last} < 0: continue\n"  
          
          exprs += f"{'  '* i}# can we make numbers 1..{coins[i] - 1} with denominations {coins[:i]}\n"
          exprs += f"{'  '* i}if not (chk := check({i}, lst_{vars[i - 1]} := [{', '.join(vars[:i])}],\n"
          if i > 1:
            exprs += f"{'  '* i}                        prod_{vars[i - 1]} := prod_{vars[i - 2]} + {vars[i - 1]} * {coins[i - 1]},\n"
          else:
            exprs += f"{'  '* i}                        prod_{vars[i - 1]} := {vars[i - 1]} * {coins[i - 1]},\n"  
          exprs += f"{'  '* i}                        mn, make_{vars[i - 1]})): continue\n"
          exprs += f"{'  '* i}if chk == 'break': break\n"
          exprs += f"{'  '* i}make_{vars[i - 1]}, make_{vars[i]} = 0, 1   # don't try to make 1..x anymore in this loop\n"
        
        if i == N - 2 and coins[-2] == 100: 
          exprs += f"{'  '* i}# we can already calculate last quantity\n"
          exprs += f"{'  '* i}{(last := vars[N - 1])}, rest = divmod(prod_{vars[i - 1]} - 100 * sum(lst_{vars[i - 1]}), 100 - coins[-1])\n"
          exprs += f"{'  '* i}if rest or {last} < 0: continue\n"
          
        if i == N: 
          exprs += f"{'  '* (i - 1)}if tot + {last} > mn: break\n"
          exprs += f"{'  '* (i - 1)}mn = tot + {last}\n"
          exprs += f"{'  '* (i - 1)}sols[mn] = sols.get(mn, []) + [lst_{vars[i - 2]} + [{last}]]\n"
      
      exprs += "if mn not in sols:\n"   
      exprs += "  print('ERROR: no solutions, mn =', mn)\n"   
      exprs += "else:\n"   
      exprs += "  print(f'solutions: {len(sols[mn])}            {cnt} calls to make_all')\n"    
      exprs += "  for s in sols[mn]: print('-------- number of coins:', mn, s)\n"   
      
      #print(exprs)  
      exec(exprs) 
      

      The following code is generated:

      mn, cnt = 1e8, 0
      
      def check(lev, qs, tv, mn, make=1):
        global cnt
        if mn < 1e8:
          # calculate minimum last quantity
          h_ = ceil((100 * mn - tv) / coins[-1])
          if sum(qs) + h_ > mn:
            return 'break'
        if make:
          n = coins[lev] - 1
          if tv < n: return False
          strt = 1 + coins[lev - 1] if lev > 1 else 1
          cnt += 1
          # can we make numbers <strt>...<n> with these denominations?
          if not make_all(n, coins[:lev], qs, strt):
            return False
        return True
      
      make_a = 1
      # can we make numbers 1..<total pence> with denominations (1, 2, 5, 10, 20, 50, 100, 200)?
      for a in range(1, 20):
        # can we make numbers 1..1 with denominations (1,)
        if not (chk := check(1, lst_a := [a],
                                prod_a := a * 1,
                                mn, make_a)): continue
        if chk == 'break': break
        make_a, make_b = 0, 1   # don't try to make 1..x anymore in this loop
        for b in range(0, 20):
          # can we make numbers 1..4 with denominations (1, 2)
          if not (chk := check(2, lst_b := [a, b],
                                  prod_b := prod_a + b * 2,
                                  mn, make_b)): continue
          if chk == 'break': break
          make_b, make_c = 0, 1   # don't try to make 1..x anymore in this loop
          for c in range(prod_b % 2, max(prod_b % 2 + 15, M) if i != N - 1 else 40, 2):
            # can we make numbers 1..9 with denominations (1, 2, 5)
            if not (chk := check(3, lst_c := [a, b, c],
                                    prod_c := prod_b + c * 5,
                                    mn, make_c)): continue
            if chk == 'break': break
            make_c, make_d = 0, 1   # don't try to make 1..x anymore in this loop
            for d in range(0, 20):
              # can we make numbers 1..19 with denominations (1, 2, 5, 10)
              if not (chk := check(4, lst_d := [a, b, c, d],
                                      prod_d := prod_c + d * 10,
                                      mn, make_d)): continue
              if chk == 'break': break
              make_d, make_e = 0, 1   # don't try to make 1..x anymore in this loop
              for e in range(0, 20):
                # can we make numbers 1..49 with denominations (1, 2, 5, 10, 20)
                if not (chk := check(5, lst_e := [a, b, c, d, e],
                                        prod_e := prod_d + e * 20,
                                        mn, make_e)): continue
                if chk == 'break': break
                make_e, make_f = 0, 1   # don't try to make 1..x anymore in this loop
                for f in range(0, 20):
                  # can we make numbers 1..99 with denominations (1, 2, 5, 10, 20, 50)
                  if not (chk := check(6, lst_f := [a, b, c, d, e, f],
                                          prod_f := prod_e + f * 50,
                                          mn, make_f)): continue
                  if chk == 'break': break
                  make_f, make_g = 0, 1   # don't try to make 1..x anymore in this loop
                  # we can already calculate last quantity
                  h, rest = divmod(prod_f - 100 * sum(lst_f), 100 - coins[-1])
                  if rest or h < 0: continue
                  for g in range(0, 40):
                    tot = sum(lst_f) + g
                    # can we make numbers 1..199 with denominations (1, 2, 5, 10, 20, 50, 100)
                    if not (chk := check(7, lst_g := [a, b, c, d, e, f, g],
                                            prod_g := prod_f + g * 100,
                                            mn, make_g)): continue
                    if chk == 'break': break
                    make_g, make_h = 0, 1   # don't try to make 1..x anymore in this loop
                    if tot + h > mn: break
                    mn = tot + h
                    sols[mn] = sols.get(mn, []) + [lst_g + [h]]
      if mn not in sols:
        print('ERROR: no solutions, mn =', mn)
      else:
        print(f'solutions: {len(sols[mn])}            {cnt} calls to make_all')
        for s in sols[mn]: print('-------- number of coins:', mn, s)
      

      Like

      • Ruud's avatar

        Ruud 3:12 pm on 1 July 2024 Permalink | Reply

        You could generate exprs with:

        exprs = f"""\
        mn, cnt = 1e8, 0
        
        def check(lev, qs, tv, mn, make=1):
          global cnt
          if mn < 1e8:
            # calculate minimum last quantity
            h_ = ceil((100 * mn - tv) / coins[-1])
            if sum(qs) + h_ > mn:
              return 'break'
          if make:
            n = coins[lev] - 1
            if tv < n: return False
            strt = 1 + coins[lev - 1] if lev > 1 else 1
            cnt += 1
            # can we make numbers <strt>...<n> with these denominations?
            if not make_all(n, coins[:lev], qs, strt):
              return False
          return True
        
        make_{vars[0]} = 1
        # can we make numbers 1..<total pence> with denominations {coins}?
        """
        

        No change in functionality, just easier to understand and maintain.

        Like

        • Frits's avatar

          Frits 5:27 pm on 1 July 2024 Permalink | Reply

          Thanks, I had seen it before but had forgotten it.

          I also tried to use make() and make_all() in this way but had to use
          yield ns | {{n}}
          for my original line 15. I hoped in making these 2 functions local as well I wouldn’t need the performance enhancement but it is still lowers the run time.

          Like

      • Frits's avatar

        Frits 5:44 pm on 1 July 2024 Permalink | Reply

        Does anybody have an explanation why line 38 (in the first program) lowers the run time? Is it a memory thing and does it also occur on a linux system?

        Like

    • Frits's avatar

      Frits 9:26 pm on 18 July 2024 Permalink | Reply

      from sys import argv
      from math import ceil
      from time import perf_counter
      
      # coin denominations
      coins = [1, 2, 5, 10, 20, 50, 100, 200] if len(argv) == 1 else \
               list(int(x.replace(",", "", 1)) for x in argv[1:])
      coins.sort()
      N = len(coins)
      assert 1 in coins and coins[-1] > 100 and len(set(coins)) == N
      last_odd = [i for i, x in enumerate(coins) if x % 2][-1]
      mx_factor = max(divs := [ceil(coins[i + 1] / coins[i]) for i in range(N - 1)])
      print(f"{coins = }")
      
      # solve problem by adding coins of <N> different denominations  
      def solve(ds, k=0, tv=0, nc=0, qs=[]):
        global mn
        if k == N - 1:
          # we can calculate the quantity of the highest denomination
          last, r = divmod(tv - 100 * nc, 100 - ds[-1])
          if r or last < 0 or (nc_ := nc + last) > mn: return
          mn = nc_
          yield qs + [last]
        else:
          min_i = max(0, ceil((ds[k + 1] - tv - 1) / ds[k]))
          if k == last_odd:
            min_i += (tv + min_i) % 2
          
          remaining = 100 * mn - tv
          # minimum elements to follow = ceil((remaining - (i * ds[k])) / ds[-1])
          # nc + i + minimum elements to follow <= mn 
          max_i = min((ds[-1] * (mn - nc) - remaining) // (ds[-1] - ds[k]),
                  mn - nc,
                  # do not make a total which is higher than the minimum
                  remaining // ds[k])
          
          if max_i < min_i: 
            return
            
          if k < N - 2:
            # with <j - 1> quantities can we still make numbers up to ds[j] - 1
            # if we choose <i> of ds[k]    (for j >= k + 2)
            # tv + i * ds[k] + (mn - nc - i) * ds[j - 1] >= ds[j] - 1
            mx1 = min(((mn - nc) * ds[j - 1] + tv - c + 1) // (ds[j - 1] - ds[k])
                    for j, c in enumerate(ds[k + 2:], k + 2))
            max_i = min(max_i, mx1)
          
          for i in range(min_i, max_i + 1, 1 if k != last_odd else 2):
            yield from solve(ds, k + 1, tv + i * ds[k], nc + i, qs + [i])
          
      sols = dict()
      mn = mx_factor + 27 # solving approx 80% of all cases in one pass 
      inc = 15
      cnt = 0
      while not sols:
        print(f"try {mn = }")
        M = mn
        for s in solve(coins):
          cnt += 1
          # limit the number of printed solutions (causing slowness due to memory)
          if cnt > 999 and mn in sols: continue    
          if mn not in sols: 
            sols = dict()
            cnt = 1
          sols[mn] = sols.get(mn, []) + [s]
           
        if not sols:
          mn += inc  
      
      for s in sols[mn]: print('-------- number of coins:', mn, s)  
      print(f'[{cnt:>5} solutions]')     
      

      solving 1000 random testcases in approximately 200 seconds.

      from random import sample
      from os import system
      from sys import argv, stdout, stderr
      
      N = 8
      
      c = 0
      for _, _ in enumerate(iter(bool, 1), 1): # infinite loop     
        c += 1
        if c == 1001: break
        lst = [1] + sorted(sample(range(2, 2001), N - 1))
        if lst[-1] < 101: continue
        
        lst = ' '.join(str(x) for x in lst)
        print(f"{c = } {lst = }", file=stderr)
        system("python TheRightMoney.py " + lst)
      

      Like

  • Unknown's avatar

    Jim Randell 5:08 pm on 13 June 2024 Permalink | Reply
    Tags:   

    Teaser 2577: Number waves 

    From The Sunday Times, 12th February 2012 [link] [link]

    I challenged Sam to find a 9-digit number using all of the digits 1 to 9, such that each digit occupying an even position was equal to the sum of its adjacent digits. (For example, the digit occupying position four would be equal to the sum of the digits occupying positions three and five). Sam produced a solution that was exactly divisible by its first digit, by its last digit, and by 11.

    What was his number?

    [teaser2577]

     
    • Jim Randell's avatar

      Jim Randell 5:08 pm on 13 June 2024 Permalink | Reply

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It runs in 77ms. (Internal runtime of the generated program is 230µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # suppose the number is: ABCDEFGHI
      #                  pos = 123456789
      
      SubstitutedExpression
      
      --digits="1-9"
      
      # even positions are the sum of the adjacent digits
      "A + C = B"
      "C + E = D"
      "E + G = F"
      "G + I = H"
      
      # solution is divisible by A, I and 11
      "ABCDEFGHI % A = 0"
      "ABCDEFGHI % I = 0"
      "ABCDEFGHI % 11 = 0"
      
      --answer="ABCDEFGHI"
      

      Solution: Sam’s number was: 143968275.

      Like

      • Ruud's avatar

        Ruud 7:49 pm on 15 June 2024 Permalink | Reply

        No a priori knowledge, just brute force:

        from istr import istr
        
        for i in istr.concat(istr.permutations(istr.digits("1-"), 9)):
            if (
                i[1] == i[0] + i[2]
                and i[3] == i[2] + i[4]
                and i[5] == i[4] + i[6]
                and i[7] == i[6] + i[8]
                and i.divisible_by(11)
                and i.divisible_by(i[0])
                and i.divisible_by(i[8])
            ):
                print(i)
        
        

        Note that this requires the latest-soon to be published- version of istr.

        Like

    • GeoffR's avatar

      GeoffR 6:56 pm on 13 June 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:a; var 1..9:b; var 1..9:c;
      var 1..9:d; var 1..9:e; var 1..9:f;
      var 1..9:g; var 1..9:h; var 1..9:i;
      
      constraint all_different([a,b,c,d,e,f,g,h,i]);
      
      var 123456789..987654321:N == a*pow(10,8) + b*pow(10,7) + c*pow(10,6) 
      + d*pow(10,5) + e*pow(10,4) + f*pow(10,3) + g*pow(10,2) + h*pow(10,1) + i;
       
      constraint N mod a == 0;
      constraint N mod i == 0;
      constraint N mod 11 == 0;
      
      constraint a + c == b /\ c + e == d /\ e + g == f /\ g + i ==h;
      
      solve satisfy;
       
      output [ "His number was " ++ show(N)];
      
      % His number was 143968275
      % ----------
      % ==========
      % Finished in 461 msec - Chuffed Solver
      
      

      Like

    • Frits's avatar

      Frits 7:01 pm on 13 June 2024 Permalink | Reply

           
      #! python3 -m enigma -r
       
      # suppose the number is: ABCDEFGHI
      #                  pos = 123456789
       
      SubstitutedExpression
       
      --digits="1-9"
      
      # divisibility of 11 rule, abs(sum odd digits - sum even digits) is 0 or a 
      # multiple of 11, sum odd: A + C + E + G + I, sum even: A + 2.(C + G + E) + I
       
      "(11 if C + E < 11 else 22) - (C + E) = G"
       
      # even positions are the sum of the adjacent digits
      "A + C = B"
      "C + E = D"
      "E + G = F"
      "G + I = H"
       
      # solution is divisible by A and I
      "ABCDEFGHI % A = 0"
      "ABCDEFGHI % I = 0"
       
      --answer="ABCDEFGHI"
      

      Like

      • Frits's avatar

        Frits 9:23 pm on 13 June 2024 Permalink | Reply

        You only need to know three digits (like the 1st, 3rd and 5th digits).

        from functools import reduce
        
        # convert digit sequences to number
        d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
        
        dgts8 = set(range(1, 9))
        dgts9 = dgts8 | {9}
        
        for C in dgts8:
          for E in dgts8 - {C}:
            # 11 divisibility rule
            G = (11 if C + E < 11 else 22) - (C + E)
            D, F = C + E, E + G
            # different digits
            if len(r1 := dgts9 - {C, E, G, D, F}) != 4: continue
            for A in r1:
              B = A + C
              if len(r2 := sorted(r1 - {A, B})) != 2: continue
              H, I = r2[1], r2[0]
              if H != G + I: continue
              
              ABCDEFGHI = d2n([A, B, C, D, E, F, G, H, I])
              if ABCDEFGHI % A: continue
              if ABCDEFGHI % I: continue
              print("answer:", ABCDEFGHI)     
        

        Like

    • Ruud's avatar

      Ruud 6:44 am on 16 June 2024 Permalink | Reply

      This version does not require the divisible_by method:

      from istr import istr
      
      for i in istr.concat(istr.permutations(istr.digits("1-"), 9)):
          if i[1] == i[0] + i[2] and i[3] == i[2] + i[4] and i[5] == i[4] + i[6] and i[7] == i[6] + i[8] and i % 11 == 0 and i % i[0] == 0 and i % i[8] == 0:
              print(i)
      

      Like

  • Unknown's avatar

    Jim Randell 5:20 pm on 6 June 2024 Permalink | Reply
    Tags:   

    Teaser 2573: Prime cricketers 

    From The Sunday Times, 15th January 2012 [link] [link]

    George and Martha support their local cricket team and keep records of all the results. At the end of last season, they calculated the total number of runs scored by each of the 11 players. George commented that the totals were all different three-digit palindromic primes. Martha added that the average of the 11 totals was also a palindromic prime.

    What were the two highest individual totals?

    [teaser2573]

     
    • Jim Randell's avatar

      Jim Randell 5:21 pm on 6 June 2024 Permalink | Reply

      Very straightforward constructive solution.

      This Python program runs in 57ms. (Internal runtime is 877µs).

      Run: [ @replit ]

      from enigma import (primes, is_npalindrome, subsets, div, printf)
      
      # 3-digit palindromic primes
      scores = list(n for n in primes.between(100, 999) if is_npalindrome(n))
      
      # choose 11 different scores
      for ss in subsets(scores, size=11):
        # calculate the mean
        avg = div(sum(ss), 11)
        # which is also in scores
        if avg and avg in scores:
          # output solution
          printf("{avg} -> {ss}")
      

      Solution: The two highest individual totals are 787 and 919.

      The full list of totals is:

      101, 131, 151, 181, 191, 313, 353, 373, 383, 787, 919
      mean = 353

      Like

    • Ruud's avatar

      Ruud 6:41 pm on 6 June 2024 Permalink | Reply

      Very similar to @Jim Randell’s solution, but without enigma:

      import sympy
      from itertools import combinations
      
      palindromic_primes_with_length_3 = []
      for i in range(1, 1000):
          prime = sympy.prime(i)
          prime_as_str = str(prime)
          if len(prime_as_str) > 3:
              break
          if len(prime_as_str) == 3 and str(prime_as_str) == str(prime_as_str)[::-1]:
              palindromic_primes_with_length_3.append(prime)
      
      solutions = set()
      for scores in combinations(palindromic_primes_with_length_3, 11):
          average_score = sum(scores) / 11
          if average_score in palindromic_primes_with_length_3:
              solutions.add((scores, int(average_score)))
      print(solutions)
      

      Like

      • Frits's avatar

        Frits 11:28 am on 7 June 2024 Permalink | Reply

        Sympy also has a primerange() method.

        Like

    • GeoffR's avatar

      GeoffR 8:16 pm on 6 June 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 100..999:A; var 100..999:B; var 100..999:C; var 100..999:D;
      var 100..999:E; var 100..999:F; var 100..999:G; var 100..999:H;
      var 100..999:I; var 100..999:J; var 100..999:K; 
      
      constraint all_different([A,B,C,D,E,F,G,H,I,J,K]);
      
      predicate is_prime(var int: x) = 
       x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) ((i < x) -> (x mod i > 0));
       
      constraint A div 100 == A mod 10 /\ is_prime(A);
      constraint B div 100 == B mod 10 /\ is_prime(B);
      constraint C div 100 == C mod 10 /\ is_prime(C);
      constraint D div 100 == D mod 10 /\ is_prime(D);
      constraint E div 100 == E mod 10 /\ is_prime(E);
      constraint F div 100 == F mod 10 /\ is_prime(F);
      constraint G div 100 == G mod 10 /\ is_prime(G);
      constraint H div 100 == H mod 10 /\ is_prime(H);
      constraint I div 100 == I mod 10 /\ is_prime(I);
      constraint J div 100 == J mod 10 /\ is_prime(J);
      constraint K div 100 == K mod 10 /\ is_prime(K);
      
      var 1000..9999:asum;
      var 100..999:average;
      
      constraint asum  == A+B+C+D+E+F+G+H+I+J+K;
      constraint average == asum div 11 /\ asum mod 11 == 0;
      
      constraint is_prime(average);
      
      constraint card( {average} intersect {A,B,C,D,E,F,G,H,I,J,K}) == 1;
      
      constraint increasing( [A,B,C,D,E,F,G,H,I,J,K]);
      
      solve satisfy;
      
      output["Scores are " ++ show([A,B,C,D,E,F,G,H,I,J,K])
      ++ "\n" ++ "Average = " ++ show(average) ];
      
      % Scores are [101, 131, 151, 181, 191, 313, 353, 373, 383, 787, 919]
      % Average = 353
      % ----------
      % ==========
      
      

      Like

      • GeoffR's avatar

        GeoffR 9:39 am on 7 June 2024 Permalink | Reply

        Shorter code, but still slow.

        % A Solution in MiniZinc
        include "globals.mzn";
        
        % Eleven cricket scores
        var 100..999:A; var 100..999:B; var 100..999:C; var 100..999:D;
        var 100..999:E; var 100..999:F; var 100..999:G; var 100..999:H;
        var 100..999:I; var 100..999:J; var 100..999:K; 
         
        constraint all_different([A,B,C,D,E,F,G,H,I,J,K]);
        
        var 100..999:average;
         
        predicate is_pr_pal(var int: x) = 
         x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) 
         ((i < x) -> (x mod i > 0)) /\ x div 100 == x mod 10;
         
        constraint sum([is_pr_pal(A), is_pr_pal(B),is_pr_pal(C), is_pr_pal(D),
         is_pr_pal(E), is_pr_pal(F), is_pr_pal(G), is_pr_pal(H),
         is_pr_pal(I), is_pr_pal(J), is_pr_pal(K)]) == 11;
           
        constraint average == (A+B+C+D+E+F+G+H+I+J+K) div 11
        /\ (A+B+C+D+E+F+G+H+I+J+K) mod 11 == 0;
         
        constraint is_pr_pal(average);
         
        constraint card( {average} intersect {A,B,C,D,E,F,G,H,I,J,K}) == 1;
         
        constraint increasing( [A,B,C,D,E,F,G,H,I,J,K]);
         
        solve satisfy;
         
        output["Scores are " ++ show([A,B,C,D,E,F,G,H,I,J,K])
        ++ "\n" ++ "Average = " ++ show(average) ];
         
        
        
        

        Like

    • GeoffR's avatar

      GeoffR 9:57 pm on 6 June 2024 Permalink | Reply

      
      from enigma import is_prime
      from itertools import combinations
      
      scores = [x for x in range(100,999) if is_prime(x) \
                if x // 100 == x % 10 ]
      
      for s in combinations(scores, 11):
          av, r = divmod(sum(s), 11)
          if r != 0: continue
          if av in scores and av //100 == av % 10:
              print(f"Average = {av}")
              print(f"Scores = {s}")
      

      Like

      • Ruud's avatar

        Ruud 10:20 am on 7 June 2024 Permalink | Reply

        You can leave out the av // 100 == av % 10 test in the final if condition, as that’d guaranteed by the membership of scores.

        Like

    • Frits's avatar

      Frits 10:17 am on 7 June 2024 Permalink | Reply

      Same number of iteratons. It also works if scores would have had exactly 11 items.

      from itertools import combinations
      
      # prime numbers from 101-999
      P = [3, 5, 7]
      P += [x for x in range(11, 100, 2) if all(x % p for p in P)]
      P = [x for x in range(101, 1000, 2) if all(x % p for p in P)]
      # 3-digit palindromic primes
      scores = {p for p in P if p // 100 == p % 10}
      assert (ln := len(scores)) >= 11
      
      t = sum(scores)
      
      # choose scores outside the 11
      for ss in combinations(scores, ln - 11):
        # calculate the average
        avg, r = divmod(t - sum(ss), 11)
        # which is also in scores
        if r or avg not in scores: continue
        print("answer", sorted(scores.difference(ss))[-2:])     
      

      Like

  • Unknown's avatar

    Jim Randell 8:04 am on 16 May 2024 Permalink | Reply
    Tags:   

    Teaser 2615: Gunpowder, treason and plot 

    From The Sunday Times, 4th November 2012 [link] [link]

    Last year I overheard this conversation one day during the first seven days of November:

    Alec: It’s the 5th today, let’s have a bonfire.
    Bill: No, the 5th is tomorrow.
    Chris: You’re both wrong — the 5th is the day after tomorrow.
    Dave: All three of you are wrong.
    Eric: Yesterday was certainly not the 1st.
    Frank: We’ve missed bonfire night.

    If you knew how many of their statements were true, then you could work out the date in November on which this conversation took place.

    What was that date?

    In Britain, Bonfire night is 5th November.

    [teaser2615]

     
    • Jim Randell's avatar

      Jim Randell 8:05 am on 16 May 2024 Permalink | Reply

      A straightforward puzzle to solve, either manually or programatically.

      This Python program evaluates the statements on each of the 7 possible days, and looks for situations where the number of true statements uniquely identifies a single day.

      Run: [ @replit ]

      from enigma import (irange, group, seq2str, printf)
      
      # calculate statement values for date <d>
      def statements(d):
        # A: "It is the 5th today ..."
        A = (d == 5)
        # B: "The 5th is tomorrow ..."
        B = (d + 1 == 5)
        # C: "The 5th is the day after tomorrow ..."
        C = (d + 2 == 5)
        # D: "A, B, C are all wrong ..."
        D = not (A or B or C)
        # E: "Yesterday was not the 1st ..."
        E = (d - 1 != 1)
        # F: We have missed bonfire night (the 5th) ..."
        F = (d > 5)
        return [A, B, C, D, E, F]
      
      # group dates by the number of true statements
      g = group(irange(1, 7), by=(lambda d: statements(d).count(True)))
      
      # look for groups with a single member
      for (k, vs) in g.items():
        if len(vs) == 1:
          printf("{k} true -> {vs} November", vs=seq2str(vs, sort=1, enc=""))
      

      Solution: The conversation took place on 2nd November.

      There is only 1 true statement, and it is made by Dave.

      Like

    • Frits's avatar

      Frits 10:31 am on 16 May 2024 Permalink | Reply

      # the number of correct statements 
      fn = lambda d: sum([d == 5, d == 4, d == 3, d < 3 or d > 5, d != 2, d > 5])
       
      seen, sols = set(), dict()
      # now find a count that is unique
      for d in range(1, 8):
        if (c := fn(d)) in seen:
          try:
            del sols[c]
          except KeyError:
            pass
        else:
          sols[c] = str(d)  
          seen.add(c)  
          
      print(f"answer: November {' or '.join(sols.values())}")
      

      Like

    • Lise Andreasen's avatar

      Lise Andreasen 7:06 pm on 16 May 2024 Permalink | Reply

      T = the date today.
      A: T = 5.
      B: T = 4.
      C: T = 3.
      D: T != 3, 4, 5.
      E: T != 2.
      F: T = 6 v T = 7.
      Of A, B, C and D, 1 is true, 3 are false.
      There can therefore be 1, 2 or 3 true statements.
      With 3 true statements, both E and F are true. T could be 6 or 7.
      With 2 true statements, either E or F is true. If E is true, T = 2. If F is true, T could be 1, 3, 4 or 5.
      With 1 true statement, both E and F are false. T = 2. This is the only option leading to 1 answer. Therefore there was 1 true statement and it’s 2nd November.

      Like

    • GeoffR's avatar

      GeoffR 1:54 pm on 17 May 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..7: T; % Date conversation took place
      
      % A - F statements
      constraint sum([T==5, T==4, T==3, T!=3 \/ T!=4 \/ T!=5,
      T!=2, T==6 \/ T==7]) == 1;
      
      solve satisfy;
      
      output [" T = " ++ show(T)];
      
      % T = 2;
      %---------
      %=========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:33 pm on 9 May 2024 Permalink | Reply
    Tags:   

    Teaser 2610: Odd ages 

    From The Sunday Times, 30th September 2012 [link] [link]

    Recently Alex, Bernard and Charles were comparing their ages in years. Alex’s age was an odd two-digit number and Bernard’s age was a whole number percentage more than that. On the other hand, Charles’s age was also an odd two-digit number but Bernard’s age was a whole number percentage less than that. They were surprised to find that the percentage was the same in both cases.

    What were their three ages?

    [teaser2610]

     
    • Jim Randell's avatar

      Jim Randell 7:34 pm on 9 May 2024 Permalink | Reply

      Supposing the 3 are all different ages (so the percentage is non-zero).

      A’s age is an odd 2-digit number, and C’s age is a (larger) odd 2-digit number, and so B’s age is between them, hence also a 2-digit number.

      For percentage p we have:

      A (100 + p) / 100 = B
      C (100 − p) / 100 = B

      p = 100 (C − A) / (C + A)

      The following Python program runs in 67ms. (Internal runtime is 463µs).

      Run: [ @replit ]

      from enigma import (irange, subsets, div, printf)
      
      # ages for A and C (2-digit, odd)
      for (A, C) in subsets(irange(11, 99, step=2), 2):
        # calculate percentage
        p = div(100 * (C - A), C + A)
        if p is None: continue
        # calculate B
        B = div(A * (100 + p), 100)
        if B is None: continue
        # output solution
        printf("A={A} B={B} C={C} [p={p}%]")
      

      Solution: Alex is 15. Bernard is 21. Charles is 35.

      And the percentage in question is 40%:

      15 + 40% = 15 × (1 + 0.4) = 21
      35 − 40% = 35 × (1 − 0.4) = 21

      Like

    • Frits's avatar

      Frits 10:06 am on 10 May 2024 Permalink | Reply

      for a in range(11, 100, 2):
        # determine valid percentages, maximum percentage = (99 - 12) / 99 = 0.87878
        for p in [p for p in range(2, 88, 2) if (p * a) % 100 == 0]:
          b = a + (p * a) // 100
          c, r = divmod(100 * b, 100 - p)
          if c > 99: break
          if r or c % 2 == 0: continue
          print("answer:", (a, b, c))     
      

      Like

    • GeoffR's avatar

      GeoffR 10:35 am on 10 May 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 11..99:A; var 11..99:B; var 11..99:C;
      var 1..95:p; % Percentage
      
      constraint all_different([A, B, C]);
      constraint sum([A mod 2 == 1, B mod 2 == 1, C mod 2 == 1]) == 3;
      constraint 100*A + p*A == 100*B;
      constraint 100*C - p*C == 100*B;
      
      solve satisfy;
      
      output["[Alex, Bernard, Charles] = " ++ show([A, B, C]) ];
       
      % [Alex, Bernard, Charles] = [15, 21, 35]
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 3:02 pm on 7 May 2024 Permalink | Reply
    Tags:   

    Teaser 2617: A case of ambiguity 

    From The Sunday Times, 18th November 2012 [link] [link]

    Every schoolchild knows that it is possible for two triangles to have two of their lengths-of-sides in common and one of their angles in common without the two triangles being identical or “congruent”.

    This can happen when the common angle is not included between the two common sides. I have found such a pair of non-congruent triangles with all the lengths of the sides being a whole number of centimetres and with the longest side of each triangle being 10cm in length.

    What are the lengths of the other two sides of the smaller triangle?

    [teaser2617]

     
    • Jim Randell's avatar

      Jim Randell 3:02 pm on 7 May 2024 Permalink | Reply

      Each triangle has a longest side of 10, so the remaining two sides must be 1 – 9, so there aren’t that many triangles to consider.

      This Python program considers the side lengths of possible triangles and uses the cosine rule to calculate the internal angles, and then looks for two different triangles that share an angle.

      It runs in 72ms. (Internal runtime is 664µs).

      Run: [ @replit ]

      from enigma import (fraction as Q, irange, intersect, call, cache, printf)
      
      # return values as cosines (using cosine rule)
      cosA = lambda x, y, z: Q(y*y + z*z - x*x, 2*y*z)
      
      # find the angles of a triangle with sides x, y, z
      @cache
      def _angles(x, y, z):
        # is it a valid triangle?
        if x + y > z:
          return {cosA(x, y, z), cosA(y, z, x), cosA(z, x, y)}
      angles = lambda *ss: call(_angles, sorted(ss))
      
      # choose the shared side a
      for a in irange(1, 9):
        # choose the shorter remaining side b
        for b in irange(1, 8):
          angles1 = angles(a, b, 10)
          if not angles1: continue
          # choose the longer remaining side c
          for c in irange(b + 1, 9):
            angles2 = angles(a, c, 10)
            if not angles2 : continue
            # check for a common angle
            if intersect([angles1, angles2]):
              # output solution
              printf("({a}, {b}, 10) -> {angles1}; ({a}, {c}, 10) -> {angles2}")
      

      Solution: The other two sides of the smaller triangle are: 4cm and 8cm.

      The triangles look like this:

      And the common angle α is acos(13/20) ≈ 49.46°.

      Like

    • Frits's avatar

      Frits 5:46 pm on 7 May 2024 Permalink | Reply

      We also have 10^2 – 8^2 = 4 * 9.

      See (here).

      Like

      • Jim Randell's avatar

        Jim Randell 4:41 pm on 8 May 2024 Permalink | Reply

        This result follows directly from using the cosine rule on the shared angle of both triangles:

        Suppose the base of the triangle is d, and we can make two triangles with the same angle α using sides b, a and c, a.

        Then, using the cosine rule on both triangles:

        cos(α) = (d² + b² − a²)/2db = (d² + c² − a²)/2dc

        c(d² + b² − a²) = b(d² + c² − a²)
        (c − b)(d² − a²) − bc(c − b) = 0
        (c − b)(d² − a² − bc) = 0
        [b ≠ c] ⇒
        bc = d² − a² = (d − a)(d + a)

        Which gives a straightforward program:

        from enigma import (irange, divisors_pairs, printf)
        
        d = 10
        for a in irange(1, d - 1):
          for (b, c) in divisors_pairs(d*d - a*a):
            if b < c < d:
              printf("triangles = ({d}, {a}, {b}) ({d}, {a}, {c})")
        

        Like

    • GeoffR's avatar

      GeoffR 6:37 pm on 8 May 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 1..9:B; var 1..9:C;
      int: D == 10;  % Two triangles are ABD and ACD
      
      constraint B * C == D * D - A * A;
      constraint B < C /\ C < D;
      
      solve satisfy;
      output ["Two triangles are " ++ show([A,B,D]) ++ " and " ++ show([A,C,D]) ];
      
      % Two triangles are [8, 4, 10] and [8, 9, 10]
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 5:28 pm on 30 April 2024 Permalink | Reply
    Tags:   

    Teaser 2612: Star signs 

    From The Sunday Times, 14th October 2012 [link] [link]

    Five friends, Abel, Bob, Jedd, Tim and Will, were investigating birthdays. Abel commented that there was just one letter of the alphabet that occurred in both his star sign and in his month of birth.

    After further investigation he found that for any pair of his star sign, his month of birth, his name, and the day of the week on which he was born, just one letter of the alphabet occurred in both. Remarkably, the same turned out to be true for each of the friends.

    Their names are listed alphabetically. What, respectively, are their star signs?

    [teaser2612]

     
    • Jim Randell's avatar

      Jim Randell 5:29 pm on 30 April 2024 Permalink | Reply

      This is a “grouping” style problem (for which there is a solver in enigma.py), but in this case the non-primary values (i.e. signs, month, day) can be re-used, and the signs and months are not independent (i.e. a particular sign spans two consecutive months).

      This Python program runs in 67ms. (Internal runtime is 4.4ms).

      Run: [ @replit ]

      from enigma import grouping
      
      # names
      names = "Abel Bob Jedd Tim Will".split()
      
      # map months -> month number
      months = "January February March April May June July August September October November December".split()
      months = dict((x, i) for (i, x) in enumerate(months, start=1))
      
      # map signs -> valid month numbers
      signs = {
        "Aries": {3, 4},
        "Taurus": {4, 5},
        "Gemini": {5, 6},
        "Cancer": {6, 7},
        "Leo": {7, 8},
        "Virgo": {8, 9},
        "Libra": {9, 10},
        "Scorpio": {10, 11},
        "Sagittarius": {11, 12},
        "Capricorn": {12, 1},
        "Aquarius": {1, 2},
        "Pisces": {2, 3},
      }
      
      # days
      days = "Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split()
      
      # check a (<name>, <sign>, <month>, <day>) group
      def check(n, s, m, d, fn=grouping.share_letters(1)):
        # sign/month should match, as well as the shared letters
        return (months[m] in signs[s]) and fn(n, s, m, d)
      
      # find candidate groups (allowing repeated values)
      grouping.solve([names, signs.keys(), months.keys(), days], check, distinct=0)
      

      Solution: The star signs are: Pisces, Virgo, Leo, Virgo, Leo.

      The full solution is:

      Abel: Pisces, March, Sunday
      Bob: Virgo, September, Monday
      Jedd: Leo, July, Monday
      Tim: Virgo, August, Monday
      Will: Leo, July, Wednesday

      Like

    • Ruud's avatar

      Ruud 4:14 am on 2 May 2024 Permalink | Reply

      Here’s my solution:

      import itertools
      
      
      def day_month_to_zodiac(day, month):
          signs = [
              ("capricorn", 19),
              ("aquarius", 18),
              ("pisces", 20),
              ("aries", 19),
              ("taurus", 20),
              ("gemini", 20),
              ("cancer", 22),
              ("leo", 22),
              ("virgo", 22),
              ("libra", 22),
              ("scorpio", 21),
              ("sagittarius", 21),
              ("capricorn",),
          ]
          return signs[month - (day <= signs[month - 1][1])][0]
      
      
      month_names = "x january febuary march april may june july august september october november december".split()
      
      weekday_names = "monday tuesday wednesday thursday friday saturday sunday".split()
      
      for first_name in "abel bob jedd tim will".split():
          for month, weekday_name in itertools.product(range(1, 13), weekday_names):
              month_name = month_names[month]
              for zodiac_name in (day_month_to_zodiac(1, month), day_month_to_zodiac(31, month)):
                  if all(len(set(c0) & set(c1)) == 1 for c0, c1 in itertools.combinations((first_name, month_name, zodiac_name, weekday_name), 2)):
                      print(first_name, zodiac_name, month_name, weekday_name)
      

      Like

    • Frits's avatar

      Frits 11:24 am on 2 May 2024 Permalink | Reply

       
      first_names = "abel bob jedd tim will".split()
      
      zodiac_names = "capricorn aquarius pisces aries taurus gemini cancer " \
                     "leo virgo libra scorpio sagittarius".split()
      
      month_names = "january febuary march april may june july august " \
                    "september october november december".split()
      
      weekday_names = "monday tuesday wednesday thursday " \
                      "friday saturday sunday".split()
      
      # does sequence <x> share exactly one item with all sequences in <g>
      share1 = lambda x, g: all(len(set(x).intersection(z)) == 1 for z in g)
      
      sols = []
      for m, mo in enumerate(month_names, start=1):
        for zo in (zodiac_names[m - 1], zodiac_names[m % 12]):
          if not share1(zo, [mo]): continue
          for wd in weekday_names:
            if not share1(wd, [mo, zo]): continue
            for nm in first_names:
              if not share1(nm, [mo, zo, wd]): continue
              sols.append((nm, zo))
      
      print(', '.join(x[1] for x in sorted(sols)))    
      

      Like

  • Unknown's avatar

    Jim Randell 10:10 am on 17 April 2024 Permalink | Reply
    Tags:   

    Teaser 2607: Tribonacci 

    From The Sunday Times, 9th September 2012 [link] [link]

    In the Fibonacci sequence 1, 2, 3, 5, 8, 13, … each number after the second is the sum of the previous two. Pat has experimented with some “Tribonacci” sequences of positive whole numbers where each number after the third is the sum of the previous three.

    He chooses the first three numbers to be different and in increasing order and then generates the sequence. He has shown us one where just the first five numbers are less than a hundred and where a later number is ten thousand.

    What are the first three numbers in this sequence?

    [teaser2607]

     
    • Jim Randell's avatar

      Jim Randell 10:10 am on 17 April 2024 Permalink | Reply

      If the first 3 terms are a, b, c (where a < b < c) then the first 5 terms are:

      a, b, c, a+b+c, a+2b+2c, …

      And so the fifth term of the sequence must be at least:

      a + 2(a + 1) + 2(a + 2) = 5a + 6
      a + 2b + 2(b + 1) = a + 4b + 2

      We can use these to reduce the search space for the (a, b, c) values.

      This Python program runs in 78ms. (Internal runtime is 12ms).

      Run: [ @replit ]

      from enigma import (irange, fib, first, le, printf)
      
      # suppose the sequence starts (a, b, c, a+b+c, a+2b+2c, ...)
      # and these terms are all < 100
      for a in irange(1, 97):
        if not (5*a + 6 < 100): break
        for b in irange(a + 1, 98):
          if not (a + 4*b + 2 < 100): break
          for c in irange(b + 1, 99):
            if not (a + 2*b + 2*c) < 100: break
            if 2*a + 3*b + 4*c < 100: continue
      
            # calculate terms up to 10_000
            ss = first(fib(a, b, c), le(10000))
      
            # is the final term 10_000?
            if ss[-1] == 10000:
              # output solution sequence
              printf("{ss}")
      

      Solution: The first three numbers in the sequence are: 6, 11, 24.

      The terms of the sequence up to 10,000 are:

      [6, 11, 24, 41, 76, 141, 258, 475, 874, 1607, 2956, 5437, 10000]

      Like

      • Frits's avatar

        Frits 3:17 pm on 17 April 2024 Permalink | Reply

        “just the first five numbers are less than a hundred” :

        sixth term: 2a + 3b + 4c > 99

        Like

        • Jim Randell's avatar

          Jim Randell 3:29 pm on 17 April 2024 Permalink | Reply

          Good point. I’ve added a check in my program to ensure the 6th term is at least 100.

          Like

      • Frits's avatar

        Frits 8:30 pm on 17 April 2024 Permalink | Reply

        I wanted to start from the back of the sequence. It has been a lot of work.
        This might be automated with the sympy or z3 packages (or with Enigma’s Matrix.linear_solve).

        from math import ceil
        
        # round
        r = lambda n, k=0: int(round(n, k)) if k == 0 else round(n, k)
         
        # suppose the sequence ends like the following list
        s = [
          "18*y+27*z-200000", "-20*y-2*z+70000", "7*y-13*z+50000", "5*y+12*z-80000",
          "-8*y-3*z+40000",   "4*y-4*z+10000",   "y+5*z-30000",    "-3*y-2*z+20000",   
          "2*y-z",            "2*z-10000",       "-y-z+10000",     "y", "z", "10000"]
        
        # the sequence must be increasing:
        
        # 7*y-13*z+50K < 5*y+12*z-80K,   2*y < 25*z-130K,     y < 12.5*z - 65K
        # 5*y+12*z-80K < -8*y-3*z+40K,   13y < -15*z + 120K,  y < -15/13*z + 120K/13
        # -8*y-3*z+40K <  4*y-4*z+10K,   12y > z + 30K        y > z/12 + 2500
        #  4*y-4*z+10K < y+5*z-30K   ,   3*y < 9*z - 40K      y < 3*z - 40K/3 
        #  y+5*z-30K   < -3*y-2*z+20K,   4*y < -7*z + 50K     y < -7/4*z + 12500
        # -3*y-2*z+20K < 2*y-z       ,   5*y > -z + 20K       y > -z/5 + 4K
        # 2*y-z        < 2*z-10K     ,                        y < 1.5*z - 5K 
        
        
        # determine minimum for <z>
        # y < 12.5*z - 65K, z > y / 12.5 + 5200 and y > -z/5 + 4K 
        # z > -z / 62.5 + 5520,  63.5/62.5*z > 5520, z > 5433.07 
        
        # determine maximum for <z>
        # 15/13*z < -y + 120K/13 and y > -z/5 + 4K 
        # 15*z < 13*(z/5 - 4K) + 120K, z < 68K / 12.4 = 5483.87
        
        # maximum looks too high (knowing the answer) so:
        
        # 15/13*z < -y + 120K/13 and y > z/12 + 2500
        # 15*z < -13*(z/12 + 2500) + 120K, z < 12 * 87500 / 193 = 5440.41
        
        for z in range(5434, 5441):
          # determine boundaries for <y>
          mx = min(1.5*z - 5000, 12.5*z - 65000, -15/13*z + 120000/13, 
                   3*z - 40000/3, -7/4*z + 12500)
          mn = max(z / 12 + 2500, -z/5 + 4000)
          for y in range(int(mn) + 1, ceil(mx)):
            # calculate the sequence numbers
            ns = [eval(x) for x in s]  
            # determine the 5 numbers below 100
            b100 = [n for n in ns if n < 100]  
            if len(b100) < 5: continue
            f5 = b100[-5:]
            
            # five numbers must be increasing and without a valid previous number
            if sorted(f5) == f5 and f5[0] > 0 and \
               not (0 < f5[2] - f5[1] - f5[0] < f5[0]):
              print("answer:", f5[:3], "\n")
              # print the sequence
              for i, x in enumerate(s):
                print(f"{i + 1:>2}  {x:<20} {ns[i]:<10}")    
        

        Like

      • ruudvanderham's avatar

        ruudvanderham 8:18 am on 18 April 2024 Permalink | Reply

        Instead of a very clever, fast algorithm, here’s a super simple -brute force- solution without any reasoning beforehand.

        import itertools
        
        for i1, i2, i3 in itertools.combinations(range(1, 100), 3):
            n_min2 = i1 + i2 + i3
            n_min1 = i2 + i3 + n_min2
            if n_min2 < 100 and n_min1 < 100:
                n = i3 + n_min2 + n_min1
                while n < 10000:
                    n_min2, n_min1, n = n_min1, n, n_min2 + n_min1 + n
                if n == 10000:
                    print(i1, i2, i3)
        

        Like

      • Jim Randell's avatar

        Jim Randell 11:39 am on 18 April 2024 Permalink | Reply

        Here is a different approach, that I didn’t think would be very fast, but it turns out it has an internal runtime of 11ms. (Although it does stop when it finds the first solution).

        It works by generating the coefficients of (a, b, c) in each term, i.e.

        term 1 = 1a + 0b + 0c → (1, 0, 0)
        term 2 = 0a + 1b + 0c → (0, 1, 0)
        term 3 = 0a + 0b + 1c → (0, 0, 1)
        term 4 = 1a + 1b + 1c → (1, 1, 1)
        term 5 = 1a + 2b + 2c → (1, 2, 2)
        term 6 = 2a + 3b + 4c → (2, 3, 4)

        And then, when we reach sufficiently large terms, it looks for (a, b, c) values between 1 and 99 that would give the term a value of 10000. If an acceptable set of values is found we construct the sequence of terms up to 10000 and check the remaining conditions.

        Run: [ @replit ]

        from enigma import (unzip, express, fib, first, dot, printf)
        
        # solve the puzzle, by looking for a term = N
        def solve(N=10000):
          # a, b, c coefficients for terms 1, 2, 3
          ts = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
          k = 3
          # find the coefficients in the k'th term
          while True:
            k += 1
            cs = tuple(sum(ss) for ss in unzip(ts))
            if k > 5 and not (dot(cs, [97, 98, 99]) < N):
              # is the k'th term is N
              for (a, b, c) in express(N, cs, min_q=1, max_q=99):
                if not (a < b < c): continue
                # calculate the first k terms
                ss = first(fib(a, b, c), k)
                # check 5th term < 100 and 6th term >= 100
                if ss[4] < 100 and not (ss[5] < 100):
                  yield ss
            # move on to the next term
            ts.pop(0)
            ts.append(cs)
        
        for ss in solve():
          printf("{ss}")
          break
        

        A custom version of express() that only generates quantities in ascending order would probably make this code even faster.

        Like

      • Frits's avatar

        Frits 5:58 pm on 18 April 2024 Permalink | Reply

        Mix of Jim’s and Ruud’s code.

        # 5a + 6 < 100
        for a in range(1, 19):
          # a + 4 * b + 2 < 100
          for b in range(a + 1, (101 - a) // 4):
            # a + 2b + 2c < 100 and not all odd
            # sixth term 2 * a + 3 * b + 4 * c > 99
            mn = max(b + 1, (103 - 2 * a - 3 * b) // 4)
            mxplus1 = (101 - a - 2 * b) // 2
            for c in range(mn, mxplus1, 1 + (a % 2 and b % 2)):
              n_min2 = a + b + c
              n_min1 = a + 2 * b + 2 * c
              n = c + n_min2 + n_min1
              while n < 10000:
                n_min2, n_min1, n = n_min1, n, n_min2 + n_min1 + n
              if n == 10000:
                print("answer:", a, b, c)     
        

        Like

    • Hugo's avatar

      Hugo 11:03 am on 17 April 2024 Permalink | Reply

      I solved this rather differently.
      In a Fibonacci sequence, the ratio of successive terms tends to the positive root of the equation
      x^2 = x + 1. Here it tends to the real root of x^3 = x^2 + x + 1: x = 1.839 287 approx.

      I divided 10000 by that number twice, taking the nearest integer each time.
      The method may lead to incorrect values when they become small,
      but I continued with the relationship T(n) = T(n + 3) – T(n + 2) – T(n + 1)
      for successively smaller n and got the required answer.

      Like

    • GeoffR's avatar

      GeoffR 10:54 am on 18 April 2024 Permalink | Reply

      Not a rigorous solution – I had to use the fact that the answer is the 13th term, in order to get a solution in MiniZinc.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % First three terms of the series
      var 1..100: a; var 1..100: b; var 1..100: c;
      
      % Knowing the answer is the 13th term of the Tribonacci series...
      int:T13 == 10000;
      
      constraint a < b /\ b < c;
      
      % The algebraic value of the 13th term of the series
      constraint 149 * a + 230 * b + 274 * c == T13;
      
      solve satisfy;
      
      output ["First three numbers are " ++ show([a, b, c]) ];
      
      % First three numbers are [6, 11, 24]
      % ----------
      % ==========
      
      
      

      Like

    • Frits's avatar

      Frits 2:06 pm on 18 April 2024 Permalink | Reply

      An implementation with the z3 solver (of my previous program).

      Downside is that I had to fix the sequence length to 13. I tried to loop over sequence lengths but for some reason the program got really slow after one or 2 runs.

        
      from z3 import Solver, Int, simplify, sat
      
      s = Solver()
      
      M = 13 # length of final sequence
      
      # variables
      y, z = Int('y'), Int('z')
      
      # start from the back of the sequence
      a, b, c = "y", "z", "10000"
      
      seq = [a, b, c]
      eqs = ["y < z", "z < 10000"]
      # add <M - 3> previous numbers
      for _ in range(M - 3):
        a, b, c = c + " - (" + a + ") - (" + b + ")", a, b, 
        # simplify and prettify the new equation
        a_ = str(eval(f"simplify({a})"))
        seq = [a_.replace("+ -", "-").replace("-", "- ").replace(" 1*", " ")] + seq
        eval(f"s.add({a} < {b})")
      
      # first sequence number must be positive (otherwise y = 2957 is a solution)
      eval(f"s.add({seq[0]} > 0)")
      
      # run z3 solver
      if(s.check() == sat):
        m = s.model()
        # convert y and z to integers
        y = int(str(m[y]))
        z = int(str(m[z]))
        
        # calculate the sequence numbers
        ns = [eval(x) for x in seq]
        
        # determine the 5 numbers below 100
        b100 = [n for n in ns if n < 100]  
        if len(b100) >= 5: 
          f5 = b100[-5:]
      
          # five numbers must be increasing
          if sorted(f5) == f5 and f5[0] > 0:
            print("answer:", f5[:3], "\n")
            # print the sequence
            for i, x in enumerate(seq):
              print(f"{i + 1:>2}  {x:<22} {ns[i]:<10}")   
      

      Like

  • Unknown's avatar

    Jim Randell 9:01 am on 2 April 2024 Permalink | Reply
    Tags:   

    Teaser 2614: The end of time 

    From The Sunday Times, 28th October 2012 [link] [link]

    In the table the letters represent the numbers 1 to 25 in some order. In each row, each column and each main diagonal the sum of the five numbers is the same. If you list all the letters in increasing numerical order then somewhere in the list you will get …, S, U, N, D, A, Y, T, I, M,.. with E coming later.

    Which letters are equal to 11, 9, 7, 3, 23 and 22?

    [teaser2614]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 2 April 2024 Permalink | Reply

      The total of the numbers from 1 to 25 is 325. So the magic constant is 325/5 = 65.

      We can solve the puzzle using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      The following run file executes in 91ms. (Internal runtime of the generated program is 4.1ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --base=26
      --digits="1-25"
      
      # rows are magic
      "A + B + C + D + E == 65"
      "F + G + H + I + J == 65"
      "K + L + M + N + O == 65"
      "P + Q + R + S + T == 65"
      "U + V + W + X + Y == 65"
      
      # columns are magic
      "A + F + K + P + U == 65"
      "B + G + L + Q + V == 65"
      "C + H + M + R + W == 65"
      "D + I + N + S + X == 65"
      "E + J + O + T + Y == 65"
      
      # diagonals are magic
      "A + G + M + S + Y == 65"
      "E + I + M + Q + U == 65"
      
      # look for required subsequence
      "S + 1 = U"
      "U + 1 = N"
      "N + 1 = D"
      "D + 1 = A"
      "A + 1 = Y"
      "Y + 1 = T"
      "T + 1 = I"
      "I + 1 = M"
      "M < E"
      
      --template=""
      

      And here is a wrapper that assembles the required answer(!).

      from enigma import (SubstitutedExpression, rev, join, printf)
      
      # load the alphametic puzzle
      p = SubstitutedExpression.from_file("teaser2614.run")
      
      # solve it
      for s in p.solve(verbose=0):
        r = rev(s)
        # output solution
        vs = [11, 9, 7, 3, 23, 22]
        printf("{vs} -> {ks}", ks=join(r[v] for v in vs))
      

      Solution: The given values spell: ANSWER.

      The grid looks like this:

      And the letters ordered by value are:

      J B W K Q H S U N D A Y T I M O V P C G L R E F X

      Like

      • Frits's avatar

        Frits 10:59 am on 2 April 2024 Permalink | Reply

        @Jim, 5th column equation is same as 5th row equation.

        Like

        • Jim Randell's avatar

          Jim Randell 11:13 am on 2 April 2024 Permalink | Reply

          Thanks. Fixed now.

          I did make a version of the code that derives the expressions from the rows of the square [@replit] (which eliminates mistakes like this), but I posted the literal version as it is more readable.

          Like

    • GeoffR's avatar

      GeoffR 2:22 pm on 2 April 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..25:A;   var 1..25:B;   var 1..25:C;   var 1..25:D;
      var 1..25:E;   var 1..25:F;   var 1..25:G;   var 1..25:H;
      var 1..25:I;   var 1..25:J;   var 1..25:K;   var 1..25:L;
      var 1..25:M;   var 1..25:N;   var 1..25:O;   var 1..25:P;
      var 1..25:Q;   var 1..25:R;   var 1..25:S;   var 1..25:T;
      var 1..25:U;   var 1..25:V;   var 1..25:W;   var 1..25:X;
      var 1..25:Y;
      
      constraint all_different([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y]);
      
      % ROWS
      constraint A+B+C+D+E == 65 /\ F+G+H+I+J == 65 /\ K+L+M+N+O == 65
      /\ P+Q+R+S+T == 65 /\ U+V+W+X+Y == 65;  % Jim's analysis
      
      % COLUMNS
      constraint  A+F+K+P+U == 65 /\ B+G+L+Q+V == 65 /\ C+H+M+R+W == 65
      /\ D+I+N+S+X == 65 /\ E+J+O+T+Y == 65;
      
      % DIAGONALS
      constraint A+G+M+S+Y == 65 /\ U+Q+M+I+E == 65;
      
      %  S, U, N, D, A, Y, T, I, M are in increasing sequence
       constraint S+1 == U /\ U+1 == N /\ N+1 == D /\ D+1 == A /\ A+1 == Y
      /\ Y+1 == T /\ T+1 ==I /\ I + 1 == M;
      
      constraint M < E; %... with E coming later
      
      solve satisfy;
      
      output[" [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y] = " ++
      "\n" ++ show(  [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y] ) ++
      "\n" ++ "Grid : " ++ "\n" ++ show ([A,B,C,D,E] )
      ++"\n" ++ show([F,G,H,I,J]) ++ "\n" ++ show( [K,L,M,N,O] )
      ++"\n" ++ show( [P,Q,R,S,T]) ++ "\n" ++ show ( [U,V,W,X,Y]) ];
      
      % [A,  B, C,  D,  E,  F,  G,  H, I,  J, K, L,  M,  N, O,  P,  Q, R,  S, T,  U, V,  W, X,  Y] =
      % [11, 2, 19, 10, 23, 24, 20, 6, 14, 1, 4, 21, 15, 9, 16, 18, 5, 22, 7, 13, 8, 17, 3, 25, 12]
      % Grid :
      % [11, 2, 19, 10, 23]
      % [24, 20, 6, 14, 1]
      % [4, 21, 15, 9, 16]
      % [18, 5, 22, 7, 13]
      % [8, 17, 3, 25, 12]
      %----------
      %==========
      % By interpolation 11, 9, 7, 3 , 23, 22 = A, N, S, W, E, R
      

      Like

    • Frits's avatar

      Frits 6:09 pm on 3 April 2024 Permalink | Reply

      # make sure loop variable value is not equal to previous ones
      def domain(v, r=range(1, 26)):
        # find already used loop values  ...
        vals = set()
        # ... by accessing previously set loop variable names
        i = 0  # initially <i> (otherwise compiler error)
        for i, s in enumerate(lvd[v], 1):
          val = globals()[s]
          # general domain check
          if not (0 < val < 26): return []
          vals.add(val)
        
        # don't except duplicates in previous variables
        if len(vals) != i:
          return []
       
        return [x for x in r if x not in vals]
      
      # set up dictionary of for-loop variables
      lv = list("NXSUDAYTIMGEQVWBCLOKJFHPR")
      lvd = {v: lv[:i] for i, v in enumerate(lv)}
      
      # D + I + N + S = (N + 1) + (N + 5) + N + (N - 2) = 4N + 4 --> X = 61 - 4N
      for N in domain('N', range(9, 16)):
        X = 61 - 4 * N
        S, U, N, D, A, Y, T, I, M = [N + x for x in range(-2, 7)]
        G = 65 - (A + M + S + Y)
        # E + I + M + Q + U = 65, E = 65 - Q - (I + M + U) and E > M
        # assume E = M + x, x > 0 --> x = 65 - Q - (I + 2M + U) > 0 
        if (I + 2 * M + U) > 63: continue
        # check rest of numbers
        for E in domain('E', range(M + 1, 26)):
          Q = 65 - (E + I + M + U)
          for V in domain('V'):
            W = 65 - (U + V + X + Y)
            for B in domain('B'):
              C = 65 - (A + B + D + E) 
              L = 65 - (B + G + Q + V)
              for O in domain('O'):
                K = 65 - (L + M + N + O)
                J = 65 - (E + O + T + Y)
                for F in domain('F'):
                  H = 65 - (F + G + I + J)
                  P = 65 - (A + F + K + U)
                  for R in domain('R'):
                    if R != 65 - (P + Q + S + T): continue
                    
                    # check remaining equations to be sure we give a correct answer
                    if P + Q + R + S + T != 65: continue
                    if C + H + M + R + W != 65: continue
      
                    vs = [11, 9, 7, 3, 23, 22]
                    nums = [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y]
                    chars = "ABCDEFGHIJKLMNOPQRSTUVWXY"
      
                    print(f"answer: {[chars[nums.index(v)] for v in vs]}")     
      

      Like

      • Frits's avatar

        Frits 8:39 pm on 3 April 2024 Permalink | Reply

        line 30 also implies that N is less than 12.

        Like

  • Unknown's avatar

    Jim Randell 10:35 am on 28 March 2024 Permalink | Reply
    Tags:   

    Teaser 2636: Simple Easter Teaser 

    From The Sunday Times, 31st March 2013 [link] [link]

    I have written down three numbers, at least one of which is odd. Furthermore, one of the three is the sum of the other two. Then I have consistently replaced digits by letters, using different letters for different digits, and the numbers have become:

    SIMPLE
    EASTER
    TEASER

    What number is EASTER?

    [Incidentally, Easter Sunday 2013 has a palindromic date, 31/3/13. Interestingly, this happens next in 2031].

    This puzzle completes the archive of puzzles originally published in 2013.

    [teaser2636]

     
    • Jim Randell's avatar

      Jim Randell 10:36 am on 28 March 2024 Permalink | Reply

      At least one of the numbers is odd, so at least one of E or R must be odd.

      We can check that one of the numbers is the sum of the other two by adding all of them together, and then this total must be twice one of the original numbers:

      SIMPLE + EASTER + TEASER ∈ { 2×SIMPLE, 2×EASTER, 2×TEASER }

      So the LHS must be an even number, and if both E and R were odd, the LHS would be an odd number, so only one of them can be odd. And if E is odd then the LHS is also odd, hence E must be even and R must be odd.

      We can solve the puzzle using this expression directly with the [[ SubstitutedExpression ]] solver from the enigma.py library.

      The following run file executes in 383ms. (Internal runtime of the generated program is 201ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "SIMPLE + EASTER + TEASER in { 2 * SIMPLE, 2 * EASTER, 2 * TEASER }"
      
      # E is even, R is odd
      "E % 2 = 0"
      "R % 2 = 1"
      
      --answer="EASTER"
      

      Solution: EASTER = 278521.

      And the sum is:

      EASTER + TEASER = SIMPLE → 278521 + 527821 = 806342

      There is a further solution to this alphametic sum where all the terms are even:

      EASTER + TEASER = SIMPLE → 417342 + 341742 = 759084


      For a faster program we can consider the three possible sums (which can be solved using the [[ SubstitutedExpression.split_sum() ]] solver.

      The following Python program runs in 58ms. (Internal runtime is 8.6ms).

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, delete, sprintf, join, printf)
      
      # words involved in the alphametic
      words = ['SIMPLE', 'EASTER', 'TEASER']
      
      # try each word as the result
      for (i, result) in enumerate(words):
        expr = sprintf("{terms} = {result}", terms=join(delete(words, [i]), sep=" + "))
        # construct the alphametic puzzle
        p = SubstitutedExpression.split_sum(
          expr,
          extra=["(E % 2 != 0) or (R % 2 != 0)"],  # at least one is odd
        ).solver()
        # solve the puzzle
        for s in p.solve(verbose=0):
          # output solution
          printf("{expr} / {s}", s=p.substitute(s, expr))
      

      Like

    • GeoffR's avatar

      GeoffR 1:47 pm on 28 March 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:S; var 0..9:I; var 0..9:M; var 0..9:P; var 0..9:L;
      var 1..9:E; var 0..9:A; var 1..9:T; var 1..9:R;
      
      constraint all_different ([S, I, M, P, L, E, A, T, R]);
      
      % At least one of three numbers is odd.
      constraint sum([E mod 2 == 1, R mod 2 == 1]) > 0;
      
      var 100000..999999:SIMPLE == 100000*S + 10000*I + 1000*M + 100*P + 10*L + E;
      var 100000..999999:EASTER == 100000*E + 10000*A + 1000*S + 100*T + 10*E + R;
      var 100000..999999:TEASER == 100000*T + 10000*E + 1000*A + 100*S + 10*E + R;
      
      %  One of the three numbers is the sum of the other two
      constraint SIMPLE == EASTER + TEASER
      \/ EASTER == SIMPLE + TEASER
      \/ TEASER == SIMPLE + EASTER;
      
      solve satisfy;
      
      output[ "EASTER = " ++ show(EASTER) 
      ++ "\n" ++ "SIMPLE = " ++ show(SIMPLE) 
      ++ "\n" ++ "TEASER = " ++ show(TEASER) ];
      
      % EASTER = 278521
      % SIMPLE = 806342
      % TEASER = 527821
      % ----------
      % ==========
      % So SIMPLE = EASTER + TEASER
      

      Like

    • Frits's avatar

      Frits 9:13 pm on 28 March 2024 Permalink | Reply

        
      from itertools import permutations
      
      # EASTER
      # TEASER     only valid sum ot the three sums (otherwise E = 0)
      # ------ +
      # SIMPLE
      
      # A can't be zero as it would also lead to M = 0
      for R, E, S, T in permutations(range(1, 10), 4):
        # basic checks (R must be odd)
        if not ((2 * R) % 10 == E and R % 2 and E + T in {S - 1, S}): continue
      
        if (L := (2 * E + (R > 5)) % 10) in {R, E, S, T}: continue
        if (P := (S + T + (E > 4)) % 10) in {R, E, S, T, L}: continue
        
        for A in set(range(1, 10)).difference({R, E, S, T, L, P}):
          EASTER = int(''.join(str(x) for x in [E, A, S, T, E, R]))
          TEASER = int(''.join(str(x) for x in [T, E, A, S, E, R]))
          
          if (SIMPLE := EASTER + TEASER) // 100000 != S: continue
          IM = (SIMPLE // 1000) % 100
          if any(int(x) in {R, E, S, T, L, P, A} for x in str(IM)): continue
          print("answer:", EASTER)   
      

      Like

  • Unknown's avatar

    Jim Randell 10:09 am on 26 March 2024 Permalink | Reply
    Tags:   

    Teaser 2674: Roll model 

    From The Sunday Times, 22nd December 2013 [link] [link]

    My new board game has squares numbered from 1 to 100 and has two unusual dice. The first die is 10-sided with numbers from 1 to 10, and the second is 4-sided with a prime number on each side. A move consists of throwing the two dice and then choosing either one of the numbers or their sum and moving that number of squares in either direction. I found myself on one square and realised that there were just two squares which it would be impossible for me to reach with my next move. Both of those squares had prime numbers that did not appear on the dice.

    (a) Which particular square was I on?
    (b) What are the four numbers on the second die?

    [teaser2674]

     
    • Jim Randell's avatar

      Jim Randell 10:10 am on 26 March 2024 Permalink | Reply

      There are (10 + 4 + 10×4) = 54 possible scores, and we can move in either direction.

      So, when we start on a square the reachable squares spread out from it in both directions. If we have 56 consecutive numbers, and we can make 54 of them using the dice, this would be the maximum possible. And so the largest possible square we can be on is 57 and the smallest possible is 44. And we can’t afford more than 6 non-usable scores.

      We cannot afford more than 1 gap in the scores from 1 to 43 (as any gaps will be mirrored on the other side), or more than 2 gaps overall (if they are close to the end, the gaps could be on the same side – although this seems improbable if they are both primes).

      This Python program considers possible sets of primes on the second die, and let looks for an appropriate starting square. (We could just consider all possible combinations of primes, but it is a bit faster check we don’t introduce too many gaps or overlaps as we build up the sets).

      It runs in 58ms. (Internal runtime is 1.3ms).

      Run: [ @replit ]

      from enigma import (irange, primes, printf)
      
      # squares on the board
      squares = set(irange(1, 100))
      
      # die 1
      d1 = list(irange(1, 10))
      
      # fill out die 2
      def die2(ns, d1, ds, d2=list()):
        k = len(d2)
        if k == 4:
          yield (d2, ds)
        else:
          # choose the next number on die 2
          for (i, n) in enumerate(ns):
            # update the set of deltas
            ds_ = ds.union([n], (n + x for x in d1))
            m = 11 * k + 10
            # check for overlaps and gaps
            if len(ds_) < m + 5: continue
            if len(set(irange(1, m)).difference(ds_)) > (1 if k < 3 else 2): continue
            # fill out the remaining faces
            yield from die2(ns[i:], d1, ds_, d2 + [n])
      
      # die 2 has 4 [different] primes
      ps = list(primes.between(5, 50))
      
      # consider possible die 2 and deltas
      for (d2, ds) in die2(ps, d1, set(d1)):
        n = len(ds)
        # consider starting on square i
        for i in irange(98 - n, 5 + n):
          # find unreachable squares
          missing = squares.difference((i - x for x in ds), (i + x for x in ds), [i])
          # there are exactly 2 primes missing, that are not on d2
          if len(missing) != 2: continue
          if any(x in d2 or x not in primes for x in missing): continue
          # output solution
          printf("die2={d2}; square={i}; missing={missing}", missing=sorted(missing))
      

      Solution: (a) You were on square 51; (b) The numbers on the second die are: 11, 23, 31, 41.

      And the squares that cannot be reached are 29 and 73.


      Manually we can adopt a “greedy” strategy to maximise the reach of the dice while minimising the gaps:

      Die 10 will cover deltas 1..10 on either side, and if 11 is not on die 2, then both deltas of 11 and 12 are unreachable, which would make at least 4 unreachable squares (2 on each site). So 11 must be on die 2 and we can cover deltas 1..21.

      The next missing delta is now 22 (which is not prime), so we either leave 22 as unreachable, or we put a prime less than 22 on die 2.

      If we leave 22 as unreachable, and put 23 on die 2 then we can cover deltas 1..33, our unreachable values are at ±22, and we can’t have any more gaps.

      The next missing delta is 34, which is not a prime, so the largest prime we can add to die 2 is 31, which means we can cover deltas 1..41 (except 22).

      The next missing delta is 42, so the largest prime we can add is 41, and so with die 2 = (11, 23, 31, 41) we can cover deltas 1..51 (except 22).

      Starting on squares 49..52 we have the following unreachable squares:

      49 → 27, 71 [not both prime]
      50 → 28, 72 [not both prime]
      51 → 29, 73 [both prime, not on die 2 ⇒ solution]
      52 → 30, 74 [not both prime]

      So we have found a single candidate solution, but we can continue and confirm this is the only candidate:

      If we choose to cover 22, by placing the largest possible prime (i.e. 19) on die 2, then we can cover deltas 1..29.

      The next missing delta is now 30, we could choose to leave ±30 unreachable, and place 31 on die 2, so we can cover 1..41 (except 30).

      And then we place 41 on die 2, and we can cover 1..51 (except 30).

      This gives the following unreachable squares with die 2 = (11, 19, 31, 41):

      49 → 19, 79 [both prime, but 19 on die 2]
      50 → 20, 80 [not both prime]
      51 → 21, 81 [not both prime]
      52 → 22, 82 [not both prime]

      This gives no solutions.

      So we need to cover 30, so we place 29 on die 2, we can then cover deltas 1..39.

      We can leave 40 as unreachable and place 41 on die 2, and we cover 1..51 (except 40).

      This gives the following unreachable squares with die 2 = (11, 19, 29, 41):

      49 → 9, 89 [not both prime]
      50 → 10, 90 [not both prime]
      51 → 11, 91 [not both prime]
      52 → 12, 92 [not both prime]

      This gives no solutions.

      So we need to cover 40, so we place 37 on die 2, and we cover 1..47, and this is not enough to leave just 2 unreachable squares.

      And we have found no further candidate solutions.

      Like

      • Hugo's avatar

        Hugo 11:39 am on 26 March 2024 Permalink | Reply

        A ten-sided die is not very practical, and a tetrahedron doesn’t roll.
        I suggest using an icosahedron and an octahedron, with each number occurring twice
        (for example, on opposite faces).

        Like

        • Jim Randell's avatar

          Jim Randell 12:23 pm on 26 March 2024 Permalink | Reply

          @Hugh: I think I have seen 10 sided dice based on a pentagonal trapezohedron [@wikipedia]. You could also roll a long dodecahedral prism. A square prism might not be so good though.

          Like

          • Lise Andreasen's avatar

            Lise Andreasen 4:34 pm on 4 April 2024 Permalink | Reply

            There are standard 4 and 10 sided easily available, among other things for paper RPG.

            Like

    • Frits's avatar

      Frits 10:47 pm on 26 March 2024 Permalink | Reply

       
      # primes up to 100
      P = [3, 5, 7]
      P = [2] + P + [x for x in range(11, 100, 2) if all(x % p for p in P)]
      
      # this program tries to follow Jim's manual solution.
      
      # for delta's 1 - 43 we can afford to miss one delta
      
      # add a new prime allowing only 1 gap for the first 43 delta's
      def solve(d, ds=[], skip=0):
        if len(ds) == 4:
          # check number of possible reachable squares
          if 2 *(ds[-1] + 10 - (skip > 0)) > 96:
            yield (ds, skip)
        else:
          # try to make delta <d> or ...
          if d in P:
            yield from solve(d + 11, ds + [d], skip)    
          else:
            # largest prime we can add to die 2
            lp = [p for p in P if p < d and p != skip][-1]
            yield from solve(lp + 11, ds + [lp], skip)    
          # ... leave delta <d> as unreachable
          if not skip:
            yield from solve(d + 1, ds, d)  
      
      # deltas 1-10 will be covered by the first die
      for d2, skip in solve(11):
        # we can cover deltas 1..r except possible <skip>
        r = d2[-1] + 10
        
        # one delta must have been skipped, otherwise the number of possible
        # reachable squares is too low
        
        # consider starting on square <pos>
        for pos in range(100 - r, r + 2) :
          # non-reachables
          nr = [pos - skip, pos + skip] 
          if any(x for x in nr if x not in P or x in d2): continue
          print(f"answer: Peter was on square {pos}, "
                f"the four numbers on the second die are: {d2}")    
      

      Like

  • Unknown's avatar

    Jim Randell 8:54 am on 19 March 2024 Permalink | Reply
    Tags:   

    Teaser 2673: Footprints 

    From The Sunday Times, 15th December 2013 [link] [link]

    A cubical dice, with faces labelled as usual, is placed in one of the nine squares of a three-by-three grid, where it fits exactly. It is then rotated about one of its edges into an adjacent square and this is done a total of eight times so that the dice visits each square once. The “footprint” of the route is the total of the nine faces that are in contact with the grid.

    (a) What is the lowest footprint possible?
    (b) What is the highest footprint possible?
    (c) Which whole numbers between those two values cannot be footprints?

    [teaser2673]

     
    • Jim Randell's avatar

      Jim Randell 8:55 am on 19 March 2024 Permalink | Reply

      I used the [[ Cube() ]] class to deal with rotations of the standard die (see: Teaser 2835).

      This Python program considers possible representative starting squares on the grid (a corner, an edge, the centre square) using a standard right-handed die, and then positioning it in all possible orientations it looks for possible footprint values as it moves around the grid. (We get the same results with the remaining squares on the grid, and also if we were to use a left-handed die).

      It runs in 82ms. (Internal runtime is 17ms).

      Run: [ @replit ]

      from enigma import (irange, append, diff, printf)
      from cube import (Cube, U, D, L, R, F, B)
      
      # possible moves:
      #
      #  1 2 3
      #  4 5 6
      #  7 8 9
      #
      move = {
        1: { F: 2, L: 4 },
        2: { B: 1, F: 3, L: 5 },
        3: { B: 2, L: 6 },
        4: { R: 1, F: 5, L: 7 },
        5: { R: 2, B: 4, F: 6, L: 8 },
        6: { R: 3, B: 5, L: 9 },
        7: { R: 4, F: 8 },
        8: { R: 5, B: 7, F: 9 },
        9: { R: 6, B: 8 },
      }
      
      # calculate footprint totals
      # d = current die orientation
      # i = current die position
      # t = accumulated footprint total
      # vs = visited positions
      def footprints(d, i, t=0, vs=set()):
        # add in the value on the D face
        t += d.faces[D]
        vs = append(vs, i)
        # have we visited each square in the grid?
        if len(vs) == 9:
          yield t
        else:
          # make a move
          for (r, j) in move[i].items():
            if j not in vs:
              yield from footprints(d.rotate([r]), j, t, vs)
      
      # a standard die (U, D, L, R, F, B)
      die = (1, 6, 2, 5, 3, 4)
      
      # accumulate footprint totals
      ts = set()
      # consider possible starting squares: corner = 1, edge = 2, centre = 5
      for i in [1, 2, 5]:
        # consider possible initial orientations for the die
        for d in Cube(faces=die).rotations():
          # calculate footprint totals
          ts.update(footprints(d, i))
      
      # calculate min/max footprints
      (a, b) = (min(ts), max(ts))
      # output solution
      printf("min = {a}; max = {b}; missing = {ms}", ms=diff(irange(a, b), ts))
      

      Solution: (a) The minimum possible footprint is 21; (b) The maximum possible footprint is 42; (c) 29 and 34 cannot be footprints.

      The following paths starting in the centre square (5) and visiting (5, 6, 3, 2, 1, 4, 7, 8, 9) (i.e. spiralling out from the centre) give the minimum and maximum:

      (1); F → (2); R → (4); B → (1); B → (3); L → (2); L → (4); F → (1); F → (3) = footprint 21
      (6); F → (5); R → (4); B → (6); B → (3); L → (5); L → (4); F → (6); F → (3) = footprint 42


      With a bit of analysis we can get a faster program:

      There are only 3 essentially different paths (every other path is a reflection/rotation/reversal of one of these):

      If we start at the beginning of one of these paths with a die showing (x, y, z) around one corner, and opposite faces (X, Y, Z) (so x + X = y + Y = z + Z = 7), then we get the following footprints:

      X + z + x + y + z + Y + X + z + x = 21 + 3z
      X + z + x + y + X + z + x + y + z = 14 + 2y + 3z
      X + z + y + X + Z + y + z + X + Z = 14 + 2y + 3X

      (y, z) = (2, 1) gives a minimum of 21 in second equation, and (y, z) = (5, 6) gives a maximum of 42.

      We can examine the full range of footprints by considering the value of 1 face in the first equation, and 2 adjacent faces in the second (or third) equation.

      This Python program examines all possible footprints, and has an internal runtime of 63µs.

      Run: [ @replit ]

      from enigma import (irange, diff, printf)
      
      # calculate footprint totals
      scores = [1, 2, 3, 4, 5, 6]
      ts = set()
      for x in scores:
        ts.add(21 + 3*x)
        for y in scores:
          if x == y or x + y == 7: continue
          ts.add(14 + 2*x + 3*y)
      
      # calculate min/max footprints
      (a, b) = (min(ts), max(ts))
      # output solution
      printf("min = {a}; max = {b}; missing = {ms}", ms=diff(irange(a, b), ts))
      

      Like

  • Unknown's avatar

    Jim Randell 11:52 am on 12 March 2024 Permalink | Reply
    Tags:   

    Teaser 2646: Monday birthdays 

    From The Sunday Times, 9th June 2013 [link] [link]

    In one auspicious month last year our family had two great celebrations: Harry, the oldest member of the family, had his 100th birthday and, in the same month, his great-grandson Peter was born.

    It turns out that they were both born on the same day of the week. Of course, Harry has celebrated several of his 100 birthdays on a Monday, as will Peter. However, even if Peter lives that long, the number of his first 100 birthdays that fall on a Monday will be two fewer than Harry’s.

    On which day of the week were they born?

    [teaser2646]

     
    • Jim Randell's avatar

      Jim Randell 11:52 am on 12 March 2024 Permalink | Reply

      This puzzle was set in 2013, so Harry had his 100th birthday in 2012, and so was born in 1912.

      Peter was born in the same month in 2012 as Harry’s 100th birthday, so Peter’s 100th birthday will be in 2112.

      And we are interested in counting birthdays that fall on a Monday.

      This Python program collects candidate birthdays >(month, day) for each of them and groups them by the number of Monday birthdays in the first 100. We then look for situations where Harry has 2 more Monday birthdays that Peter, and then try to find common (weekday, month) combinations.

      It runs in 63ms. (Internal runtime is 3.1ms).

      Run: [ @replit ]

      from datetime import (date, timedelta)
      from enigma import (defaultdict, group, intersect, printf)
      
      # collect birthdays between y1 and y2 that fall on a Monday
      def collect(y1, y2):
        # find the first Monday (weekday = 0) in the range
        inc = timedelta(days=1)
        d = date(y1, 1, 1)
        while d.weekday() != 0: d += inc
        # and then skip forward by weeks
        inc = timedelta(days=7)
        # count the number of Monday birthdays by date
        r = defaultdict(int)
        while True:
          r[(d.month, d.day)] += 1
          d += inc
          if d.year > y2: break
        # group dates by Monday count
        return group(r.keys(), by=r.get)
      
      # count birthdays on a Monday for Harry from 1913 - 2012
      gH = collect(1913, 2012)
      # count birthdays on a Monday to Peter from 2013 - 2112
      gP = collect(2013, 2112)
      
      # collect results
      rs = set()
      
      # consider possible counts for H
      for (kH, vH) in gH.items():
        # P has a count that is 2 less
        kP = kH - 2
        vP = gP.get(kP)
        if vP is None: continue
      
        # collect possible (weekday, month) combinations for Harry and Peter
        fn = lambda d: (d.weekday(), d.month)
        dH = group((date(1912, m, d) for (m, d) in vH), by=fn)
        dP = group((date(2012, m, d) for (m, d) in vP), by=fn)
      
        # look for (weekday, month) combinations common to both
        for (w, m) in intersect([dH.keys(), dP.keys()]):
          rs.add(w)
      
      # find possible days of the week
      days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      for w in rs:
        printf("weekday = {w}", w=days[w])
      

      Solution: Harry and Peter were both born on a Tuesday.

      And they could be both born on Tuesdays in any month from March to December.

      For example:

      Harry was born on Tuesday, 5th March 1912, and has had 15 birthdays on a Monday up to 2012 (in years: 1917, 1923, 1928, 1934, 1945, 1951, 1956, 1962, 1973, 1979, 1984, 1990, 2001, 2007, 2012).

      Peter was born on Tuesday, 6th March 2012, and will have 13 birthdays on a Monday up to 2112 (in years: 2017, 2023, 2028, 2034, 2045, 2051, 2056, 2062, 2073, 2079, 2084, 2090, 2102).

      The sequences differ because 2000 was a leap year, but 2100 will not be.

      Like

  • Unknown's avatar

    Jim Randell 8:57 am on 5 March 2024 Permalink | Reply
    Tags:   

    Teaser 2649: Right to left 

    From The Sunday Times, 30th June 2013 [link] [link]

    I have given each letter of the alphabet a different value from 0 to 25, so some letters represent a single digit and some represent two digits. Therefore, for example, a three-letter word could represent a number of three, four, five or six digits. In fact the word CORRECT represents a nine-figure number. It turns out that:

    TO
    REFLECT
    RIGHTTOLEFT

    are three even palindromes.

    What is the CORRECT number?

    [teaser2649]

     
    • Jim Randell's avatar

      Jim Randell 8:58 am on 5 March 2024 Permalink | Reply

      See also: Teaser 2859.

      We can use the [[ SubstitutedExpression ]] solver to solve this puzzle.

      Encoding the conditions directly gives us a run file that executes is 1.4s, but we can speed thing up a bit by checking the starts and ends of partial palindromes to make sure they match. By considering (R, T), (RE, CT) and (RI, FT) we bring the runtime down to 95ms (and the internal runtime of the generated program down to 18ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # use base 26 for values 0-25
      --base=26
      
      --invalid="0,CRT" # no leading zeros in words
      --invalid="0|10|20,OT" # no trailing zeros in palindromes
      --invalid="1|3|5|7|9|11|13|15|17|19|21|23|25,OT" # O and T must end in an even digit
      --invalid="1|3|5|7|9|11-19,RT" # R and T must start with an even digit
      
      # check digits form palindromic numbers
      --code="pal = lambda ds: is_palindrome(join(ds))"
      "pal([T, O])"
      "pal([R, E, F, L, E, C, T])"
      "pal([R, I, G, H, T, T, O, L, E, F, T])"
      
      # check digits have n characters
      --code="length = lambda ds: len(join(ds))"
      "length([C, O, R, R, E, C, T]) == 9"
      
      # [optional] speed things up by checking for partial palindromes
      --code="check = lambda xs, ys: zip_eq(join(xs), reverse(join(ys)))"
      "check([R], [T])"
      "check([R, E], [C, T])"
      "check([R, I], [F, T])"
      
      --answer="(C, O, R, R, E, C, T)"
      --template=""
      

      Solution: CORRECT = 124421124.

      We get the following assignments:

      C=1, E=21, F=12, G=11, I=22, O=2, R=4, T=24
      C=1, E=21, F=12, G=11, I=22, O=2, R=4, T=24
      C=1, E=21, F=12, G=11, I=22, O=2, R=4, T=24

      which gives CORRECT (= 1:2:4:4:21:1:24).

      And one of:

      H=20, L=0
      H=23, L=3
      H=25, L=5

      Which give the palindromes:

      TO = 24:2
      REFLECT = 4:21:12:x:21:1:24
      RIGHTTOLEFT = 4:22:11:2x:24:24:2:x:21:12:24
      where x = 0, 3, 5

      Liked by 1 person

    • Frits's avatar

      Frits 6:27 am on 6 March 2024 Permalink | Reply

      Reusing part of Jim’s code.

       
      n2d     = lambda n: [n] if n < 10 else n2d(n // 10) + [n % 10]
      jn      = lambda s, fn = lambda x: x: [fn(y) for x in s for y in n2d(x)]
      is_pal  = lambda *s: (j := jn(s)) == j[::-1]
      check   = lambda xs, ys: all(x == y for x, y in zip(jn(xs), jn(ys)[::-1]))
      
      palins = ["TO", "REFLECT", "RIGHTTOLEFT"]
      letters = {y for x in palins for y in x}
      B = 26
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(B):
        vs = set()
        if d == 0: vs.update('CORT')     # no leading zeros in words
        if (d % 10) in {10, 20}: 
          vs.update('OT')                # no trailing zeros in palindromes
        if d % 2: vs.update('OT')        # O and T must end in an even digit
        if (int(str(d)[::-1])) % 2: 
          vs.update('RT')                # R and T must start with an even digit
      
        d2i[d] = vs 
      
      # valid digits
      d2v = {lt: {k for k, vs in d2i.items() if lt not in vs} for lt in letters}
      
      if 1:
        print("domain:")
        for k, vs in d2v.items():
          print(f"{k}: {','.join(str(v) for v in vs)}")
      
      # return a valid loop structure string, indent after every "for" statement
      def indent(s):
        res, ind = "", 0
        for ln in s:
          res += " " * ind + ln + "\n"
          if len(ln) > 2 and ln[:3] == "for":
            ind += 2
        return res   
      
      # generate expression for unused letters in palindrome <p>     
      def add_palin_expressions(p, extras=[], exprs=[], done=set()):
        s, i, part = p, 0, dict()
        
        # loop over all letters in <s>
        while s:
          if (lt := s[0]) not in done:
            exp = f"for {lt} in {d2v[lt]}.difference([{','.join(done)}]):"
            exprs.append(exp.replace(", ", ","))
          
          done.add(lt)
          # fill left/right parts  
          part[i % 2] = part.get(i % 2, []) + [lt]
          i += 1
          if i > 1: # we have more than one letter
            if len(s) > 1:  # we'll use is_pal for the last one
              exprs.append(f"if not check([{', '.join(part[0])}], [{', '.join(part[1][::-1])}]): continue")
            # add extra condition if all variables are present  
            for extra in extras : 
              if len(done | extra[0]) == len(done):
                exprs.append(f"if not ({extra[1]}): continue")
                extras.remove(extra) 
          # remove precessed letter and start from the other side        
          s = s[1:][::-1]
      
        exprs.append(f"# final palindrome check for {p}")  
        exprs.append(f"if not is_pal({', '.join(p)}): continue")  
        
        return extras, exprs, done
      
      # extra conditions besides palindromes  
      conds = [(set("CORRECT"), "len(jn([C, O, R, R, E, C, T])) == 9")]
      es, dn = [], set()
      # main loop
      for pal in palins:
        es.append(f"# -----  process palindrome {pal}  -----")
        conds, es, dn = add_palin_expressions(pal, conds, es, dn)
        es.append(" ")
      
      # add final print statement for the answer
      es.append("print('answer: CORRECT =', ''.join(jn([C, O, R, R, E, C, T], fn=str)), end =', ')")
      ltrs = "{" + '=}, {'.join(letters) + "=}"
      es.append(f"print(f'{ltrs}')")
      
      exprs = indent(es)
      
      print(exprs)    
      exec(exprs)   
      

      Like

  • Unknown's avatar

    Jim Randell 9:06 am on 29 February 2024 Permalink | Reply
    Tags:   

    Teaser 2602: Over fifty years 

    From The Sunday Times, 5th August 2012 [link] [link]

    We have now had over fifty years of Teasers and to celebrate this I found three special numbers using only non-zero digits and I wrote the numbers down in increasing order. Then I consistently replaced all the digits by letters, with different letters for different digits.

    In this way the numbers became:

    FIFTY
    YEARS
    TEASER

    In fact the third number was the sum of the other two.

    Which number does TEASER represent?

    The first numbered Teaser puzzle was published 63 years ago on 26th February 1961 (although there was a gap of almost a year in 1978/9). However the earliest Teaser puzzle I have found was published in The Sunday Times on 2nd January 1949.

    [teaser2602]

     
    • Jim Randell's avatar

      Jim Randell 9:07 am on 29 February 2024 Permalink | Reply

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It runs in 64ms. (Internal runtime of the generated program is 445µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      --invalid="0,AEFIRSTY"
      
      "FIFTY + YEARS = TEASER"
      
      --extra="F < Y"
      

      Solution: TEASER = 158453.

      The complete sum is:

      62619 + 95834 = 158453

      Liked by 1 person

      • GeoffR's avatar

        GeoffR 3:12 pm on 29 February 2024 Permalink | Reply

        Trying a posting using WP Reader…

        from itertools import permutations
        
        for p1 in permutations ('123456789', 8):
            F, I, T, Y, E, A, R, S = p1
            if int(F) < int(Y):
                FIFTY = int(F + I + F + T + Y)
                YEARS = int(Y + E + A + R + S)
                TEASER = int(T + E + A + S + E + R)
                if FIFTY + YEARS == TEASER:
                    print(f"Sum is {FIFTY} + {YEARS} = {TEASER}")
        
        # Sum is 62619 + 95834 = 158453
        

        Like

c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel
Design a site like this with WordPress.com
Get started