Updates from June, 2025 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:35 am on 3 June 2025 Permalink | Reply
    Tags: ,   

    Teaser 2541: Lotto luck 

    From The Sunday Times, 5th June 2011 [link]

    Chris was lucky in the lotto, winning a whole number of pounds (under £ 5,000), which he shared with his five children. John got £ 1 more than a fifth of the total; Ciaran got £ 1 more than a fifth of the remainder. After Ciaran got his share, Fergal got £ 1 more than a fifth of the remainder, and after Fergal got his share, Brendan got £ 1 more than a fifth of what was left. After Brendan got his share, Chris gave Grainne £ 1 more than a fifth of what was left and kept the remainder (a whole number of pounds).

    How much did Chris keep?

    [teaser2541]

     
    • Jim Randell's avatar

      Jim Randell 8:36 am on 3 June 2025 Permalink | Reply

      For a starting amount X, the next child’s share is X/5 + 1, and the remainder is 4X/5 − 1.

      Each of the shares and remainders must be an integer, because if we ever get a share that has a fractional part, then the fractional part of the remainder will be further subdivided into a smaller fraction. And as both the starting amount and the final remainder are whole numbers, then all the intermediate numbers are too.

      The following Python program runs in 71ms. (Internal runtime is 3.7ms).

      from enigma import (irange, ediv, printf)
      
      # calculate share and remainder from total X
      def calc(X):
        share = ediv(X, 5) + 1
        return (share, X - share)
      
      # consider possible winnings
      for W in irange(1, 4999):
      
        # calculate share and remainder for each child
        try:
          (J, R) = calc(W)  # John
          (C, R) = calc(R)  # Ciaran
          (F, R) = calc(R)  # Fergal
          (B, R) = calc(R)  # Brendan
          (G, R) = calc(R)  # Grainne
        except ValueError:
          continue
      
        # output solution
        printf("W={W} -> J={J} C={C} F={F} B={B} G={G} -> R={R}")
      

      Solution: Chris kept £ 1019.

      The initial winnings were £ 3120, and they are distributed as:

      John = £ 625
      Ciaran = £ 500
      Fergal = £ 400
      Brendan = £ 320
      Grainne = £ 256
      Chris = £ 1019
      total = £ 3120

      It is easy to see that the initial amount won must be a multiple of 5, which allows us to skip 80% of the cases checked. (And brings the internal runtime down to 1.0ms).


      And with further analysis we can get an even faster solution:

      We find the remainder after step k is:

      R[k] = (4^k W − 5(5^k − 4^k)) / (5^k)

      So after the shares for the 5 sons have been distributed we have:

      R[5] = (1024 W − 10505) / 3125

      1024 W − 3125 R = 10505

      where: 0 < R < W < 5000.

      This is a Linear Diophantine Equation in 2 variables, and can be solved using the Extended Euclidean Algorithm [@wikipedia], implemented as [[ egcd() ]] in the enigma.py library.

      Here is a general solver for this kind of equation:

      from enigma import (egcd, div, divc)
      
      # solve linear diophantine equations in 2 variables:
      def diop_linear(a, b, c, mX=0, fn=0):
        """
        solve the linear Diophantine equation a.X + b.Y = c for integers X, Y
        (where a, b are non-zero, and gcd(a, b) divides c).
      
        return ((X0, Y0), (Xd, Yd)) to give solutions of the form:
      
          (X, Y) = (X0 + t.Xd, Y0 + t.Yd) for integer t
      
        the value of X0 returned is the smallest integer possible above (or equal
        to) mX, and Xd is positive.
      
        however, if <fn> is set, then a function f: t -> (X, Y) is returned instead.
        """
        if a == 0 or b == 0: raise ValueError("diop_linear: invalid equation")
        (X, Y, g) = egcd(a, b)
        if g > 1:
          (a, b, c) = (a // g, b // g, div(c, g))
          if c is None: raise ValueError("diop_linear: no solutions")
        # calculate particular solution (X0, Y0) and deltas (Xd, Yd)
        (X0, Y0) = (c * X, c * Y)
        (Xd, Yd) = ((-b, a) if b < 0 else (b, -a))
        # adjust X0 to be the smallest possible value
        t = divc(mX - X0, Xd)
        X0 += t * Xd
        Y0 += t * Yd
        if fn: return (lambda t: (X0 + t * Xd, Y0 + t * Yd))
        return ((X0, Y0), (Xd, Yd))
      

      Which we can then use to solve the puzzle:

      from enigma import (irange, inf, printf)
      
      # k = number of sons
      k = 5
      # solve the Diophantine equation: 4^k W - 5^k R = 5(5^k - 4^k)
      (p4k, p5k) = (4**k, 5**k)
      fn = diop_linear(p4k, -p5k, 5 * (p5k - p4k), fn=1)
      
      # consider increasing solutions where 0 < W < 5000
      for t in irange(0, inf):
        (W, R) = fn(t)
        if not (W < 5000): break
        if R < 0: continue
        # output solution
        printf("W={W} R={R}")
      

      This program has an internal runtime of just 35µs.

      Like

      • Hugo's avatar

        Hugo 10:54 am on 9 January 2026 Permalink | Reply

        A slightly different way of looking at it:

        He borrows £5 from his wife, so the amount to be distributed is £3125 (= 5^5).
        The eldest child gets a fifth of that, and each successive child four fifths as much as the previous child, being a fifth of what is left. He ends up with £1024 (= 4^5), from which he can repay his wife.

        There are further solutions with an integer times as much, always including the borrowed £5.

        If there were six children, the smallest solution would be £15625 (= 5^6),
        if only four then £625 (= 5^4), in each case including the borrowed £5.

        It’s not hard to come up with variants in which each child gets a quarter, or a sixth, or some other fraction of what is left.

        Like

    • John Crabtree's avatar

      John Crabtree 8:03 pm on 4 June 2025 Permalink | Reply

      This teaser is a variation of the Monkey and the Coconuts puzzle which Martin Gardner wrote about decades ago in his column in the Scientific American. The puzzle is older than that.
      R[1] = 4W/5-1 = 4(W+5)/5 – 5
      And so R[k] = 4^k (W + 5) / (5^k) – 5
      R[5] = 1024(W+5) / 3125 – 5, and so (W+5) = 3125, and the answer follows.

      Like

  • Unknown's avatar

    Jim Randell 6:21 am on 1 June 2025 Permalink | Reply
    Tags:   

    Teaser 3271: Uncut diamonds 

    From The Sunday Times, 1st June 2025 [link] [link]

    Andrew was building a rhombus-shaped patio. He could have paved it with equilateral triangular slabs with 1ft edges, but he wanted a layout with a hexagonal slab in place of six triangular ones at various places. He considered two layouts that were symmetrical in two perpendicular directions:

    Layout 1: every triangular slab touches the perimeter of the patio in at least one point.

    Layout 2: each group of three adjacent hexagonal slabs encloses exactly one triangular one.

    The triangular and hexagonal slabs come in boxes of 12, and Andrew chose layout 1 because he would only need a third as many boxes of triangular slabs as layout 2.

    What length were the sides of the patio and how many slabs in total would be left over?

    [teaser3271]

     
    • Jim Randell's avatar

      Jim Randell 7:13 am on 1 June 2025 Permalink | Reply

      Once you have determined how the two layouts work (isometric graph paper helps), this is a relatively straightforward puzzle.

      Here are the two layouts on a rhombus with sides of size 8:

      (Layout 1 uses 17 hexes and 26 tris. Layout 2 uses 16 hexes and 32 tris. Note that there are tris in Layout 2 that are not surrounded by 3 hexes).

      The following Python program generates increasing sizes for each layout, and determines the (side, hexes, tris) for each case, and then looks for two layouts with the same size where layout 1 requires 1/3 the number of boxes of triangular tiles as layout 2.

      It runs in 60ms. (Internal runtime is 266µs).

      from enigma import (irange, inf, sq, intersect, divc, printf)
      
      # generate (<side>, <hexes>, <tris>) for layout 1
      def layout1(M):
        # consider number of hexagons on the long diagonal
        for n in irange(1, inf):
          s = n + 1
          if s > M: break
          h = 2 * sum(irange(n, 1, step=-3)) - n
          t = 2 * sq(s) - 6 * h
          yield (s, h, t)
      
      # generate (<side>, <hexes>, <tris>) for layout 2
      def layout2(M):
        # there is a sq(n) array of hexagons
        for n in irange(1, inf):
          s = 2 * n
          if s > M: break
          h = sq(n)
          t = 2 * sq(s) - 6 * h
          yield (s, h, t)
      
      # collect (h, t) by s for each layout
      M = 100
      (d1, d2) = (dict(), dict())
      for (s, h, t) in layout1(M): d1[s] = (h, t)
      for (s, h, t) in layout2(M): d2[s] = (h, t)
      
      # calculate boxes and left overs
      def boxes(n, k=12):
        b = divc(n, k)
        return (b, k * b - n)
      
      # examine common side lengths
      for s in sorted(intersect([d1.keys(), d2.keys()])):
        ((h1, t1), (h2, t2)) = (d1[s], d2[s])
        # check for layout 1 needing 1/3 as many boxes of tris as layout 2
        ((b1, r1), (b2, r2)) = (boxes(t1), boxes(t2))
        if 3 * b1 == b2:
          # output solution
          printf("s={s}: layout 1 = {h1} hex + {t1} tri; layout 2 = {h2} hex + {t2} tri")
          printf("-> tri: layout 1 = {b1} boxes (rem = {r1}); layout 2 = {b2} boxes (rem = {r2})")
          ((b1, r1), (b2, r2)) = (boxes(h1), boxes(h2))
          printf("-> hex: layout 1 = {b1} boxes (rem = {r1}); layout 2 = {b2} boxes (rem = {r2})")
          printf()
      

      Solution: The sides of the patio were 24 ft. There are 9 slabs left over.

      Layout 1 uses 177 hex slabs (= 15 boxes, with 3 unused), and 90 tri slabs (= 8 boxes, with 6 unused).

      Layout 2 uses 144 hex slabs (= 12 boxes, with 0 unused), and 288 tri slabs (= 24 boxes, with 0 unused).

      Like

      • Jim Randell's avatar

        Jim Randell 9:45 am on 1 June 2025 Permalink | Reply

        Some analysis gives a shorter program:

        For a rhombus with side s, the number of hexagons in each of the layouts is:

        layout 1: h1 = ceil(sq(s − 1)/3), where s ≥ 2
        layout 2: h2 = sq(s/2), where s is even

        The following program considers increasing even length sides of the rhombus, until a solution is found.

        from enigma import (irange, inf, sq, divc, ediv, printf)
        
        # calculate boxes and left overs
        def boxes(n, k=12):
          b = divc(n, k)
          return (b, k * b - n)
        
        # consider possible sides of the rhombus
        for s in irange(2, inf, step=2):
          n = 2 * sq(s)  # number of triangular cells
          # calculate the number of hex tiles
          (h1, h2) = (divc(sq(s - 1), 3), sq(ediv(s, 2)))
          # calculate the number of tri tiles
          (t1, t2) = (n - 6 * h1, n - 6 * h2)
          # calculate number of boxes of tri tiles
          ((b1, r1), (b2, r2)) = (boxes(t1), boxes(t2))
          if 3 * b1 == b2:
            # output solution
            printf("s={s}: layout 1 = {h1} hex + {t1} tri; layout 2 = {h2} hex + {t2} tri")
            printf("-> tri: layout 1 = {b1} boxes (rem = {r1}); layout 2 = {b2} boxes (rem = {r2})")
            ((b1, r1), (b2, r2)) = (boxes(h1), boxes(h2))
            printf("-> hex: layout 1 = {b1} boxes (rem = {r1}); layout 2 = {b2} boxes (rem = {r2})")
            printf()
            break
        

        Like

  • Unknown's avatar

    Jim Randell 7:48 am on 29 May 2025 Permalink | Reply
    Tags:   

    Teaser 2537: Odds and evens 

    From The Sunday Times, 8th May 2011 [link] [link]

    I have taken two three-digit numbers and multiplied them together by long multiplication. [Below] are my workings, but with O marking the positions of the odd digits and E the even digits:

    What are the two three-digit numbers?

    [teaser2537]

     
    • Jim Randell's avatar

      Jim Randell 7:49 am on 29 May 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      #      E E E          a b c
      #  *   O O O      *   d e f
      #  ---------      ----------
      #  E E E          g h i
      #    E O E    ->    j k m
      #    O O O E        n p q r
      #  =========      =========
      #  O E O O E      s t u v w
      
      SubstitutedExpression
      
      --distinct=""
      
      "{abc} * {def} = {stuvw}"
      
      "{abc} * {f} = {npqr}"
      "{abc} * {e} = {jkm}"
      "{abc} * {d} = {ghi}"
      
      # odds and evens
      --invalid="0,adefgjknpqsuv"
      --invalid="2|4|6|8,defknpqsuv"
      --invalid="1|3|5|7|9,abcghijmrtw"
      
      # reconstruct the multiplication sum (disable with: -O -A -S)
      --answer="({abc}, {def})"
      --output="lambda p, s, ans: output_mul(*ans, pre='  ', start='', end='')"
      

      Solution: The three-digit numbers are: 226 and 135.

      The completed multiplication sum looks like this:

      Like

    • ELBAZ HAVIV's avatar

      ELBAZ HAVIV 6:06 pm on 9 June 2025 Permalink | Reply

      Hello Jim

      Please help me to understand
      the removing of the 3 E’s

      from the original puzzle

      or why
      the original puzzle add those 3 E’s
      if they are not needed.

      Thank you.

      Like

      • Jim Randell's avatar

        Jim Randell 8:34 pm on 9 June 2025 Permalink | Reply

        The removed Es are the 0 padding in the intermediate multiplications.

        So instead of doing:

        226 × 100 = 22600
        226 × 30 = 6780
        226 × 5 = 1130

        We just do:

        226 × 1 = 226
        226 × 3 = 678
        226 × 5 = 1130

        And shift the results across by the appropriate number of places.

        Like

    • ELBAZ HAVIV's avatar

      ELBAZ HAVIV 9:04 pm on 9 June 2025 Permalink | Reply

      Hello again

      That’s a bit strange.
      but it’s ok

      because regularly long multiplication
      is as you show in the We just do:

      Thank you.

      Like

  • Unknown's avatar

    Jim Randell 9:39 am on 27 May 2025 Permalink | Reply
    Tags:   

    Teaser 2520: [Phone number] 

    From The Sunday Times, 9th January 2011 [link] [link]

    I have a brand-new 11-digit phone number. The first and last digits are zero and the middle section uses each of the digits 1 to 9 once. If you called the number by pressing the buttons on the standard keypad of a modern phone, you would find no two consecutive digits of the number in the same row or column. Looking at the fifth and sixth digits, I see one is double the other. The second and third digits differ by two, the seventh is lower than the sixth and the 10th is odd, and one more than the eighth.

    What is my number?

    This puzzle was originally published with no title.

    [teaser2520]

     
    • Jim Randell's avatar

      Jim Randell 9:40 am on 27 May 2025 Permalink | Reply

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

      The following run file executes in 77ms. (Internal runtime of the generated program is 170µs).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # the number is: 0ABCDEFGHI0
      --digits="1-9"  # for A-I
      
      # a keypad is laid out as:
      #
      #   1 2 3
      #   4 5 6
      #   7 8 9
      #     0
      #
      # map digits to (<row>, <col>):
      #              0  1  2  3  4  5  6  7  8  9
      --code="row = [4, 1, 1, 1, 2, 2, 2, 3, 3, 3]"
      --code="col = [2, 1, 2, 3, 1, 2, 3, 1, 2, 3]"
      
      # no digits share row or col with an adjacent digit
      --code="check = lambda x, y: col[x] != col[y] and row[x] != row[y]"
      "check(0, A)"
      "check(A, B)"
      "check(B, C)"
      "check(C, D)"
      "check(D, E)"
      "check(E, F)"
      "check(F, G)"
      "check(G, H)"
      "check(H, I)"
      "check(I, 0)"
      
      # one of 5th or 6th digits is double the other
      "(D == 2 * E) or (E == 2 * D)"
      
      # 2nd and 3rd digits differ by 2
      "abs(A - B) = 2"
      
      # 7th digit is lower than 6th
      "F < E"
      
      # 10th digit is odd, and one more than 8th
      "I % 2 = 1"
      "G + 1 = I"
      
      --template="0 A B C D E F G H I 0"
      --solution=""
      

      Solution: The number is: 03594816270.

      Like

    • Ruud's avatar

      Ruud 3:02 pm on 27 May 2025 Permalink | Reply

      Very brute force, but still vey fast:

      row = {0: 0, 1: 1, 2: 1, 3: 1, 4: 4, 5: 4, 6: 4, 7: 7, 8: 7, 9: 7}
      column = {0: 2, 1: 1, 2: 2, 3: 3, 4: 1, 5: 2, 6: 3, 7: 1, 8: 2, 9: 3}
      
      for n in itertools.permutations(range(1, 10)):
          if (
              ((number:=(None,0)+ n + (0,))[5] == 2 * number[6] or number[6] == 2 * number[5])
              and abs(number[2] - number[3]) == 2
              and number[7] < number[6]
              and number[10] % 2 == 1
              and number[10] == number[8] + 1
              and all(row[number[i]] != row[number[i + 1]] and column[number[i]] != column[number[i + 1]] for i in range(1, 11))
          ):
              print("".join(str(number[i]) for i in range(1, 12)))
      

      Like

    • Frits's avatar

      Frits 6:29 pm on 27 May 2025 Permalink | Reply

      #! python3 -m enigma -rr
       
      SubstitutedExpression
      
      # the number is: 0ABCDEFGHI0
      --digits="1-9"  # for A-I
       
      # a keypad is laid out as:
      #
      #   1 2 3      
      #   4 5 6      
      #   7 8 9      
      #     0       
      #
      # map digits to (<row>, <col>):
      #              0  1  2  3  4  5  6  7  8  9
      --code="row = [4, 1, 1, 1, 2, 2, 2, 3, 3, 3]"
      --code="col = [2, 1, 2, 3, 1, 2, 3, 1, 2, 3]"
      
      --invalid="1|3|5|7|9,G"     # G is even
      --invalid="1|3|5|6|7|9,DE"  # {D, E} is either {2, 4} or {4, 8}
      --invalid="8|9,F"           # F < E and E is 2, 4 or 8
      --invalid="4,ABCFGH"        # either D = 4 or E = 4
      --invalid="2|4|6|8,AB"      # D, E and G are even, so A and B can't be even
      --invalid="1|9,AB"          # A and B not both on same row
      --invalid="5,CDEFGH"        # {3, 5, 7} remains for A and B
      --invalid="2|5|8,A"         # not adjacent to "0" button
      
      --assign="B,5"              # either A = 5 or B = 5 but also A != 5
      --invalid="2|4|6|8,AC"      # not on same row/col as B(5)
       
      # no digits share row or col with an adjacent digit
      --code="check = lambda x, y: col[x] != col[y] and row[x] != row[y]"
      "check(A, B)"
      "check(B, C)"
      "check(C, D)"
      "check(D, E)"
      "check(E, F)"
      "check(F, G)"
      "check(G, H)"
      "check(H, I)"
      
      # parity of F and H is unknown
      "45 - (A + B + C + D + E + F + G + I) = H"
       
      # one of 5th or 6th digits is double the other
      "(D == 2 * E) or (E == 2 * D)"
       
      # 2nd and 3rd digits differ by 2
      "abs(A - B) = 2"
       
      # 7th digit is lower than 6th
      "F < E"
       
      # 10th digit is odd, and one more than 8th
      "G + 1 = I"
       
      --template="0 A B C D E F G H I 0"
      --distinct="ABCDEFGI"
      --solution=""
      

      or

      N = 3
      r = lambda i: (i - 1) // N
      c = lambda i: (i - 1) % N if i else 1
      
      forbidden = {i: set(j for j in range(10) if j != i and
                      (r(i) == r(j) or c(i) == c(j))) for i in range(10)}
      
      def solve(k=9, ns=set(range(1, 10)), ss=[0]):
        if k == 0:
          # check 10th number
          if 0 not in forbidden[ss[-1]] and ss[-1] == ss[-3] + 1:
            yield ss + [0]
        else:
          match(10 - k):
           case(3): 
             if abs(ss[-1] - ss[-2]) != 2: return
           case(6): 
             if not (ss[-1] == 2 * ss[-2] or ss[-2] == 2 * ss[-1]): return
           case(7):   
             if ss[-1] > ss[-2]: return
           case(8):    
             if ss[-1] % 2: return
      
          for n in ns:
            if n not in forbidden[ss[-1]]:
              yield from solve(k - 1, ns - {n}, ss + [n])
      
      for s in solve():
        print("answer:", ''.join(str(x) for x in s))
      

      Like

  • Unknown's avatar

    Jim Randell 7:12 am on 25 May 2025 Permalink | Reply
    Tags:   

    Teaser 3270: Tunnel vision 

    From The Sunday Times, 25th May 2025 [link] [link]

    Four ramblers came to a tunnel that they all had to travel through. The tunnel was too dangerous to travel through without a torch, and unfortunately they only had one torch. It was also too narrow to walk through more than two at a time. The maximum walking speed of each of the walkers was such that they could walk through the tunnel in an exact number of minutes, less than ten, which was different for each walker. When two walkers walked together, they would walk at the speed of the slower one.

    They all managed to get through the tunnel and in the quickest possible time, this time being five sixths of the total of their individual crossing times.

    In ascending order, what are their four four individual crossing times?

    [teaser3270]

     
    • Jim Randell's avatar

      Jim Randell 7:34 am on 25 May 2025 Permalink | Reply

      See also: Enigma 981, [@wikipedia].

      This Python program runs in 63ms. (Internal runtime is 1.8ms).

      from enigma import (Accumulator, irange, subsets, div, cache, printf)
      
      # find minimum total crossing times for individual times <ts> (all different)
      # where <xs> is the times of the participants on the far side
      @cache
      def _time(ts, xs):
        # 1 or 2 people take the max of the times
        if len(ts) < 3: return max(ts)
        # otherwise, find minimal time for 2 to cross (x, y) and 1 to return (z)
        r = Accumulator(fn=min)
        for xy in subsets(ts, size=2):
          for z in xs.union(xy):
            # minimise the remaining scenario
            t = _time(ts.difference(xy).union({z}), xs.union(xy).difference({z}))
            r.accumulate(max(xy) + z + t)
        return r.value
      time = lambda ts, xs=(): _time(frozenset(ts), frozenset(xs))
      
      # choose 4 individual crossing times; all different and <10 minutes
      for ts in subsets(irange(1, 9), size=4):
        # target time is 5/6 the sum of the individual times
        t = div(5 * sum(ts), 6)
        if t is None or time(ts) != t: continue
        # output solution
        printf("{ts} -> {t}")
      

      Solution: The individual crossing times are: 1, 2, 7, 8 minutes.

      The total of the individual crossing times is 18 minutes.

      And the minimum crossing time is 15 minutes (15 = 18 × 5/6), for example:

      t=0: 1+2 cross (2 min) {1, 2}
      t=2: 1 returns (1 min) {2}
      t=3: 7+8 cross (8 min) {2, 7, 8}
      t=11: 2 returns (2 min) {7, 8}
      t=13: 1+2 cross (2 min) {1, 2, 7, 8}
      t=15: crossing complete

      Like

      • Jim Randell's avatar

        Jim Randell 1:48 pm on 27 May 2025 Permalink | Reply

        I experimented further with this puzzle.

        Here is a modified version of my program which will produce a minimal sequence of crossings, and is also modified to use [[ multiset ]] instead of [[ set ]], which allows individual crossing times to be repeated:

        from enigma import (Accumulator, multiset, irange, subsets, div, printf)
        
        # find minimum total crossing times for individual times <ts>
        # where <xs> is the times of the participants on the far side
        def _time(ts, xs, ss):
          # 1 or 2 people take the max of the times
          if ts.size() < 3: return (ts.max(), ss + [list(ts.sorted())])
          # otherwise, find minimal time for 2 to cross (x, y) and 1 to return (z)
          r = Accumulator(fn=min)
          for xy in ts.subsets(size=2):
            for z in xs.combine(xy):
              # minimise the remaining scenario
              (t, ss_) = _time(ts.difference(xy).add(z), xs.combine(xy).remove(z), ss + [list(xy.sorted()), [z]])
              r.accumulate_data(xy.max() + z + t, ss_)
          return (r.value, r.data)
        time = lambda ts, xs=(): _time(multiset(ts), multiset(xs), list())
        
        # choose 4 individual crossing times
        for ts in subsets(irange(1, 9), size=4, select='C'):
          # target time is 5/6 the sum of the individual times
          t = div(5 * sum(ts), 6)
          if t is None: continue
          (t_, ss) = time(ts)
          if t_ != t: continue
          # output solution
          printf("{ts} -> {t} {ss}")
        

        We find there are 2 further solutions if repeated individual crossing times are allowed.

        And the program can also be used to find solutions for other variations of the puzzle, for example, where there are 5 participants, all with different crossing times less than 12.

        [Solutions below.]


        If repeated crossing times are allowed in the original puzzle (use [[ select='R' in line 19), the following scenarios are also solutions to the puzzle:

        (1, 1, 4, 6) → 10 [[1, 1], [1], [4, 6], [1], [1, 1]]
        (2, 2, 7, 7) → 15 [[2, 2], [2], [7, 7], [2], [2, 2]]

        And if we extend the puzzle to 5 participants (with different individual crossing times less than 12 minutes):

        (1, 2, 6, 10, 11) → 25 [[1, 6], [1], [1, 2], [1], [10, 11], [2], [1, 2]]

        Like

    • Frits's avatar

      Frits 2:49 pm on 26 May 2025 Permalink | Reply

      Only using analysis for the multiple of 6.

      from itertools import combinations
      
      # decompose the integer <t> into <k> increasing
      # numbers between mn and mx (inclusive)
      def decompose(t, k=4, mn=1, mx=9, vs=[]):
        if k == 1:
          if vs[-1] < t <= mx:
            yield set(vs + [t])
        else:
          for n in range(mn, mx + 1):
            if 2 * k * n + k * (k - 1) > 2 * t:
              break
            yield from decompose(t - n, k - 1, n + 1, mx, vs + [n])
      
      # get all four ramblers through the tunnel from a to b
      def solve(a, b=set(), e=0, ss=[], t=0): 
        global quickest
        if len(a) == 2 and not e: # penultimate stage
          quickest = min(quickest, t + max(a)) 
        else:
          if e: # we are at the end of the tunnel
            for c in combinations(b, 1):
              if (t_ := t + c[0]) < quickest:
                solve(a | set(c), b.difference(c), 0, ss + [c], t_)
          else: # we are at the start of the tunnel   
            for c in combinations(a, 2):
              if (t_ := t + max(c)) < quickest:
                solve(a.difference(c), b  | set(c), 1, ss + [c], t_)
      
      # total of their individual crossing times <ti> must be a multiple of 6
      # to get to five sixths
      for ti in range(12, 31, 6):
        # choose times for the four ramblers (with sum <ti>)
        for t4 in decompose(ti):
          quickest = 999
          # get the quickest possible time for these four times
          solve(t4)
          # five sixths?
          if 6 * quickest == 5 * ti:
            print(f"answer: {sorted(t4)}")
      

      or faster

      from itertools import combinations, permutations
      from collections import defaultdict
      
      # assume a and e are walking back times (may have same value) 
      # b, c, d and f must have different values
      
      # 1: (a, b)  2: a  3: (c, d)  4: e  5: (e, f)
      dct = defaultdict(lambda: 45) # default value 45
      dgts = set(range(1, 10))
      for c, d in combinations(range(1, 10), 2):
        for b, f in permutations(dgts - {c, d}, 2):
          # total of their individual crossing times must be 
          # a multiple of 6 to get to five sixths
          if (b + c + d + f) % 6 == 0: 
            # go for quickest possible time
            a, e = min(c, f), min(b, c) 
            # store the quickest possible time
            t = max(a, b) + a + d + e + max(e, f)
            t4 = tuple(sorted([b, c, d, f]))
            dct[t4] = min(dct[t4], t)
      
      for k, v in dct.items():
        # five sixths of the total of their individual crossing times
        if 5 * sum(k) == 6 * v:
          print(f"answer: {k}") 
      

      Like

  • Unknown's avatar

    Jim Randell 8:04 am on 22 May 2025 Permalink | Reply
    Tags:   

    Teaser 2546: Secret agents 

    From The Sunday Times, 10th July 2011 [link] [link]

    Secret agents James and George exchange coded messages. The code is given by an addition sum, with different letters replacing different digits. The message is then written below the sum. James needs to send George the time (24-hour clock) at which their next secret operation is to begin. He texts as follows:

    THREE + THREE + TWO = EIGHT
    time: DATA

    When does this secret operation begin?

    [teaser2546]

     
    • Jim Randell's avatar

      Jim Randell 8:05 am on 22 May 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "THREE + THREE + TWO = EIGHT"
      --invalid="0,ET"
      
      # presumably DATA is a time expressed using the 24 hour clock (00:00 - 23:59)
      --extra="DA < 24"
      --extra="TA < 60"
      
      # format answer as a time using a 24 hour clock
      --code="time = lambda h, m: sprintf('{h:02d}:{m:02d}')"
      --answer="time(DA, TA)"
      

      Solution: The operation begins at 15:35.

      Like

    • Ruud's avatar

      Ruud 5:04 pm on 24 May 2025 Permalink | Reply

      import istr
      
      print(*(d|a|':'|t|a for t,h,r,e,w,o,i,g,a,d in istr.permutations(istr.digits()) if d|a<24 and t|a<60 and 2*(t|h|r|e|e)+(t|w|o)==(e|i|g|h|t)))
      

      Like

  • Unknown's avatar

    Jim Randell 9:06 am on 20 May 2025 Permalink | Reply
    Tags:   

    Teaser 2522: [Letter values] 

    From The Sunday Times, 23rd January 2011 [link] [link]

    I have numbered the letters of the alphabet from 1 to 26, but not in alphabetical order, giving each letter its own unique value. So, for any word, I can work out its value by adding up the values of its letters. In this way, I have found some words with equal values. For example, the following nine words all have the same value:

    I
    SAY
    ALL
    THESE
    HERE
    HAVE
    EQUAL
    VALUES
    TOO

    What are the values of Y, O, U, R, S?

    This puzzle was originally published with no title.

    [teaser2522]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 20 May 2025 Permalink | Reply

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

      It runs in 356ms. (Internal runtime of the generated code is 206ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # use values 1-26
      --base=27
      --digits="1-26"
      
      # all the words give sums equal to I
      "S + A + Y = I"
      "A + L + L = I"
      "T + H + E + S + E = I"
      "H + E + R + E = I"
      "H + A + V + E = I"
      "E + Q + U + A + L = I"
      "V + A + L + U + E + S = I"
      "T + O + O = I"
      
      --template=""
      --answer="(Y, O, U, R, S)"
      

      Solution: Y = 20; O = 10; U = 3; R = 8; S = 2.

      The complete list of values determined are:

      A = 4
      E = 1
      H = 16
      I = 26
      L = 11
      O = 10
      Q = 7
      R = 8
      S = 2
      T = 6
      U = 3
      V = 5
      Y = 20

      Like

  • Unknown's avatar

    Jim Randell 6:50 am on 18 May 2025 Permalink | Reply
    Tags:   

    Teaser 3269: Combination lock 

    From The Sunday Times, 18th May 2025 [link] [link]

    Phil buys a four-digit combination lock where each dial can be set from 0 to 9, and needs to decide what number to set to unlock it. He opts for a number with all different digits and a leading zero so he only has to remember a three-digit number. When the chosen number is set for the lock to open, nine other four-digit numbers are visible. For instance, if he chooses 0123, then 1234, 2345, 3456, 4567, 5678, 6789, 7890, 8901 and 9012 are all visible. As a retired Maths teacher with a natural interest in numbers he examines the other nine numbers that are displayed when the lock is set to open. None of these numbers are prime but two of them are perfect squares.

    What three-digit number does Phil need to remember?

    [teaser3269]

     
    • Jim Randell's avatar

      Jim Randell 7:06 am on 18 May 2025 Permalink | Reply

      A brute force approach gives a short and acceptably fast program.

      The following Python program runs in 77ms. (Internal runtime is 8.4ms).

      from enigma import (irange, nconcat, subsets, is_prime, is_square_p, printf)
      
      # shift digits <ds> by increment <i> (modulo <n>)
      shift = lambda ds, i, n=10: ((d + i) % n for d in ds)
      
      # consider possible 3 digit numbers
      for (X, Y, Z) in subsets(irange(1, 9), size=3, select='P'):
        # calculate each of the "other" numbers
        ns = list(nconcat(shift((0, X, Y, Z), i)) for i in irange(1, 9))
        # (exactly) 2 are squares
        if sum(is_square_p(n) for n in ns) != 2: continue
        # none are prime
        if any(is_prime(n) for n in ns): continue
        # output solution
        printf("0{X}{Y}{Z} -> {ns}")
      

      Solution: The number Phil has to remember is 912.

      So the combination is: 0912.

      Of the other numbers visible (1023, 2134, 3245, 4356, 5467, 6578, 7689, 8790, 9801):

      4356 = 66^2
      9801 = 99^2

      There is one other combination that gives 2 squares: 0327.

      Of the other numbers visible (1438, 2549, 3650, 4761, 5872, 6983, 7094, 8105, 9216):

      2549 is prime
      4761 = 69^2
      6983 is prime
      9216 = 96^2

      So this fails the requirement that none of the other numbers is prime.

      And these are the only 2 combinations that give more than one square among the “other” numbers.

      Like

      • Jim Randell's avatar

        Jim Randell 10:39 am on 18 May 2025 Permalink | Reply

        Here is a slightly longer, but faster, alternative approach.

        It considers the possible 4-digit squares, and then looks for 2 that give the same combination number (0xyz).

        It has an internal runtime of 253µs.

        from enigma import (defaultdict, irange, nsplit, nconcat, is_prime, join, printf)
        
        # shift digits <ds> by increment <i> (modulo <n>)
        shift = lambda ds, i, n=10: ((d + i) % n for d in ds)
        
        # count 4-digit squares by the combination (smallest number on the lock)
        m = defaultdict(int)
        for i in irange(32, 99):
          ds = nsplit(i * i)
          if len(set(ds)) != len(ds): continue
          # reduce the first digit to zero
          k = tuple(shift(ds, -ds[0]))
          m[k] += 1
        
        # consider combinations that give exactly 2 squares
        for (k, n) in m.items():
          if n != 2: continue
        
          # construct the 9 "other" numbers
          ns = list(nconcat(shift(k, i)) for i in irange(1, 9))
          # none of them are prime
          if any(is_prime(n) for n in ns): continue
        
          # output solution
          printf("{k} -> {ns}", k=join(k))
        

        Like

        • Frits's avatar

          Frits 11:04 am on 18 May 2025 Permalink | Reply

          Nice, I was also considering possible 4-digit squares but was using the less efficient “itertools.combinations”.

          Like

  • Unknown's avatar

    Jim Randell 8:24 am on 15 May 2025 Permalink | Reply
    Tags:   

    Teaser 2552: Ever-increasing circles 

    From The Sunday Times, 21st August 2011 [link] [link]

    I took a piece of A4 paper and cut it straight across to make a square and a rectangle. I then cut the square along a diagonal and, from the rectangle, cut as large a circle as I could. My friend Ron said that if I needed two triangles of those sizes and as large a circle in one piece as possible from an A4 sheet, then he could do much better. With a fresh sheet of A4, he produced the two triangles and the biggest circle possible.

    What is the area of his circle divided by the area of mine?

    [teaser2552]

     
    • Jim Randell's avatar

      Jim Randell 8:24 am on 15 May 2025 Permalink | Reply

      A-series paper has the aspect ratio 1 : √2.

      Cutting a square from a sheet will leave a 1 × (√2 − 1) rectangle, out of which we can cut out a circle with radius (√2 − 1)/2.

      By rotating one of the triangles so that the √2 side lies along one of the long edges of the sheet, we open up a kite shaped area, in which we can inscribe a circle.

      The radius of a semi-circle inscribed in a right-angled triangle with sides (a, b, h) is given by:

      1/r = 1/a + 1/b; or:
      r = a.b / (a + b)

      (See proof below).

      And in this case we have:

      a = 1
      b = √2 − 1

      r = (√2 − 1) / √2
      r = (√2 − 1)(√2/2)

      And so we see the radius of Ron’s circle is √2 times the radius of the setters original circle.

      And so the area of Ron’s circle is twice the area of the setters circle.

      Solution: The answer is 2.


      Proof:

      Consider the right-angled triangle ABC, where the length of the sides BC = a, AC = b.

      The area of the triangle is: a.b/2.

      But we can also write the area as the sum of the areas of triangles AXC and BXC, both of which have a height of r, the radius of the semicircle.

      So:

      a.r/2 + b.r/2 = a.b/2
      r(a + b) = a.b
      r = a.b / (a + b)

      Like

  • Unknown's avatar

    Jim Randell 7:32 am on 13 May 2025 Permalink | Reply
    Tags: ,   

    Teaser 2350: [Circles and tangents] 

    From The Sunday Times, 7th October 2007 [link]

    A puzzle in a magazine was flawed: it had two different answers. The puzzle showed two circles that did not meet. It gave the radii (prime numbers of centimetres) and the distance between the centres (a whole number of centimetres less than 50).

    It then said:

    “Imagine a straight line that is a tangent to both the circles. The distance between the two points where that line touches the circles is. a whole number of centimetres.

    How many?”

    What was the radius of the smaller circle?

    This puzzle was originally published with no title.

    [teaser2350]

     
    • Jim Randell's avatar

      Jim Randell 7:32 am on 13 May 2025 Permalink | Reply

      We are to construct the tangents to two non-touching circles. See: [ @wikipedia ]

      I am assuming that, as we are asked to find the radius of the smaller circle, then the two circles must be of different sizes. (And without this condition the Teaser puzzle itself is flawed).

      If the circles have radii r and R (where r < R), and the separation between their centres is d, then the tangents have length:

      outer tangent, length T: d^2 = T^2 + (R − r)^2
      inner tangent, length t: d^2 = t^2 + (R + r)^2

      This Python program runs in 59ms. (Internal runtime is 2.3ms).

      from enigma import (primes, irange, ircs, printf)
      
      # r = radius of smaller circle (a prime)
      for r in primes.between(2, 24):
        # R = radius of the larger circle (a prime)
        for R in primes.between(r + 1, 48 - r):
          # d = separation of centres (< 50)
          for d in irange(r + R + 1, 49):
            # calculate: T = length of outer tangent, t = length of inner tangent
            (T, t) = (ircs(d, -(R - r)), ircs(d, -(R + r)))
            if T is None or t is None or t == T: continue
            # output viable configurations
            printf("r={r} R={R} d={d} -> T={T} t={t}")
      

      Solution: The smaller circle has a radius of 7 cm.

      There are two possible scenarios that provide tangents with two different integer lengths:

      d=26, r=7, R=17 → T=24, t=10
      d=34, r=7, R=23 → T=30, t=16

      And so one of these must be the scenario presented in the magazine puzzle.

      The first of these solutions looks like this:


      If the circles were allowed to be the same size, then we can find the following additional viable configurations, that give two different integer tangent lengths:

      d=5, r=2, R=2 → T=5, t=3
      d=10, r=3, R=3 → T=10, t=8
      d=26, r=5, R=5 → T=26, t=24

      And the Teaser puzzle would not have a unique solution.

      These additional solutions can be seen by changing line 6 in the program to start the search for R from the value of r.

      Like

      • Frits's avatar

        Frits 7:13 pm on 14 May 2025 Permalink | Reply

        In both scenarios we have (not a general rule):

        d^2 = T^2 + t^2 = 2.(r^2 + R^2)

        Like

    • Frits's avatar

      Frits 11:02 am on 16 May 2025 Permalink | Reply

      from enigma import primes, pythagorean_triples, subsets
      from collections import defaultdict
      
      prms = set(primes.between(2, 48))
      
      # collect Pythagorean triples for each distance between the centres
      pt = defaultdict(list)
      for x, y, h in pythagorean_triples(49):
        pt[h] += [x]
        pt[h] += [y]
      
      for d, vs in pt.items():  
        for t, T in subsets(sorted(vs), 2):
          # make sure r and R are integers
          if t % 2 != T % 2: continue
          r, R = (T - t) // 2, (T + t) // 2
          if any(x not in prms for x in (r, R)): continue 
          print(f"{r=} {R=} {d=} -> {T=} {t=}")
      

      Like

  • Unknown's avatar

    Jim Randell 6:36 am on 11 May 2025 Permalink | Reply
    Tags:   

    Teaser 3268: W-hoops! 

    From The Sunday Times, 11th May 2025 [link] [link]

    The PE game “W-hoops!” involves throwing inflexible hoops to hook onto a W-shaped frame. The torus-shaped hoops all have the same cross-section but different diameters and fit snugly, in contact, into a storage box. The illustration (not to scale) shows side- and top-views with three hoops, but the box actually contains the maximum possible number of hoops, allowing for a hole at the centre. The internal width of the box is a multiple of its internal depth.

    Di preferred maths to PE and after her throws she calculated, using 22/7 for 𝛑, that the total volume of the hoops was over 60% of the internal volume of their box. Knowing this would overstate the value, she then used 3.14 for 𝛑, and calculated a value under 60%.

    How many hoops does the box hold?

    [teaser3268]

     
    • Jim Randell's avatar

      Jim Randell 7:39 am on 11 May 2025 Permalink | Reply

      See [ @wikipedia ].

      In the side view we see that it must be possible to exactly fit a whole number of discs in a line (as the width of the box is an exact multiple of the depth).

      If there is an odd number of discs in the line, then the smallest hoop must have a 1 disc wide hole (so it has a major radius of 2, where the discs are unit radius). And if there is an even number of discs, then the smallest hoop must have a 2 disc wide hole (and have a major radius of 3). The smallest hoop cannot be any larger, as it would then be possible to fit a smaller hoop inside. So these are the only two starting hoops we need to consider (R=2, R=3).

      Python floats are sufficient to find the answer to the puzzle, but for exact computations we can use [[ import rdiv as div ]] instead.

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

      from enigma import (irange, inf, fdiv as div, sq, printf)
      
      # approximations for pi; (a, b) represents a/b
      pis = [(22, 7), (314, 100)]
      
      # solve starting with a smallest hoop with major radius = R (minor radius = 1)
      def solve(R):
        H = 0
        for n in irange(1, inf):
          H += 2 * R  # vol = pi^2 2R r^2
          w = 2 * (R + 1)  # w = box width
          V = 2 * sq(w)  # V = vol of box (depth = 2)
      
          # calculate percentage volumes using pi = 22/7 and pi = 3.14
          (p1, p2) = (div(100 * H * sq(a), V * sq(b)) for (a, b) in pis)
          # are we done?
          if not (p2 < 60): break
          if p1 > 60:
            # output solution
            printf("{n} hoops [maxR={R} w={w}; V={V} H={H} -> p1={p1:.2f}% p2={p2:.2f}%]", p1=float(p1), p2=float(p2))
          # the next hoop is larger
          R += 2
      
      # start with smallest hoops R=2 and R=3
      solve(2)
      solve(3)
      

      Solution: The box holds 5 hoops.

      I calculated the fraction of the box occupied by the hoops as:

      f = 2 × (3 + 5 + 7 + 9 + 11) × 𝛑^2 / (24 × 24 × 2)
      f = (35/1152) 𝛑^2 ≈ 59.97%

      The two approximations for 𝛑 give:

      𝛑 = 22/7 → f ≈ 60.02%
      𝛑 = 3.14 → f ≈ 59.91%


      Using analysis:

      For n hoops, the major radii R of the hoops follow one of the following sequences:

      R[] = 2, 4, 6, 8, 10, …, 2n → sum(R) = n(n + 1); box width = 4n + 2
      R[] = 3, 5, 7, 9, 11, …, 2n + 1 → sum(R) = n(n + 2); box width = 4n + 4

      In the first case the volumes of the box and the hoops are:

      V = 2(4n + 2)^2
      H = 𝛑^2 . 2n(n + 1)
      H/V = (1/4) 𝛑^2 n(n + 1) / (2n + 1)^2

      And the ratio H/V is 60% at n ≈ 2.52.

      In the second case:

      V = 2(4n + 4)^2
      H = 𝛑^2 . 2n(n + 2)
      H/V = (1/16) 𝛑^2 n(n + 2) / (n + 1)^2

      And the ratio H/V is 60% at n ≈ 5.05.

      And so the second case with n = 5 gives ratios just above and just below 60% for the approximations to 𝛑 given in the puzzle.

      Like

  • Unknown's avatar

    Jim Randell 8:13 am on 9 May 2025 Permalink | Reply
    Tags:   

    Teaser 2553: The X-Factor 

    From The Sunday Times, 28th August 2011 [link] [link]

    George and Martha’s daughter entered a singing competition. The entrants were numbered from one upwards. The total number of entrants was a three-digit number and their daughter’s number was a two-digit number. The five digits used in those two numbers were all different and non-zero. Martha noted that the number of entrants was a single-digit multiple of their daughter’s number. George realised the same would be true if the two digits of their daughter’s number were reversed.

    How many entrants were there?

    [teaser2553]

     
    • Jim Randell's avatar

      Jim Randell 8:14 am on 9 May 2025 Permalink | Reply

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

      The following run file executes in 84ms. (Internal runtime of the generated code is 10.9ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # total number of entrants = ABC
      # daughters number = XY
      
      --digits="1-9"
      
      "ediv(ABC, XY) < 10"
      "ediv(ABC, YX) < 10"
      
      --answer="ABC"
      

      Solution: There were 168 entrants.

      The daughter’s number is either 24 or 42.

      168 / 24 = 7
      168 / 42 = 4

      Like

      • Frits's avatar

        Frits 3:31 pm on 9 May 2025 Permalink | Reply

        I tested that “ediv” is a bit faster than “div” as for “div” an error/exception may occur when comparing “None” with an integer.

        Like

        • Jim Randell's avatar

          Jim Randell 4:19 pm on 9 May 2025 Permalink | Reply

          @Frits: [[ ediv() ]] throws an error if the division is not exact, whereas [[ div() ]] returns [[ None ]].

          In Python 3 you will get type error if you compare [[ None ]] to a number. But in Python 2 [[ None ]] compares as less than any number (including [[ -inf ]]). So using [[ ediv() ]] also allows the run file to work in older versions of Python.

          Like

          • Frits's avatar

            Frits 3:18 pm on 10 May 2025 Permalink | Reply

            or skipping a loop over “J”.

            #! python3 -m enigma -rr
             
            SubstitutedExpression
             
            # total number of entrants = ABC
            # daughters number = XY
             
            --distinct="ABCXY"
            --digits="1-9"
            --invalid="1,I"
            
             
            # find the single digit multiples I, J
            "XY * I = ABC"
            "ABC % YX == 0 and ABC // YX < 10"
             
            --answer="ABC"
            

            Like

      • Jim Randell's avatar

        Jim Randell 8:33 am on 10 May 2025 Permalink | Reply

        Or without using [[ *div() ]] function calls.

        This run file has an internal runtime of 188µs.

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # total number of entrants = ABC
        # daughters number = XY
        
        --distinct="ABCXY,IJ"
        --digits="1-9"
        
        # find the single digit multiples I, J
        "XY * I = ABC"
        "YX * J = ABC"
        
        --answer="ABC"
        

        (Further speed improvements left as a simple exercise for the reader).

        Like

    • Ruud's avatar

      Ruud 3:01 pm on 9 May 2025 Permalink | Reply

      import peek
      import istr
      
      for daughter in istr(range(10, 100)):
          for n, m in istr.combinations(range(1, 10), r=2):
              if (
                  (entrants := daughter * n) == (daughter_reversed := daughter.reversed()) * m
                  and len(entrants) == 3
                  and ("0" not in entrants)
                  and (entrants | daughter).all_distinct()
                  and (entrants | daughter_reversed).all_distinct()
              ):
                  peek(entrants, daughter, daughter_reversed)
      

      Like

      • Frits's avatar

        Frits 4:09 pm on 9 May 2025 Permalink | Reply

        @Ruud,

        Line 11 doesn’t seem to be logically necessary.

        I read the readme of the peek module at your Salabim site. I wish I had known this before. I will have a look at it (I’ll send you an email concerning an installation issue).

        I made my own simple debugging tool, I don’t need to import anything as I use preprocessing.
        A basic version is described at:

        [ https://enigmaticcode.wordpress.com/notes/#comment-10234 ]

        Like

        • ruudvanderham's avatar

          ruudvanderham 1:16 pm on 10 May 2025 Permalink | Reply

          Yes, you are right. Line 11 could be omitted (although it is not wrong).
          Nice to be in contact (in Dutch!) with you on the subject of peek.

          Like

  • Unknown's avatar

    Jim Randell 9:42 am on 6 May 2025 Permalink | Reply
    Tags:   

    Teaser 2523: [Unusual Avenue] 

    From The Sunday Times, 30th January 2011 [link] [link]

    George and Martha moved to a two-figure house number in Unusual Avenue. The avenue runs from south to north and each house is directly opposite another. No 1 is the southernmost on the west side, then the houses run consecutively up the west side, then southwards back down the east side. So, No 1 is opposite the highest-numbered house.

    George and Martha’s number is a multiple of the number of the house opposite. The same is true of the five houses their daughters live in, but all their houses have three-figure numbers.

    How many houses are there in the avenue?

    This puzzle was originally published with no title.

    [teaser2523]

     
    • Jim Randell's avatar

      Jim Randell 9:42 am on 6 May 2025 Permalink | Reply

      If there are k houses on each side of the road, then opposite houses sum to (2k + 1).

      There are 2k houses on the road in total, and the number of George and Martha’s house n is a 2-digit number that is a multiple of the house opposite (which has number 2k + 1 − n).

      Hence:

      n > 2k + 1 − n

      k < (2n − 1) / 2

      And the maximum possible 2-digit value for n is 99, hence:

      k ≤ 98

      This gives us an upper bound for an exhaustive search.

      The following Python program runs in 56ms. (Internal runtime is 332µs).

      from enigma import (irange, group, ndigits, printf)
      
      # consider possible k values (number of houses on one side of the road)
      for k in irange(52, 98):
        t = 2 * k  # total number of houses
        # collect numbers that are a multiple of the house opposite
        ns = list(x for x in irange(k + 1, t) if x % (t + 1 - x) == 0)
        if len(ns) < 6: continue
        # group the numbers by digit count
        g = group(ns, by=ndigits)
        # ensure there is a 2-digit and 5 3-digits
        (n2s, n3s) = (g.get(2), g.get(3))
        if (not n2s) or (not n3s) or (len(n3s) < 5): continue
        # output solution
        printf("total houses = {t} [n2s={n2s} n3s={n3s}]")
      

      Solution: There are 134 houses in the road.

      And so the numbers of opposite houses sum to 135.

      George and Martha live at 90, which is opposite 45.

      And there are 6 houses that their 5 daughters could live at:

      108 (opposite 27)
      120 (opposite 15)
      126 (opposite 9)
      130 (opposite 5)
      132 (opposite 3)
      134 (opposite 1)


      If instead of an Avenue the road was a Close, with a house at the closed end (or several houses), then there would be further possible solutions.

      Like

    • Frits's avatar

      Frits 12:39 pm on 8 May 2025 Permalink | Reply

      Using ideas of Jim and Brian.

      # the number of houses is even, say 2.k, so we want numbers 
      # n such that:
      # 
      #    n = m.(2.k + 1 - n)  ==>  (m + 1).n = m.(2.k + 1)
      #
      # this makes both n and m even (credit: B. Gladman)
      
      d3 = 5  # five 3-digit numbers
      mn = 49 + d3            # 2.k >= 100 + (d3 - 1) * 2 
      mx = (3 * 99 - 2) // 4  # n >= 2 * (2k + 1) - n
      
      # are there at least <a> house numbers with correct opposite house number?
      def find(k, a, strt, m=-1):
        c, x, k2 = 0, strt, k + k
        while c <= a and x >= m:
          c += (x % (k2 + 1 - x) == 0)
          x -= 2
        return c >= a  
      
      # consider possible k values (number of houses on one side of the road)
      for k in range(mn, mx + 1):
        # opposite housenumbers t.m / (m + 1) and t / (m + 1) with t = 2.k + 1
        # t.m / (m + 1) <= 98 or m <= 98 / (t - 98)  
        m = 98 // ((k2 := k + k) - 97) 
        # check for solutions of even n less than 100
        if any((k2 + 1) % x == 0 for x in range(3, m + 2, 2)):
          # check for at least <d3 - 1> solutions for even numbers 
          # in range 100 ... 2.k - 2
          if find(k, d3 - 1, k2 - 2, 100):
            print("answer:", k2)
      

      Like

  • Unknown's avatar

    Jim Randell 7:14 am on 4 May 2025 Permalink | Reply
    Tags:   

    Teaser 3267: Leaf-eaters 

    From The Sunday Times, 4th May 2025 [link] [link]

    My encyclopaedia consists of many volumes, each containing the same number (over 40) of leaves. (Each leaf has a page number on each side. With n leaves Volume 1 would have pages 1 to 2n, Volume 2 would have pages 2n+1 to 4n, etc.) I keep the encyclopaedia on one long shelf so that on their spines I can read “Volume 1”, “Volume 2”, etc, from left to right.

    A voracious bookworm started at the left-hand end of the shelf and ate through some covers and leaves, finishing by eating through the leaf with a page number double the number of leaves it had eaten through. Meanwhile, another bookworm started at the right-hand end of the shelf and ate through twice as many leaves as the first bookworm. Then in two of the volumes, the percentage of the nibbled leaves was equal to the volume number.

    How many volumes are there in the set? And how many leaves per volume?

    [teaser3267]

     
    • Jim Randell's avatar

      Jim Randell 8:57 am on 4 May 2025 Permalink | Reply

      See also: Enigma 660, Tantalizer 386. (In particular the note about page numbering).

      If the worms pass each other, then 100% of the pages in each volume will have been nibbled, and the only possibility in this case is that vol 100 is 100% nibbled (providing there are at least 100 volumes).

      However if they both reach partially into the same volume (from opposite sides), then we could have a situation where this volume has a smaller percentage nibbled, while all the remaining volumes are 100% nibbled, so if we have at least 100 volumes, this could lead to a viable scenario.


      For volumes with n leaves per volume. If worm 1 nibbles v whole volumes, and x (= 1 .. n) additional leaves in the final volume it visits, then the total number of leaves nibbled is:

      t1 = nv + x

      and the even page number of the final leaf nibbled is:

      p1 = 2n(v + 1) − 2(x − 1)

      and this page number is twice the number of leaves nibbled, hence:

      2n(v + 1) − 2(x − 1) = 2(nv + x)

      x = (n + 1) / 2

      Hence n must be an odd number. (This effectively eliminates worm 1 from making the other partially nibbled volume by itself).

      And worm 2 must nibble twice as many leaves:

      t2 = 2(nv + x)
      t2 = (2v + 1)n + 1

      Hence worm 2 nibbles (2v + 1) whole volumes and 1 additional page of volume (v + 1). (And this effectively eliminates worm 2 from making the other partially nibbled volume by itself).

      We want there to be at least 100 volumes, and so:

      k = v + (2v + 1) + 1
      k ≥ 100 ⇒ v ≥ 33

      And the volume nibbled from both ends (v + 1) must have the percentage of its pages nibbled that is the number of the volume.

      100 (x + 1) / n = v + 1

      n(v − 49) = 150

      So we can determine the solution by looking at divisor pairs of 150, such that n is greater than 40 and odd. It turns out there is only one possibility:

      n = 75, v = 51 → x = 38, p = 52%

      Programatically:

      from enigma import (divisors_pairs, div, printf)
      
      # consider possibilities for n = leaves per volume (odd divisor of 150)
      for (v, n) in divisors_pairs(150):
        if not (n > 40): break
        if not (n % 2 == 1): continue
        v += 49  # v = number of whole volumes nibbled by worm 1
        x = div(n + 1, 2)  # x = number of extra leaves nibbled by worm 1
        p = v + 1  # p = volume where percentage nibbled = volume number
        v2 = 2 * v + 1  # v2 = number of whole volumes nibbled by worm 2
        x2 = 1  # x2 = number of extra leaves nibbled by worm 2
        k = v + v2 + 1  # k = total number of volumes
        # output solution
        printf("n={n} k={k} [v={v} x={x} -> v2={v2} x2={x2} p={p}%]")
      

      Solution: There are 155 volumes in the set. And 75 leaves per volume.

      Each volume has 150 pages, and the complete set consists of 23250 pages.

      Worm 1 has eaten through vols 1 → 51, and 38 leaves of vol 52, so the last leaf consumed is pages 7725/7726. The total number of leaves consumed is 51×75 + 38 = 3863, and 3863×2 = 7726.

      Worm 2 starts from the other end of the shelf and eats through 7726 leaves, which is 103 volumes and 1 leaf. It eats through vols 155 → 53, and 1 leaf of vol 52 (= page 7651/7652).

      So vol 52 has had 38 leaves eaten by worm 1 and 1 leaf eaten by worm 2, for a total of 39/75 leaves = 52% of the leaves in the volume. And this percentage is the same as the volume number.

      The remaining volumes, vols 1-51 and 53-155, have been eaten through entirely by worm 1 or worm 2 respectively. In particular vol 100 has been eaten through 100% by worm 2.

      And so there are exactly 2 volumes where the percentage of leaves eaten is equal to the volume number.

      Like

      • Frits's avatar

        Frits 4:37 pm on 4 May 2025 Permalink | Reply

        I have gotten a similar manual solution, I didn’t want to publish it. The formula and variables are based on Jim’s original program.

         
        # n = 2x - 1
        # percentage 100(x + x2) = n(v + 1)
        # 50(n + 1) = n.v + n - 100x2
        # v = 49 + (100.x2 + 50) / n
        
        # 2t/n = (2nv + 2x) / n = 2v + (n + 1) / n = 2v + 1 + 1/n
        # so v2 = 2v + 1 and x2 must be 1
        
        # x2 = 1 implies v = 49 + 150 / n
        # as n must be odd and > 40 only one value remains for n
        # all other values can now be calculated.
        

        Like

      • Frits's avatar

        Frits 6:48 pm on 4 May 2025 Permalink | Reply

        We should also prove that there is/are no solution(s) if the percentage of the nibbled leaves for worm 1 was equal to the volume number (v < 100).
        As we have v = 50 + 50 / n this can only happen for n = 50 (not odd).

        So we can disregard this option.

        Like

  • Unknown's avatar

    Jim Randell 7:37 am on 1 May 2025 Permalink | Reply
    Tags:   

    Teaser 2554: Odd and even 

    From The Sunday Times, 4th September 2011 [link] [link]

    Roddy and Eve have a set of cards on which is written a consecutive sequence of whole numbers, one number on each card. Roddy challenges Oliver to choose two cards at random, promising a prize if the sum of the numbers is odd. Eve issues the same challenge, but offers a prize for an even sum. Oliver accepts the challenge that gives him the greater probability of winning. This probability is a whole number percentage and, when reversed, it gives the two-digit number of cards.

    Whose challenge did Oliver accept and how many cards are there?

    [teaser2554]

     
    • Jim Randell's avatar

      Jim Randell 7:37 am on 1 May 2025 Permalink | Reply

      We can consider sets of cards numbered 1 .. n, as if each card has a offset x from these values, then the sum of the pairs is increased by 2x, and this doesn’t change whether the sum is odd or even.

      The percentage must be > 50%, and it is the reverse of the number of cards.

      This Python program runs in 77ms. (Internal runtime is 16ms).

      from enigma import (irange, subsets, div, nrev, printf)
      
      # consider 2-digit percentages >50%
      for p in irange(51, 99):
        # reverse is the number of cards
        n = nrev(p)
        if n < 10: continue
      
        # count odd/even pairs
        t = [0, 0]
        for (x, y) in subsets(irange(1, n), size=2):
          t[(x + y) % 2] += 1
        T = sum(t)
      
        # calculate higher probability as a percentage
        x = max(t)
        q = div(100 * x, T)
        if q == p:
          # output solution
          printf("n={n} -> t={t} p={p}% (= {x}/{T})")
      

      Solution: There are 25 cards. Oliver chooses Roddy’s challenge.

      With 25 cards there are C(25, 2) = 300 ways of choosing a pair of cards.

      144 of them give an even total, and 156 of them give an odd total.

      So the probability of choosing cards that give an odd total is:

      156 / 300 = 0.52 or 52%

      Like

    • Frits's avatar

      Frits 6:06 pm on 1 May 2025 Permalink | Reply

      # only consider odd number of cards, n = 10 * a + b
      # number odd sums = 1st even * 2nd odd + 1st odd * 2nd even
      # ((n - 1) // 2) * ((n + 1) // 2) + ((n + 1) // 2) * ((n - 1) // 2) or
      # (n**2 - 1) / 2
      # percentage = number odd / (n * (n - 1) = (n + 1) / (2 * n)
      
      # a whole number percentage and, when reversed, 
      # it gives the two-digit number of cards
      # 100 * ((10 * a + b) + 1) / ((10 * a + b) * 2) = 10 * b + a
      # 500 * a + 50 * b + 50 = (a + 10 * b) * (10 * a + b)
      # 10 * a**2 + (101 * b - 500) * a + 10 * b**2 - 50 * b - 50 = 0
      
      for b in range(5, 10, 2):
        a, r = divmod(500 - 101 * b + 3 * (1089 * b**2 - 11000 * b + 28000)**.5, 20)
        if r: continue
        print(f"There are {10 * int(a) + b} cards. "
              f"Oliver chooses Roddy's challenge")
      

      Like

  • Unknown's avatar

    Jim Randell 8:04 am on 29 April 2025 Permalink | Reply
    Tags: ,   

    Teaser 2526: [Lucky dates] 

    From The Sunday Times, 20th February 2011 [link] [link]

    Orla was married in this century, on her lucky day of the week, and her twin sister Enya’s lucky-number day of the month. Enya told Orla on that big day that she intended her own wedding to be on the same lucky day of the week and lucky-number day of the month. She realised that it could not be in the same year as Orla’s, but Orla added that Enya could not be married in the following year, either. Enya married months later, the actual number of months being the sum of the six digits of Orla’s wedding date.

    What was Orla’s wedding date?

    This puzzle was originally published with no title.

    [teaser2526]

     
    • Jim Randell's avatar

      Jim Randell 8:04 am on 29 April 2025 Permalink | Reply

      I am assuming that “this century” means the 21st century (which started on 2001-01-01).

      And “the sum of the six digits of Orla’s wedding date” refers to the sum of date expressed as “DD/MM/YY” (i.e. using the final 2 digits of the year, rather than indicating that Orla’s wedding date is of the form “D/M/YYYY”).

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

      from datetime import (date, timedelta)
      from enigma import (irange, repeat, inc, dsum, catch, printf)
      
      # increment date (<d>, <m>, <y>) by <i> months
      def incmonth(d, m, y, i=1):
        m += i
        while m > 12:
          m -= 12
          y += 1
        return catch(date, y, m, d)
      
      # consider dates in the 21st century for Orla's wedding
      for d1 in repeat(inc(timedelta(days=1)), date(2001, 1, 1)):
        if d1.year > 2009: break
        # calculate the day of the week
        wd = d1.isoweekday()
        valid = lambda x: x is not None and x.isoweekday() == wd
        # calculate the digit sum of the date (6 digits)
        (d, m, y) = (d1.day, d1.month, d1.year)
        n = dsum(d) + dsum(m) + dsum(y % 100)
        # consider a date <n> months in advance
        d2 = incmonth(d, m, y, n)
        if not valid(d2): continue
        if not (d2.year - d1.year > 1): continue
        # and check no intervening date would do
        if any(valid(incmonth(d, m, y, i)) for i in irange(1, n - 1)): continue
        # output solution
        printf("Orla = {d1}, Enya = {d2} [{n} months later]")
      

      Solution: Orla’s wedding day was: Wednesday, 31st December 2008.

      The next (Wednesday, 31st) occurs on: Wednesday, 31st March 2010, and so this is Enya’s wedding day.

      Enya’s wedding day occurs 15 months after Orla’s, and the sum of the digits in Orla’s date expressed as: 31/12/08 is 15.


      If we were to use an 8-digit format for the dates (e.g. “DD/MM/YYYY” or “YYYY-MM-DD”), we can get a solution of:

      Orla = Tuesday, 31st July 2007
      Enya = Tuesday, 31st March 2009 (20 months later)

      Like

    • Frits's avatar

      Frits 12:32 pm on 30 April 2025 Permalink | Reply

      from enigma import SubstitutedExpression
      from datetime import date
      
      # check date format and possible other checks
      def check(AB, CD, EF, extra=False):
        try:
          wd = date(2000 + EF, CD, AB).weekday()
        except ValueError:
          return False
        
        if extra:
          # check if in the next year there are any dates with day number AB
          # on the same weekday as Orla's
          for m in range(1, 13):
            try:
              if date(2000 + EF + 1, m, AB).weekday() == wd: 
                return False
            except ValueError:
              continue
        return True
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # Orla : AB-CD-EF  Enya: AB-GH-IJ  (dd-mm-yy)
          "AB < 32",
          "CD < 13",
          # Enya married months later but not in the following year
          "(sd := A + B + C + D + E + F) > 24 - CD",
          # check validity of Orla's date and that Enya could not be married 
          # in the following year
          "check(AB, CD, EF, 1)",
          
          "IJ <= 11",   # 2011
          # Enya could not be married in the following year
          "IJ > EF + 1",
          
          # Enya married months later, the actual number of months being the sum 
          # of the six digits of Orla's wedding date
          "sd - 12 * (IJ - EF) + CD = GH",
          "0 < GH < 13",
          
          # check validity of Enya's date
          "check(AB, GH, IJ)",
          # Enya married on the same day of the week as Orla
          "date(2000 + EF, CD, AB).weekday() == date(2000 + IJ, GH, AB).weekday()"
        ],
        answer="(AB, CD, 2000 + EF)",
        d2i=dict([(k, "ICGE") for k in range(2, 4)] +
                 [(k, "AICGE") for k in range(4, 10)]),
        distinct="",
        s2d=dict(E=0),
        env=dict(date=date, check=check),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"answer: {'-'.join(str(x) for x in ans)}")
      

      Like

  • Unknown's avatar

    Jim Randell 6:41 am on 27 April 2025 Permalink | Reply
    Tags:   

    Teaser 3266: Where are the keys? 

    From The Sunday Times, 27th April 2025 [link] [link]

    Skaredahora’s venture into atonal music — music with no fixed key — had as its (so-called!) melody a long sequence employing 8 different notes, which he labelled Q to X. Behind his composition were 7 keys each using 4 notes. Every three consecutive melody notes obeyed specific rules:

    (1) all three had to be different;
    (2) they had to belong to exactly one of the keys;
    (3) they had not to repeat in the same order any other three consecutive notes;
    (4) the key had to change at every step.

    His chosen melody was:

    WUQRVWQRXWTRUQWRVQWTSRQWVRSTWQVRWQURTWXRQVWRTSX

    What were the keys involving X, in alphabetical order individually and collectively (e.g. QRTX, SUVX)?

    [teaser3266]

     
    • Jim Randell's avatar

      Jim Randell 8:08 am on 27 April 2025 Permalink | Reply

      Be careful to get the melody sequence exactly right! (I initially had a typo that made it impossible to find a solution).

      This Python 3 program solves the puzzle constructively. It first checks that triples of 3 consecutive notes are all different (3), and each consists of 3 different notes (1). It then fills out keys that can be used to play the triples in the required order, with a key change at each step (4). And then finally, once a set of keys is found, it checks that each triple belongs to exactly one of the keys (2).

      The program runs in 72ms. (Internal runtime is 1.2ms).

      from enigma import (tuples, join, fail, update, singleton, seq2str, printf)
      
      # the chosen melody
      melody = "WUQRVWQRXWTRUQWRVQWTSRQWVRSTWQVRWQURTWXRQVWRTSX"
      
      K = 4  # number of notes in a key
      N = 7  # number of keys
      
      # split into triples
      ts = list(tuples(melody, 3, fn=join))
      # check they are all different (3), and consist of different notes (1)
      if not (len(ts) == len(set(ts)) and all(len(t) == len(set(t)) for t in ts)):
        fail(msg="invalid melody")
      
      # find keys <ks> for the triples <ts>, with no adjacent repeats (4)
      # ts = triples to solve
      # n = number of next key to allocate
      # ks = map of keys to notes
      # pk = previous key
      def solve(ts, n=1, ks=dict(), pk=0):
        # are we done?
        if not ts:
          yield ks
          return
      
        # look at the next triple
        t = set(ts[0])
      
        # examine its relation to existing keys
        (ks1, ks2) = (list(), list())
        for (k, v) in ks.items():
          if v.issuperset(t):
            # it is already fully covered by an existing key
            ks1.append(k)
          else:
            vt = v.union(t)
            if len(vt) <= K:
              # an existing key can be extended to cover it
              ks2.append((k, vt))
      
        # there are three cases:
        if ks1:
          # it is already covered by exactly one key
          k = singleton(ks1, default=0)
          if k > 0 and k != pk: yield from solve(ts[1:], n, ks, k)
          return  # we are done
      
        # extend an existing key
        for (k, v) in ks2:
          if k != pk:
            yield from solve(ts[1:], n, update(ks, [(k, v)]), k)
      
        # allocate a new key
        if  n <= N:
          yield from solve(ts[1:], n + 1, update(ks, [(n, t)]), n)
      
      # construct the progression of keys, where each triple belongs to exactly one key (2)
      def check_keys(ts, ks):
        ss = list()
        # check each triple belongs to just one of the keys
        for t in ts:
          ss.append(singleton(k for (k, v) in ks.items() if v.issuperset(t)))
          if ss[-1] is None: return
        return ss
      
      # solve the puzzle
      for ks in solve(ts):
        ss = check_keys(ts, ks)
        if not ss: continue
        # output solution
        for k in sorted(ks):
          printf("{k}: {vs}{s}", vs=seq2str(ks[k], sort=1, sep=" "), s=(' <-' if 'X' in ks[k] else ''))
        printf("{ss}", ss=seq2str(ss, sep=" "))
        printf()
      

      Solution: The keys involving X are: QRUX, RVWX, STWX.

      The full set of keys is:

      1: (Q U V W)
      2: (Q R U X)
      3: (Q R S V)
      4: (R V W X)
      5: (Q R T W)
      6: (S T W X)
      7: (R S T U)

      (The required answer to the puzzle is keys 2, 4, 6, which all contain the note X).

      And this gives the following progression of keys:

      (1 2 3 4 1 5 2 4 6 5 7 2 1 5 4 3 1 5 6 7 3 5 1 4 3 7 6 5 1 3 4 5 1 2 7 5 6 4 2 3 1 4 5 7 6)

      Like

      • Jim Randell's avatar

        Jim Randell 12:06 pm on 4 May 2025 Permalink | Reply

        We can slightly simplify the code using the following observation (due to JohnZ [link]).

        Each set of 3 consecutive notes in the melody must uniquely identify a key, and adjacent triples must identify different keys, hence no key can consist of 4 consecutive notes in the melody, so we can identify a set of invalid keys that we can avoid to ensure each consecutive key must be different. And this means we don’t have to drag information about the previously used key as we construct the progression of keys.

        Here is a modified version of my program adapted to use this method. It has an internal runtime of 524µs.

        from enigma import (tuples, join, fail, update, singleton, seq2str, printf)
        
        # the chosen melody
        melody = "WUQRVWQRXWTRUQWRVQWTSRQWVRSTWQVRWQURTWXRQVWRTSX"
        
        K = 4  # number of notes in a key
        N = 7  # number of keys
        
        # split into triples
        ts = list(tuples(melody, 3, fn=join))
        # check they are all different (3), and consist of different notes (1)
        if not (len(ts) == len(set(ts)) and all(len(t) == len(set(t)) for t in ts)):
          fail(msg="invalid melody")
        
        # but no key can include 4 consecutive notes from the melody, so if we avoid
        # the following keys, there can be no adjacent repeats (4)
        invalid = set(tuples(melody, 4, fn=frozenset))
        
        # find keys <ks> for the triples <ts>, with no adjacent repeats (4)
        # ts = triples to solve
        # n = number of next key to allocate
        # ks = map of keys to notes
        def solve(ts, n=1, ks=dict()):
          # are we done?
          if not ts:
            yield ks
            return
        
          # look at the next triple
          t = set(ts[0])
        
          # examine its relation to existing keys
          (ks1, ks2) = (list(), list())
          for (k, v) in ks.items():
            if v.issuperset(t):
              # it is already fully covered by an existing key
              ks1.append(k)
            else:
              vt = v.union(t)
              if len(vt) <= K and vt not in invalid:
                # an existing key can be extended to cover it
                ks2.append((k, vt))
        
          # there are three cases:
          if ks1:
            # it is already covered by exactly one key
            k = singleton(ks1, default=0)
            if k > 0: yield from solve(ts[1:], n, ks)
            return  # we are done
        
          # extend an existing key
          for (k, v) in ks2:
            yield from solve(ts[1:], n, update(ks, [(k, v)]))
        
          # allocate a new key
          if  n <= N:
            yield from solve(ts[1:], n + 1, update(ks, [(n, t)]))
        
        # construct the progression of keys, where each triple belongs to exactly one key (2)
        def check_keys(ts, ks):
          ss = list()
          # check each triple belongs to just one of the keys
          for t in ts:
            ss.append(singleton(k for (k, v) in ks.items() if v.issuperset(t)))
            if ss[-1] is None: return
          return ss
        
        # solve the puzzle
        for ks in solve(ts):
          ss = check_keys(ts, ks)
          if not ss: continue
          # output solution
          for k in sorted(ks):
            printf("{k}: {vs}{s}", vs=seq2str(ks[k], sort=1, sep=" "), s=(' <-' if 'X' in ks[k] else ''))
          printf("{ss}", ss=seq2str(ss, sep=" "))
          printf()
        

        Like

    • Pete Good's avatar

      Pete Good 2:57 pm on 30 April 2025 Permalink | Reply

      Jim, The Sunday Times published this teaser under the title “Where Are the Keys?”

      Like

  • Unknown's avatar

    Jim Randell 6:45 am on 24 April 2025 Permalink | Reply
    Tags:   

    Teaser 2555: Today’s value 

    From The Sunday Times, 11th September 2011 [link] [link]

    These 10 children’s bricks are numbered from 1 to 10. Where a brick rests on two others its number is the difference of their two numbers. Given that U = 1 …

    What is the number DAY?

    [teaser2555]

     
    • Jim Randell's avatar

      Jim Randell 6:46 am on 24 April 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # numbers range from 1 to 10
      --base=11
      --digits="1-10"
      
      "abs(U - N) = S"
      "abs(D - A) = U"
      "abs(A - Y) = N"
      "abs(T - I) = D"
      "abs(I - M) = A"
      "abs(M - E) = Y"
      
      --assign="U,1"
      --answer="(D, A, Y)"
      
      --template=""
      

      Solution: DAY = 672.

      At least the digits have values: D = 6, A = 7, Y = 2.

      But since the digits go from 1 to 10, you could argue that the result is expressed in base 11, so:

      DAY = 672 [base 11] = 805 [base 10]

      Like

      • Ruud's avatar

        Ruud 6:14 am on 28 April 2025 Permalink | Reply

        Brute force one liner:

        import itertools
        
        print(
            *(
                f"{d}{a}{y}"
                for s, n, d, a, y, t, i, m, e in itertools.permutations(range(2, 11), 9)
                if s == abs(1 - n) and 1 == abs(d - a) and n == abs(a - y) and d == abs(t - i) and a == abs(i - m) and y == abs(m - e)
            )
        )
        

        Like

    • Frits's avatar

      Frits 10:51 am on 26 April 2025 Permalink | Reply

      #    S
      #   U N                            U = 1
      #  D A Y
      # T I M E
      
      sols = set()
      U, mx = 1, 10
      r0 = set(range(2, mx + 1)) # remaining digits to use
      
      for N in range(2, mx - 1):  # no using <mx - 1> and <mx> on this level
        S = N - U
        r1 = r0 - {N, S}
        if len(r1) != len(r0) - 2: continue
        for A in r1 - {mx}:       
          r2 = r1 - {A}
          for D in (A - U, A + U):
            if D not in r2 or D == mx: continue
            r3 = r2 - {D}
            for Y in (A - N, A + N):
              if Y not in r3 or Y == mx: continue
              r4 = r3 - {Y}
              if max(r4) - min(r4) < max(D, A, Y): continue
              for I in r4:
                for M in (I - A, I + A):
                  if M not in r4: continue
                  for T in (I - D, I + D):
                    if T not in r4: continue
                    for E in (M - Y, M + Y):
                      if {T, I, M, E} != r4: continue
                      sols.add(100 * D + 10 * A + Y)
      
      print("answer(s):", ' or '.join(str(s) for s in sols))
      

      Like

  • Unknown's avatar

    Jim Randell 8:18 am on 22 April 2025 Permalink | Reply
    Tags:   

    Teaser 2550: Cricket averages 

    From The Sunday Times, 7th August 2011 [link] [link]

    Playing for our local team, Sam and Oliver between them took 5 wickets in each innings, taking 5 wickets each overall. Sam’s averages (i.e. runs per wicket) were lower than Oliver’s in both innings, but overall Sam had the higher average. All six averages were single non-zero digits.

    If you knew Sam’s overall average, it would then be possible to calculate the number of runs scored against Sam and against Oliver.

    What were the total runs scored against: (a) Sam and (b) Oliver?

    [teaser2550]

     
    • Jim Randell's avatar

      Jim Randell 8:19 am on 22 April 2025 Permalink | Reply

      This Python program chooses possible numbers of wickets taken by each bowler in each innings, and then considers possible averages such than Sam has a lower average than Oliver in each innings, but a higher average overall.

      From the possible candidate scenarios we then looks for situations where knowing Sam’s overall average allows you to determine the overall number of runs scored against each bowler.

      It runs in 65ms. (Internal runtime is 2.2ms).

      from enigma import (Record, decompose, subsets, irange, div, filter_unique, printf)
      
      # generate possible scenarios
      def generate():
        # choose numbers of wickets taken by S and O in the first innings
        for (wS1, wO1) in decompose(5, 2, increasing=0, sep=0, min_v=1):
          # numbers of wickets taken in the second innings
          (wS2, wO2) = (5 - wS1, 5 - wO1)
      
          # S and O's averages in the first innings (single digit numbers)
          for (aS1, aO1) in subsets(irange(1, 9), size=2):
            # calculate runs against S and O in the first innings
            (rS1, rO1) = (aS1 * wS1, aO1 * wO1)
      
            # averages in the second innings
            for (aS2, aO2) in subsets(irange(1, 9), size=2):
              # calculate runs against S and O in the second innings
              (rS2, rO2) = (aS2 * wS2, aO2 * wO2)
      
              # overall averages
              (orS, orO) = (rS1 + rS2, rO1 + rO2)
              (oaS, oaO) = (div(orS, 5), div(orO, 5))
              if oaS is None or oaO is None or not (oaS > oaO): continue
      
              # output: first innings; second innings; totals
              printf("[1: S={wS1}w for {rS1}r avg={aS1}, O={wO1}w for {rO1}r avg={aO1}; 2: S={wS2}w for {rS2}r avg={aS2}, O={wO2}w for {rO2}r avg={aO2}; T: S=5w for {orS}r avg={oaS}, O=5w for {orO}r avg={oaO}]")
              yield Record(
                wS1=wS1, wO1=wO1, rS1=rS1, rO1=rO1, aS1=aS1, aO1=aO1,
                wS2=wS2, wO2=wO2, rS2=rS2, rO2=rO2, aS2=aS2, aO2=aO2,
                orS=orS, orO=orO, oaS=oaS, oaO=oaO,
              )
      
      # if you knew oaS, then you could determine (orS, orO)
      rs = filter_unique(generate(), (lambda r: r.oaS), (lambda r: (r.orS, r.orO)))
      for r in rs.unique:
        printf("S={r.orS}r O={r.orO}r [{r}]")
      

      Solution: (a) 35 runs were scored against Sam; (b) 25 runs were scored against Oliver.

      Here is one scenario:

      First innings:
      Sam took 1 wicket, for 3 runs (avg = 3/1 = 3).
      Oliver took 4 wickets, for 16 runs (avg = 16/4 = 4).

      Second innings:
      Sam took 4 wickets, for 32 runs (avg = 32/4 = 8).
      Oliver took 1 wicket, for 9 runs (avg = 9/1 = 9).

      Overall:
      Sam took 5 wickets, for 35 runs (avg = 35/5 = 7).
      Oliver took 5 wickets, for 25 runs (avg = 25/5 = 5).

      In each innings Sam has a lower average than Oliver, but overall Oliver has a lower average than Sam.

      There is a second scenario, with the innings in the opposite order.

      Like

    • Frits's avatar

      Frits 3:35 pm on 22 April 2025 Permalink | Reply

      Inventing the wheel again to determine a duplicate entry. I didn’t feel like grouping entries per overall average.

      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # Sam:    1st: A wickets, avg B, 2nd: C wickets, avg D, overall avg E
          # Oliver: 1st: P wickets, avg Q, 2nd: R wickets, avg S, overall avg T
          
          # between them took 5 wickets in each innings and taking 
          # 5 wickets each overall (assume Sam took more wickets in the 2nd inning)
          "5 - A = P", "5 - P = R", "5 - R = C",
          "A < C", "A + C == 5", 
          # Sam had lower averages ...
          "B < Q", "D < S",
          # total averages"
          "div(trs := A * B + C * D, 5) = E",
          "div(tro := P * Q + R * S, 5) = T",
          # ... but a higher overall average
          "E > T",
        ],
        answer="E, (trs, tro)",
        d2i=dict([(0, "ACPRQS")] +
                 [(k, "ACPR") for k in range(5, 9)] +
                 [(9, "ACPRBD")]),
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      c, prev, sols = 0, (-1, -1), set()
      # get unique values of Sam's overall average
      for (soa, tr) in sorted(p.answers()) + [(-1, prev)]:
        if soa != prev[0]:
          if c == 1:
            sols.add(prev[1])
          c = 1
        else:
          c += 1
        prev = (soa, tr)
      
      # print total runs
      for trS, trO in sols:  
        print(f"answer: a() = {trS} runs, b() = {trO} runs")
      

      Like

  • Unknown's avatar

    Jim Randell 6:59 am on 20 April 2025 Permalink | Reply
    Tags:   

    Teaser 3265: Easter prayer 

    From The Sunday Times, 20th April 2025 [link] [link]

    Millions of people will today turn their thoughts to those throughout the world whose lives are made miserable by the ravages of war and bitter conflict. I have taken a selection of letters from the alphabet and given each a different single-digit value. In this way the two words that form the title of this teaser represent two six-digit numbers, one of which is a factor of the other.

    There are two letters in EASTER PRAYER for which the following is true. If I told you the value of that letter alone, you wouldn’t be able to work out all the others, but if I told you both their values then you could work out all the values.

    What is the value of PRAYER?

    [teaser3265]

     
    • Jim Randell's avatar

      Jim Randell 7:49 am on 20 April 2025 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library to find solutions to the alphametic puzzle, and then looks for a pair of symbols that allow a unique solution to be determined if both their values are known, but not if only one of their values is known.

      It runs in 155ms. (Internal runtime is 72ms).

      from enigma import (SubstitutedExpression, filter_unique, singleton, subsets, translate, printf)
      
      # find solutions to the alphametic puzzle
      expr = "EASTER % PRAYER == 0 or PRAYER % EASTER == 0"
      tmpl = "[EASTER={EASTER} PRAYER={PRAYER}]"
      p = SubstitutedExpression(expr, template=tmpl)
      ss = list(p.solve(verbose="T"))
      
      # for each symbol record values with multiple solutions
      rs = set()
      for k in p.symbols:
        for s in filter_unique(ss, (lambda s: s[k])).non_unique:
          rs.add((k, s[k]))
      
      # look for pairs that give a unique solution
      for ((k1, v1), (k2, v2)) in subsets(rs, size=2):
        if k1 == k2: continue
        s = singleton(s for s in ss if s[k1] == v1 and s[k2] == v2)
        if s is None: continue
      
        # output solution
        (EASTER, PRAYER) = (translate(x, s) for x in ["EASTER", "PRAYER"])
        printf("{k1}={v1} {k2}={v2} -> EASTER={EASTER} PRAYER={PRAYER}")
      

      Solution: PRAYER = 159275.

      There are 3 candidate solutions:

      EASTER = 796375; PRAYER = 159275
      EASTER = 796875; PRAYER = 159375
      EASTER = 798375; PRAYER = 159675

      In each case EASTER = 5 × PRAYER.

      And if we were told either of S = 6 or T = 3 we could only narrow the candidates down to 2 solutions. But if we are told both together, then we can narrow the candidates down to a single solution (the first one).

      Like

    • Frits's avatar

      Frits 1:49 pm on 20 April 2025 Permalink | Reply

      As usual built for speed.
      Always difficult to code a branch that is not easy to test.

      from itertools import permutations, combinations, product
      from functools import reduce
      
      # digits to number
      d2n = lambda *s: reduce(lambda x,  y: 10 * x + y, s)
      # return <n>th digit of number <num>, n has to be > 0
      nth = lambda num, n, ln=1: int(str(num)[n - 1:n - 1 + ln])
      
      # EASTER % PRAYER == 0 or PRAYER % EASTER == 0 
      cands = []
      for E, R in permutations(range(10), 2):  
        if not E: continue
        ER = d2n(E, R)
        for m in range(2, 10):                   # multiplier
          if (m * ER) % 100 != ER: continue
          
          if E >= m:  # EASTER might be the numerator
            for P in [n for n in range(1, E // m + 1) if n not in {E, R}]:
              PR = d2n(P, R)
              # check if m * PR9999 can become at least Exxxxx
              if nth(m * (PR + 1), 1) < E: continue
              for A in set(range(10)) - {E, R, P}:
                PRA = d2n(P, R, A)
                EA = d2n(E, A)
                # check if m * PRA999 can become at least EAxxxx
                if nth(m * (PRA + 1), 1, 2) < EA: continue
                for Y in set(range(10)) - {E, R, P, A}:
                  PRAYER = d2n(P, R, A, Y, E, R)
                  EASTER = m * PRAYER
                  if nth(EASTER, 1, 2) != EA: continue
                  S = nth(EASTER, 3)
                  T = nth(EASTER, 4)
                  if S == T or not {E, R, P, A, Y}.isdisjoint({S, T}): continue
                  cands.append(([A, E, P, R, S, T, Y]))
                  
          if m * E < 10:  # EASTER might be the denominator 
            for P in range(m * E, 10):
              if P == R: continue
              PR = d2n(P, R)
              for A in set(range(10)) - {E, R, P}:
                EA = d2n(E, A)
                # check if m * EA9999 can become at least PRxxxx
                if nth(m * (EA + 1), 1, 2) < PR: continue
                PRA = d2n(P, R, A)
                for S in set(range(10)) - {E, R, P, A}:
                  EAS = d2n(E, A, S)
                  # check if m * EAS999 can become at least PRAxxx
                  if nth(m * (EAS + 1), 1, 3) < PRA: continue
                  for T in set(range(10)) - {E, R, P, A, S}:
                    EASTER = d2n(E, A, S, T, E, R)
                    PRAYER = m * EASTER
                    if nth(PRAYER, 1, 3) != PRA: continue
                    Y = nth(PRAYER, 4)
                    if Y in {E, R, P, A, S, T}: continue
                    cands.append(([A, E, P, R, S, T, Y]))
      
      if not cands:
        print("no solution")
      
      # positions of letters that have multiple values (but not all different)
      pos = [j  for j in range(7) if 1 < len({c[j] for c in cands}) < len(cands)]
      
      # choose two of those letters
      for a, b in combinations(pos, 2):
        # collect letter values ...
        lv = [[c[x] for c in cands] for x in (a, b)]
        # ... that occur more than once
        lv = [{x for i, x in enumerate(v) if x in v[i + 1:]} for v in lv]
        # choose for each letter a value
        for p1, p2 in product(*lv):
          # corresponding candidates
          if len(cs := [c for c in cands if c[a] == p1 and c[b] == p2]) != 1: 
            continue 
          # select PRAYER
          print("answer:", ''.join(str(cs[0][i]) for i in [2, 3, 0, 6, 1, 3]))
      

      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