Tagged: by: Danny Roth Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:44 am on 16 December 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2464 : [Trapezium plots] 

    From The Sunday Times, 13th December 2009 [link]

    George and Martha have a trapezium-shaped garden [*]. The southern border (running east-west) is 100 metres long, as is the western border (north-south). The eastern border (also north-south) is a whole number of metres long (more than 100, but less than 200) and the fourth border is also a whole number of metres long. Coincidentally, if you replace “100” with “1000” and “200” with “2000”, exactly the same can be said of the park over the road.

    “Then the park’s sides are all 10 times those of our garden”, Martha said. “Actually, they are not”, George replied.

    How long is the park’s longest side?

    [*] Trapezium = a quadrilateral with two parallel sides.

    This puzzle was originally published with no title.

    [teaser2464]

     
    • Jim Randell's avatar

      Jim Randell 8:44 am on 16 December 2025 Permalink | Reply

      We can construct the shape of the plots by taking a Pythagorean triangle (x, y, z), and then constructing the square on the y side to give a trapezium with sides (x + y, y, y, z).

      (I believe in US terminology this shape would be referred to as a trapezoid).

      This Python program collects possible triangles with y = 100 (for the garden) or y = 1000 (for the park), and then looks for a pair where not all sides are in the ratio 10:1.

      It runs in 80ms. (Internal runtime is 150µs).

      from enigma import (cproduct, printf)
      import pells
      
      # generate candidate triangles
      def triangles(y):
        for (x, z) in pells.diop_quad(1, -1, -y*y):
          if not (x < y): break
          if x > 0:
            yield (x, y, z)
      
      # find candidate dimensions for the garden and the park
      gs = list(triangles(100))
      ps = list(triangles(1000))
      
      # find garden/park combinations that are not in 1:10 ratio
      for ((x1, y1, z1), (x2, y2, z2)) in cproduct([gs, ps]):
        if (x2 == 10 * x1 and z2 == 10 * z1): continue
        printf("garden: S={y1}, W={y1}, E={E}, Z={z1}", E=x1 + 100)
        printf("park: S={y2}, W={y2}, E={E}, Z={z2}", E=x2 + 1000)
        printf()
      

      Solution: The longest side of the park measures 1225 m.

      The garden has dimensions: 175, 100, 100, 125.

      The park has dimensions: 1225, 1000, 1000, 1025.

      Like

      • Jim Randell's avatar

        Jim Randell 2:50 pm on 16 December 2025 Permalink | Reply

        Alternatively, we can just look at possible dimensions of the park, and reject any where each side is divisible by 10.

        The following code has an internal runtime of 98µs.

        from enigma import printf
        import pells
        
        # generate candidate triangles
        def triangles(y):
          for (x, z) in pells.diop_quad(1, -1, -y*y):
            if not (x < y): break
            if x > 0:
              yield (x, y, z)
        
        # find candidate dimensions for the park
        for (x, y, z) in triangles(1000):
          # reject dimensions that are all divisible by 10
          if 0 == x % 10 == y % 10 == z % 10: continue
          # output solution
          printf("park: S={y}, W={y}, E={E}, Z={z}", E=x + 1000)
        

        Like

    • Ruud's avatar

      Ruud 1:05 pm on 16 December 2025 Permalink | Reply

      import istr
      
      s = {}
      for n in (100, 1000):
          s[n] = {(a ** (1 / 2) * 1000 / n, b * 1000 / n) for b in range(n + 1, 2 * n) if istr.is_square((a := (b - n) ** 2 + n**2))}
      
      print([max(a, b) for a, b in s[1000] - s[100]])
      

      Like

    • Ellen Napier's avatar

      Ellen Napier 4:13 pm on 26 December 2025 Permalink | Reply

      Specifically, a Right Trapezoid (two right angles), which is implied by the compass directions given.

      Like

  • Unknown's avatar

    Jim Randell 9:55 am on 18 November 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2483: [Cricket scores] 

    From The Sunday Times, 25th April 2010 [link]

    George and Martha were watching cricket. After the visitors’ innings, George said: “How odd! Every time two batsmen were together, the partnership was broken when the team’s total was the product of their two batting-order numbers. But one partnership created a stand of over 50”. “And”, said Martha, “each time an odd-numbered batsman was out, the next batsman out was even-numbered. This continued until the dismissal of batsman 11”. The home side’s innings saw a similar pattern, but their biggest stand was higher.

    What was (a) the home team’s biggest stand; (b) the result of the match?

    This puzzle was originally published with no title.

    [teaser2483]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 18 November 2025 Permalink | Reply

      This Python program runs in 76ms. (Internal runtime is 238µs).

      from enigma import (defaultdict, subsets, tuples, printf)
      
      # solve for pairs <a> and <b>, next batsman <n>
      def solve(a=1, b=2, n=3, ts=list(), bs=list()):
        if n < 13:
          # total score when this pair ends
          t = a * b
          if ts and t < ts[-1]: return
          if n == 12:
            # either of the final pair is dismissed
            yield (ts + [t], bs + [(a, b)])
          else:
            # a is dismissed
            if not (a % 2 != 0 and bs and bs[-1] % 2 == 1):
              yield from solve(b, n, n + 1, ts + [t], bs + [a])
            # b is dismissed
            if not (b % 2 != 0 and bs and bs[-1] % 2 == 1):
              yield from solve(a, n, n + 1, ts + [t], bs + [b])
      
      # record scenarios by maximum pair score
      rs = defaultdict(list)
      
      # find possible scores
      for (ts, bs) in solve():
        # find the maximum score for a pair
        m = max(y - x for (x, y) in tuples(ts, 2))
        if not (m > 50): continue
        printf("[totals = {ts}; batsmen = {bs}; m = {m}]")
        rs[m].append((ts, bs))
      
      # choose max pair scores for visitors and home teams
      for (v, h) in subsets(sorted(rs.keys()), size=2):
        printf("\nmax pair: visitors = {v}, home = {h}")
        for (ts, bs) in rs[v]:
          printf("visitors: totals = {ts}; batsmen = {bs}")
        for (ts, bs) in rs[h]:
          printf("home: totals = {ts}; batsmen = {bs}")
      

      Solution: (a) The home team’s biggest stand was 81; (b) The result of the match was a tie (i.e. both teams achieved the same number of runs).

      The visitor’s scoresheet is one of the following:

      1 & 2: stand = 2; total = 2 [1 dismissed]
      2 & 3: stand = 4; total = 6 [2 dismissed]
      3 & 4: stand = 6; total = 12 [4 dismissed]

      or:

      1 & 2: stand = 2; total = 2 [2 dismissed]
      1 & 3: stand = 1; total = 3 [1 dismissed]
      3 & 4: stand = 9; total = 12 [4 dismissed]

      then:

      3 & 5: stand = 3; total = 15 [5 dismissed]
      3 & 6: stand = 3; total = 18 [6 dismissed]
      3 & 7: stand = 3; total = 21 [7 dismissed]
      3 & 8: stand = 3; total = 24 [8 dismissed]
      3 & 9: stand = 3; total = 27 [3 dismissed]
      9 & 10: stand = 63; total = 90 [10 dismissed]
      9 & 11: stand = 9; total = 99

      And the home team’s scoresheet is:

      1 & 2: stand = 2; total = 2 [2 dismissed]
      1 & 3: stand = 1; total = 3 [3 dismissed]
      1 & 4: stand = 1; total = 4 [4 dismissed]
      1 & 5: stand = 1; total = 5 [5 dismissed]
      1 & 6: stand = 1; total = 6 [6 dismissed]
      1 & 7: stand = 1; total = 7 [7 dismissed]
      1 & 8: stand = 1; total = 8 [8 dismissed]
      1 & 9: stand = 1; total = 9 [1 dismissed]
      9 & 10: stand = 81; total = 90 [10 dismissed]
      9 & 11: stand = 9; total = 99

      Each team has a final total of 99, and so the match is tied.

      Like

  • Unknown's avatar

    Jim Randell 10:09 am on 21 October 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2494: [Council election] 

    From The Sunday Times, 11th July 2010 [link] [link]

    George and Martha’s council has 40 seats shared between Labour, Conservatives and Democrats. Before a recent election, Labour had an overall majority, with Conservatives second. After it, the order was Conservatives first, Labour second, Democrats third, both Conservatives and Democrats having improved their numbers of seats. To achieve a majority, the Conservatives formed a coalition with the Democrats. Each party had a whole two-figure number percentage change in its number of seats; the Democrats’ percentage increase was an odd number.

    How many seats did each party win?

    This puzzle was originally published with no title.

    [teaser2494]

     
    • Jim Randell's avatar

      Jim Randell 10:10 am on 21 October 2025 Permalink | Reply

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

      It runs in 87ms. (Internal runtime of the generated code is 6.8ms).

      from enigma import (SubstitutedExpression, irange, div, sprintf as f, printf)
      
      # calculate (integer) percentage change
      def change(before, after): return div(100 * (after - before), before)
      
      # 2-digit numbers for change percentages
      d2 = irange(10, 99)
      d2o = irange(11, 99, step=2)  # odd numbers
      
      # symbols for each party before (= X1) and after (= X2)
      (L1, C1, D1, L2, C2, D2) = "ABCXYZ"
      
      # constraints for the solver
      exprs = [
        # total number of seats is 40
        f("{L1} + {C1} + {D1} == 40"),
        f("{L2} + {C2} + {D2} == 40"),
      
        # before: Lab had an overall majority, with Con second
        f("{L1} > {C1} + {D1}"),
        f("{C1} > {D1}"),
      
        # after: Con was first, Lab second, Dem third
        f("{C2} > {L2} > {D2}"),
      
        # both Con and Dem increased their numbers of seats
        f("{C2} > {C1}"),
        f("{D2} > {D1}"),
      
        # after: Con formed a coalition with Dem to get a majority
        f("not ({C2} > {L2} + {D2})"),
        f("{C2} + {D2} > {L2}"),
      
        # each party had a 2-digit percentage change in its number of seats
        f("abs(change({L1}, {L2})) in d2"),
        f("abs(change({C1}, {C2})) in d2"),
        f("abs(change({D1}, {D2})) in d2o"),  # Dem's change is odd
      ]
      
      # construct the solver
      p = SubstitutedExpression(
        exprs,
        base=41, # each value is 0 .. 40
        distinct="",
        env=dict(change=change, d2=d2, d2o=d2o),
        answer=f("({L1}, {C1}, {D1}, {L2}, {C2}, {D2})"),
      )
      
      # solve the puzzle
      for (L1, C1, D1, L2, C2, D2) in p.answers(verbose=0):
        # output solution
        printf("before (seats)   -> Lab  {L1:2d},  Con  {C1:2d},  Dem  {D1:2d}")
        printf("after  (seats)   -> Lab  {L2:2d},  Con  {C2:2d},  Dem  {D2:2d}")
        (cL, cC, cD) = (L2 - L1, C2 - C1, D2 - D1)
        printf("change (seats)   -> Lab {cL:+3d},  Con {cC:+3d},  Dem {cD:+3d}")
        (cL, cC, cD) = (change(L1, L2), change(C1, C2), change(D1, D2))
        printf("change (percent) -> Lab {cL:+d}%, Con {cC:+d}%, Dem {cD:+d}%")
        printf()
      

      Solution: The result of the recent election was: Conservatives = 19 seats; Labour = 11 seats; Democrats = 10 seats.

      Before the election the seats were allocated thus:

      Labour = 22 seats
      Conservatives = 10 seats
      Democrats = 8 seats

      So the changes are:

      Labour = −11 seats (−50%)
      Conservatives = +9 seats (+90%)
      Democrats = +2 seats (+25%)

      The percentages don’t add up to 0% because they are the percentage change over the previous number of seats, not the percentage change in the overall share of seats. If we use this measure, then there are many solutions, but the changes will sum to 0%. (This can be seen by making the [[ change() ]] function divide by the total number of seats [[ 40 ]] instead of [[ before ]]).

      Like

    • Ruud's avatar

      Ruud 8:24 am on 22 October 2025 Permalink | Reply

      As brute force as possible:

      import peek
      import itertools
      
      
      def check(x0, x1, has_to_be_odd=False):
          perc = (abs(x0 - x1) / x0) * 100
          if perc % 1 or perc < 10 or perc >= 100:
              return False
          return not has_to_be_odd or perc % 2 == 1
      
      
      for l0, c0, d0 in itertools.product(range(1, 41), repeat=3):
          if l0 + c0 + d0 == 40 and l0 > c0 + d0 and l0 > c0 > d0:
              for l1, c1, d1 in itertools.product(range(1, 41), repeat=3):
                  if l1 + c1 + d1 == 40 and c1 < l1 + d1 and c1 > l1 > d1 and c1 + d1 > 20 and c1 > c0 and d1 > d0:
                      if check(l0, l1) and check(c0, c1) and check(d0, d1, has_to_be_odd=True):
                          peek(l0, l1, c0, c1, d0, d1, l1 - l0, c1 - c0, d1 - d0)
      

      Like

    • Frits's avatar

      Frits 4:35 pm on 22 October 2025 Permalink | Reply

      # check for a two-figure number percentage change
      def check(old, new):
        d, r = divmod(100 * abs(new - old), old)
        if r or not (9 < d < 100): return False
        return d
            
      M = 40
      parties = "Labour Conservatives Democrats".split(" ")
      # Labour had a majority, Democrats had at least 2 seats (two-figure %)
      for l1 in range(M // 2 + 1, M - 4):
        for c1 in range((M - l1) // 2 + 1, M - l1 - 1):
          d1 = M - l1 - c1
          # up to 99 percent increase   
          for d2 in range(d1 + 1, min(2 * d1, M // 3)):
            if not (perc := check(d1, d2)): continue
            # the Democrats' percentage increase was an odd number
            if not (perc % 2): continue 
            for c2 in range(max(d1 + 1, (M - d2) // 2 + 1), M // 2 + 1):
              if not check(c1, c2): continue
              l2 = M - d2 - c2
              if l2 <= d2 or not check(l1, l2): continue
              print(list(zip(parties, [l2 - l1, c2 - c1, d2 - d1])))
      

      Like

  • Unknown's avatar

    Jim Randell 7:58 am on 19 October 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 3291: Top of the Pops 

    From The Sunday Times, 19th October 2025 [link] [link]

    George and Martha are keen pop music fans. They recently followed the progress of one record in the charts and noticed that it was in the Top Ten for three weeks in three different positions, the third week’s position being the highest. In practice, a record never zigzags; it reaches a peak and then drops. For example: 5,4,9 or 5,6,9 are possible but 2,5,3 is not.

    “That’s interesting!”, commented Martha. “If you add the three positions, you get the day of the month when my father was born; and if you multiply them, you get a number giving the month and last two digits of that year”. “Furthermore”, added George “two of the positions also indicate the last two digits of that year”.

    What were the three positions in chronological order, and what was the date of Martha’s father’s birth?

    [teaser3291]

     
    • Jim Randell's avatar

      Jim Randell 8:08 am on 19 October 2025 Permalink | Reply

      The position in the third week is the highest in the chart (i.e. the smallest number), and so the previous position must be lower (i.e. a larger number), and as the record does not “zig-zag” (i.e. go down the chart, and then back up the chart), the first position must be the lowest of the three (i.e. the largest number). So if the numbers are p1, p2, p3, we have:

      p1 > p2 > p3

      This Python program runs in 72ms. (Internal runtime is 126µs).

      from enigma import (irange, subsets, multiply, mdivmod, printf)
      
      # choose the three positions
      for ps in subsets(irange(10, 1, step=-1), size=3):
        # sum is D; product is MYY
        (d, (m, x, y)) = (sum(ps), mdivmod(multiply(ps), 10, 10))
        if m < 1: continue
        # the 2 digits of the year are also (different) positions
        if not (x != y and x in ps and y in ps): continue
      
        # output solution
        printf("{ps} -> {d}/{m}/{x}{y}")
      

      Solution: The positions were: 9, 5, 3. Martha’s father’s birth date is: 17/1/35.

      We have:

      9 + 5 + 3 = 17
      9 × 5 × 3 = 135

      Like

    • Frits's avatar

      Frits 9:14 am on 19 October 2025 Permalink | Reply

      Two solutions.

      from itertools import permutations
      from math import prod
      
      # disallow descending after ascending within sequence <s>
      def zigzag(s):
        asc = 0
        for x, y in zip(s, s[1:]):
          if y > x: 
            asc = 1
          else:
            if asc:
              return True
        return False
        
      assert not zigzag([5, 4, 9])  
      assert not zigzag([5, 6, 9])
      assert zigzag([2, 5, 3])         
      
      # a tenth position results in a year .0
      for p in permutations(range(1, 10), 3):
        # the third week's position being the highest
        if max(p) != p[-1]: continue
        # a record never zigzags
        if zigzag(p): continue
        # multiplying gives the month and last two digits of that year
        if (m := prod(p)) < 100: continue
        yy = m % 100
        if yy < 10 or yy % 10 == 0: continue
        # two of the positions also indicate the last two digits of that year
        if yy % 11 == 0: continue
        if any(x not in p for x in divmod(yy, 10)): continue
        print((f"three positions {p}, birthdate {sum(p)}/{m // 100}/{yy}"))
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:25 am on 19 October 2025 Permalink | Reply

        @Frits: In the charts a “higher” position is the lower numbered one. (So the “highest” position is number 1).

        Like

        • Frits's avatar

          Frits 9:33 am on 19 October 2025 Permalink | Reply

          @Jim, “5,6,9 is possible” doesn’t help if that is the case.

          Like

          • Jim Randell's avatar

            Jim Randell 10:00 am on 19 October 2025 Permalink | Reply

            @Frits: I think that is just to illustrate that a record doesn’t go down the charts and then back up.

            So (5, 4, 9) is viable (the record peaks at number 4 and then drops down the chart the following week). (5, 6, 9) is viable (the record peaks at 5 and then drops down the charts for the next 2 weeks). But (2, 5, 3) is not viable (the record drops down from 2 to 5, but then goes back up again to 3).

            Like

    • Frits's avatar

      Frits 9:26 am on 19 October 2025 Permalink | Reply

      The wording is a bit confusing to me. A high position doesn’t seem to mean a position close to the top of the chart/pops.

      “the third week’s position being the highest” and ” … 5,6,9 are possible”.

      Like

      • Brian Gladman's avatar

        Brian Gladman 11:54 am on 19 October 2025 Permalink | Reply

        I agree that the wording is poor. The word ‘zigzag’ could mean (hi, lo,hi) or (lo,hi,lo) but is then defined as only one of these. And it hen gives an example that directly conflicts with an earlier constraint. It would have been easier to say ‘a hit never rises after it has fallen’. It almost seems as if the author is trying to confuse us!

        Like

        • Jim Randell's avatar

          Jim Randell 1:45 pm on 19 October 2025 Permalink | Reply

          @Brian: There is an implicit “up” to enter the Top 10, so (lo, hi, lo) is actually (up, up, down), which is allowable (not a zig-zag). But (hi, lo, hi) is (up, down, up), which is not allowed (a zig-zag)

          I agree that the wording seems to be deliberately complicated, but I didn’t have any problem interpreting it.

          Liked by 1 person

    • Brian Gladman's avatar

      Brian Gladman 12:10 pm on 19 October 2025 Permalink | Reply

      I am not sure of the use of the words ‘in practice’ either since it conflicts with real world experience where ‘rising after falling’ is not unusual. Even in ‘teaserland’ it is surely better to stay somewhere near reality.

      Like

  • Unknown's avatar

    Jim Randell 8:14 am on 14 October 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2490: [Cans of paint] 

    From The Sunday Times, 13th June 2010 [link] [link]

    George and Martha supply paint to the art class. They have 1 litre cans of red, yellow and blue, and can make 2 litre cans of orange (red and yellow equally), purple (red and blue) and green (yellow and blue), and 3 litre cans of brown (red, yellow and blue).

    The class orders numbers of litres of all seven colours (totalling less than 500 litres), with the seven numbers forming an arithmetic progression. The lowest quantity is 1 litre; the highest can be shared equally among the class, each student getting a whole plural number of litres. George and Martha used equal quantities of blue and yellow making up the order.

    How much green paint was ordered?

    This puzzle was originally published with no title.

    [teaser2490]

     
    • Jim Randell's avatar

      Jim Randell 8:14 am on 14 October 2025 Permalink | Reply

      This Python program runs in 69ms. (Internal runtime is 363µs).

      from enigma import (irange, inf, first, is_composite, disjoint_cproduct, printf)
      
      # colours are:
      # N = browN, O = Orange, P = Purple, G = Green, R = Red, Y = Yellow, B = Blue
      
      # consider common difference in the arithmetic progression
      for d in irange(1, inf):
        # calculate the arithmetic progression of quantities
        qs = first(irange(1, inf, step=d), count=7)
        t = sum(qs)
        if not (t < 500): break
        # final term in the progression is composite
        if not is_composite(qs[-1]): continue
      
        # find terms divisible by 2 (for O, P, G), 3 (for N)
        (qs2, qs3) = (list(q for q in qs if q % k == 0) for k in [2, 3])
        if len(qs2) < 3 or len(qs3) < 1: continue
      
        # choose quantities for N (qs3); O, P, G (qs2); R, Y, B (qs)
        for (N, O, P, G, R, Y, B) in disjoint_cproduct([qs3, qs2, qs2, qs2, qs, qs, qs]):
          # calculate total volume of R, Y, B used in mixing the colours
          (O2, P2, G2, N3) = (O // 2, P // 2, G // 2, N // 3)
          (tR, tY, tB) = (R + O2 + P2 + N3, Y + O2 + G2 + N3, B + P2 + G2 + N3)
          if tB == tY:
            # output solution
            printf("tR={tR} tY={tY} tB={tB} [R={R} Y={Y} B={B} O={O} P={P} G={G} N={N}]")
      

      Solution: 58 litres of green paint was ordered (29 cans).

      The total amount of paint in the order was 406 litres, consisting of:

      red = 72 litres
      yellow = 167 litres
      blue = 167 litres
      total = 406 litres

      Which is mixed as follows:

      red = 1 litre (= 1 can)
      yellow = 77 litres (= 77 cans)
      blue = 115 litres (= 115 cans)
      orange = 96 litres (= 48 cans)
      purple = 20 litres (= 10 cans)
      green = 58 litres (= 29 cans)
      brown = 39 litres (= 13 cans)
      total = 406 litres (= 293 cans)

      or:

      red = 1 litre (= 1 can)
      yellow = 115 litres (= 115 cans)
      blue = 77 litres (= 77 cans)
      orange = 20 litres (= 10 cans)
      purple = 96 litres (= 48 cans)
      green = 58 litres (= 29 cans)
      brown = 39 litres (= 13 cans)
      total = 406 litres (= 293 cans)

      Giving the following arithmetic progression (with a common difference of 19):

      [1, 20, 39, 58, 77, 96, 115]

      And this is the only possible progression, as it must end with a composite number, have (at least) 3 even numbers and (at least) one number that is divisible by 3.

      The largest amount can be expressed as 115 = 5 × 23.

      So there are either 5 students in the class (each would receive 23 litres) or 23 students in the class (each would receive 5 litres) (which is probably more likely).

      Like

    • Frits's avatar

      Frits 10:09 am on 14 October 2025 Permalink | Reply

      My original program had variable, ABC, DEF, GHI, …
      Apparently the number of liters of red paint (r * YZ + 1) is not relevant for solving this teaser.
      I didn’t have to use variable “r”.

      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for dgt in range(0, 10):
        vs = set()
        if dgt > 2: vs.update('Y')
        if dgt > 6: vs.update('bgnopy')
        d2i[dgt] = vs
      
      # the alphametic puzzle
      # red:   ABC, yellow: DEF, blue: GHI, orange: JKL, purple: MNO
      # green: PQR, brown:  STU
      p = SubstitutedExpression(
        [ # difference in arithmetic expression YZ, tot = 21 * YZ + 7 < 500
          "YZ < 24",
          # the highest can be shared equally among the class
          "not is_prime(6 * {YZ} + 1)",
          # orange, purple and green
          "({o} * {YZ} + 1) % 2 == 0",
          "({p} * {YZ} + 1) % 2 == 0",
          "({g} * {YZ} + 1) % 2 == 0",
          # and brow(n)
          "({n} * {YZ} + 1) % 3 == 0",
          
          # equal quantities of blue and yellow making up the order
          #before "2 * {DEF} + {MNO} == 2 * {GHI} + {JKL}",
          "2 * {y} + {p} == 2 * {b} + {o}",
        ],
        answer="g * YZ + 1",
        symbols="YZbgnopy",
        d2i=d2i,
        distinct=("bgnopy"),
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      print("answer:", ' or '.join(set(str(x) for x in p.answers())), "liters")
      

      Like

  • Unknown's avatar

    Jim Randell 10:37 am on 7 October 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2510: [Extension number] 

    From The Sunday Times, 31st October 2010 [link] [link]

    Since we last met them, George has moved departments yet again and Martha has forgotten the extension number.

    “No problem”, said the operator.

    “If you write down the number itself, the average of its four digits, the sum of its four digits, and the cube root of the difference between the number and its reverse (which is smaller), then you will write no digit more than once”.

    What is George’s new extension number?

    This puzzle was originally published with no title.

    [teaser2510]

     
    • Jim Randell's avatar

      Jim Randell 10:37 am on 7 October 2025 Permalink | Reply

      Although not explicitly stated, I assumed both the average (mean) of the digits and the cube root are integers.

      You get the same answer to the puzzle if you do consider fractional averages, written either in fraction form (1/4, 1/2, 3/4) or in decimal (.25, .5, .75).

      The following Python program runs in 65ms. (Internal runtime is 1.9ms).

      from enigma import (irange, subsets, multiset, nsplit, nconcat, is_cube, rev, join, printf)
      
      # choose 4 (distinct) digits for the phone number
      for ds in subsets(irange(0, 9), size=4):
        # accumulate written digits
        m = multiset.from_seq(ds)
      
        # sum of the digits in the number
        t = sum(ds)
        m.update_from_seq(nsplit(t))
        if m.is_duplicate(): continue
      
        # average (= mean) of the digits in the number
        (a, f) = divmod(t, 4)
        if f > 0: continue
        m.add(a)
        if m.is_duplicate(): continue
      
        # choose an ordering for the digits in the number
        for ns in subsets(ds, size=len, select='P'):
          if ns[-1] > ns[0]: continue  # first digit > last digit
          n = nconcat(ns)
          r = nconcat(rev(ns))
          # is the difference a positive cube?
          c = is_cube(n - r)
          if not c: continue
          if m.combine(nsplit(c)).is_duplicate(): continue
      
          # output solution
          printf("number={ns} [sum={t} avg={a} cbrt={c}]", ns=join(ns))
      

      Solution: The number is: 8147.

      The sum of the digits is: 20.

      The average (mean) of the digits is: 5.

      And the difference between the number and its reverse is: 8147 − 7418 = 729 = 9³.

      So we would write the digits: “8147; 20; 5; 9”.

      Like

      • Jim Randell's avatar

        Jim Randell 5:04 pm on 7 October 2025 Permalink | Reply

        Or even faster with some analysis:

        If the phone number is abcd (where a > d, then we have:

        n^3 = 1000(a – d) + 100(b – c) + 10(c – b) + (d – a)

        n^3 = 999(a – d) + 90(b – c)

        So n^3 is a multiple of 9, and so n is a multiple of 3.

        This Python program has an internal runtime of 277µs.

        
        from enigma import (irange, cb, group, unpack, subsets, div, nsplit, printf)
        
        # possible cubes
        cubes = dict((cb(n), nsplit(n)) for n in irange(3, 21, step=3))
        
        # map 90 * (b - c) values to possible (b, c) pairs
        fn = unpack(lambda b, c: 90 * (b - c))
        g = group(subsets(irange(0, 9), size=2, select='P'), by=fn)
        
        # choose (a, d) values (a > d)
        for (d, a) in subsets(irange(0, 9), size=2):
          x = 999 * (a - d)
          # consider possible cubes
          for (n3, n) in cubes.items():
            # find (a, b) values
            for (b, c) in g.get(n3 - x, ()):
              ds = [a, b, c, d]
              ds.extend(n)
              if len(set(ds)) != len(ds): continue
              t = a + b + c + d
              m = div(t, 4)
              if m is None: continue
              ds.append(m)
              ds.extend(nsplit(t))
              if len(set(ds)) != len(ds): continue
        
              # output solution
              printf("{a}{b}{c}{d} [cube={n3} total={t} avg={m}]")
        

        Like

    • Ruud's avatar

      Ruud 4:30 pm on 7 October 2025 Permalink | Reply

      import istr
      
      cube_root = {istr(i**3): istr(i) for i in range(22)}
      
      for i in istr.range(1000, 10000):
          if sum(i).is_divisible_by(4) and (diff := i - i[::-1]) in cube_root.keys() and (i | sum(i) / 4 | sum(i) | cube_root.get(diff, "**")).all_distinct():
              print(i)
      

      Like

    • Frits's avatar

      Frits 7:17 am on 8 October 2025 Permalink | Reply

      # n^3 = 1000(a - d) + 100(b - c) + 10(c - b) + (d - a)
      # n^3 = 999(a - d) + 90(b - c)
      # n^3 = 3^3.(37.(a - d) + 10.(b - c)/3)
      
      cubes = [i**3 for i in range(10)] 
      # dictionary of cubes per cube unit digit
      dct = {cubes[i] % 10: cubes[i] for i in range(10)}
      
      for a_d in range(1, 10):
        # 37.a_d  + delta should be a cube (delta in {-30, -20, -10, 10, 20, 30})
        m37 = a_d * 37
        delta = dct[m37 % 10] - m37
        if delta not in [-30, -20, -10, 10, 20, 30]: continue
        b_c = 3 * delta // 10
        # a + b + c + d = 4.avg or 2.d + 2.c + (a - d) + (b - c) = 4.avg
        if (a_d + b_c) % 2: continue 
        
        cuberoot2 = round((m37 + delta)**(1/3))
        cuberoot = 3 * cuberoot2
        ns = set(int(x) for x in str(cuberoot))
        
        for a in range(a_d, 10):
          d = a - a_d
          if len(ns_ := ns | {a, d}) != 3 + (cuberoot > 9): continue
          for b in range(b_c if b_c > 0 else 0, 10 if b_c > 0 else 10 + b_c):
            c = b - b_c
            if not ns_.isdisjoint({b, c}): continue
            avg, r = divmod(sm := a + b + c + d, 4)
            if r: continue
            nums = ns_ | {b, c, avg} | set(int(x) for x in str(sm))
            if len(nums) != 6 + len(ns) + (sm > 9): continue
            print("answer:", 1000 * a + 100 * b + 10 * c + d)
        

      Like

  • Unknown's avatar

    Jim Randell 11:06 am on 30 September 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2482: [Running up that hill] 

    From The Sunday Times, 18th April 2010 [link]

    Jan went up the hill and down, and up and down again, running each stretch at a steady speed and taking a whole number of minutes for each of the four stretches (rather more going up!). John timed her over the first two-thirds of the overall course, Mark timed her for the middle two-thirds, and Philip over the final two-thirds. All three times were the same whole number of minutes. The time taken on one stretch was a two-figure number of minutes whose two digits, when reversed, gave the time taken overall for all four.

    What, in order, were the times taken on each of the four stretches?

    This puzzle was originally published with no title.

    [teaser2482]

     
    • Jim Randell's avatar

      Jim Randell 11:06 am on 30 September 2025 Permalink | Reply

      If we suppose the times taken on each of the 4 sections are A, B, C, D.

      The for the timings to be the same whole number value, we have:

      t = A + B + 2C/3
      t = A/3 + B + C + D/3
      t = 2B/3 + C + D

      From which we see that each of A, B, C, D (and hence the total time T = A + B + C + D) must be multiples of 3.

      This Python program runs in 75ms. (Internal runtime is 5.8ms).

      from enigma import (irange, decompose, all_same, nrev, printf)
      
      # decompose T/3 into C/3, B/3, C/3, D/3
      for (b, d, a, c) in decompose(irange(4, 32), 4, increasing=1, sep=0):
        (A, B, C, D) = (3*a, 3*b, 3*c, 3*d)
        # check times are the same
        if not all_same(A + B + 2*c, a + B + C + d, 2*b + C + D): continue
        # calculate total time
        T = A + B + C + D
        X = nrev(T)
        if not (X > 10 and X in {A, B, C, D}): continue
        printf("A={A} B={B} C={C} D={D} -> T={T}")
      

      Solution: The times taken were: A = 21 min; B = 9 min; C = 27 min; D = 15 min.

      And the total time is 72 min.

      Like

  • Unknown's avatar

    Jim Randell 1:39 pm on 2 September 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2488: [Birthdays] 

    From The Sunday Times, 30th May 2010 [link] [link]

    Each of George’s and Martha’s five great-grandchildren celebrates a single-digit birthday today. “If you add their ages and reverse the digits of the total”, Martha said, “you get a two-digit number divisible by Andrew’s age and by none of the others'”. “That pattern continues”, George added. “Eliminate Andrew and the same is true of the other four, giving a number divisible by Brian’s age and none of the other three. If you eliminate Brian, the same is true for the other three with Colin’s age. Eliminating Colin, this works for the other two with David’s age”.

    How old are the five children?

    This puzzle was originally published with no title.

    [teaser2488]

     
    • Jim Randell's avatar

      Jim Randell 1:41 pm on 2 September 2025 Permalink | Reply

      I often forget about the [[ choose() ]] function in the enigma.py library (see: Tantalizer 482), but this is a good chance to use it. And this gives us a nice compact program.

      The ages must all be different, as the first reversed sum formed must be divisible by A, but none of the others. Hence A is different from all the others. Similarly the second reversed sum is divisible by B, but not C, D, E. Hence B is different from all the others. And so on. So we can use the [[ distinct=1 ]] parameter to the [[ choose() ]] function.

      It wasn’t completely clear if the “2-digit” condition was meant to apply to all the reversed sums, or just the first. I applied it to all of them, although it doesn’t change the answer if this condition is ignored completely.

      This Python program runs in 62ms. (Internal runtime is 326µs).

      from enigma import (irange, choose, nrev, printf)
      
      # check a set of numbers
      def fn(*ns):
        # reverse the sum
        r = nrev(sum(ns))
        # must be a 2 digit number, divisible by just the last number
        return (9 < r < 100 and r % ns[-1] == 0 and all(r % n > 0 for n in ns[:-1]))
      
      # choose 5 single digit ages
      for (E, D, C, B, A) in choose(irange(1, 9), [None, fn, fn, fn, fn], distinct=1):
        printf("A={A} B={B} C={C} D={D} E={E}")
      

      Solution: The ages are: Andrew = 2; Brian = 8; Colin = 3; David = 7; E = 5.

      We don’t know the name of the fifth child, but it may well start with an E.

      Summing the five ages and reversing gives:

      2 + 8 + 3 + 7 + 5 = 25 → 52

      And 52 is divisible by 2, but not 8, 3, 7, 5.

      With the 4 ages B – E:

      8 + 3 + 7 + 5 = 23 → 32

      And 32 is divisible by 8, but not 3, 7, 5.

      With the 3 ages C – E:

      3 + 7 + 5 = 15 → 51

      And 51 is divisible by 3, but not 7, 5.

      With the 2 ages D – E:

      7 + 5 = 12 → 21

      And 21 is divisible by 7, but not 5.

      Like

    • Frits's avatar

      Frits 3:31 pm on 2 September 2025 Permalink | Reply

      from enigma import SubstitutedExpression
      
      def check(*s):
        if (rsum := int(str(sum(s))[::-1])) % s[-1] or not (9 < rsum < 100): 
          return False
        return all(rsum % x for x in s[:-1])
        
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "check(E, D)",
          "check(E, D, C)",
          "check(E, D, C, B)",
          "check(E, D, C, B, A)",
        ],
        answer="(A, B, C, D, E)",
        env=dict(check=check),
        d2i=dict([(1, "BCDE")]),
        digits=range(1, 10),
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"{', '.join((x + ': ' + y 
                for x, y in zip('ABCDE', [str(a) for a in ans])))}")
      

      Like

    • GeoffR's avatar

      GeoffR 8:37 pm on 2 September 2025 Permalink | Reply

      from itertools import permutations
      
      for a, b, c, d, e in permutations(range(1, 10), 5):
        fg = a + b + c + d + e
        f, g = fg // 10, fg % 10
        if not f > 0 and g > 0:
          continue
        gf = 10*g + f
        if gf % a == 0 and all (gf % x != 0 for x in (b, c, d, e)):
              
          # Ekiminate Andrew
          hi = b + c + d + e
          h, i = hi // 10, hi % 10
          if not h > 0 and i > 0:
            continue
          ih = 10*i + h
              
          # Eliminate Brian
          if ih % b == 0 and all( ih % x != 0 for x in (c, d, e)):
            jk = c + d + e
            j, k = jk // 10, jk % 10
            if not j > 0 and k > 0:
              continue
            kj = 10*k + j
              
            # Eliminate Colin
            if kj % c == 0 and all(kj % x != 0 for x in (d, e)):
              Lm = d + e
              L, m = Lm // 10, Lm % 10
              if not L > 0 and m > 0:
                continue
              mL = 10*m + L
      
              # Eliminate David
              if mL % d == 0 and mL % e != 0: 
                print(f" A = {a}, B = {b}, C = {c}, D = {d}, E = {e}")
      
      # A = 2, B = 8, C = 3, D = 7, E = 5 
      
      

      Like

    • Ruud's avatar

      Ruud 7:57 am on 4 September 2025 Permalink | Reply

      import istr
      
      def check(ages, i):
          return [sum(ages[i:])[::-1].is_divisible_by(age) for age in ages[i:]] == [True] + (4 - i) * [False]
      
      for ages in istr.permutations("123456789", 5):
          if all(check(ages, i) for i in range(4)):
              print(ages)
      

      Like

    • Ruud's avatar

      Ruud 2:56 pm on 4 September 2025 Permalink | Reply

      As a one-liner:

      import istr
      
      print(
          [
              ages
              for ages in istr.permutations("123456789", 5)
              if all(all(sum(ages[i:])[::-1].is_divisible_by(ages[j]) == (j == i) for j in range(i, 5)) for i in range(4))
          ]
      )
      

      Like

  • Unknown's avatar

    Jim Randell 7:45 am on 19 August 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2468: [Entry codes] 

    From The Sunday Times, 10th January 2010 [link]

    The entry code to George and Martha’s block of flats is a four-digit number consisting of four different non-zero digits. Following a breach of security, management has changed the code. “I can see what they have done”, Martha says after some protracted calculations. “They have taken a multiple of the old code to give a five-digit number, added up those five digits, then squared this total to give the new code”. “Being of simple mind”, replies George, “I’d have said that they’ve simply reversed the old code”.

    What was the old code?

    This puzzle was originally published with no title.

    [teaser2468]

     
    • Jim Randell's avatar

      Jim Randell 7:45 am on 19 August 2025 Permalink | Reply

      From George’s assessment we can see that the new code consists of 4 different non-zero digits.

      From Martha’s assessment we can see that the new code must be a square number.

      So we can look at 4-digit squares, consisting of 4 different non-zero digits, for the new number, reverse it, to get the old number, and then see if we can apply Martha’s algorithm to the old number to get the new number.

      This Python program runs in 69ms. (Internal runtime is 658µs).

      from enigma import (irange, sq, nsplit, nrev, dsum, printf)
      
      # Martha's algorithm
      def martha(n):
        # consider 5-digit multiples of n
        for m in irange.round(10000, 99999, step=n, rnd='I'):
          # find the square of the digit sum
          yield sq(dsum(m))
      
      # the new code is a 4-digit square with no repeated digits
      for i in irange(36, 99):
        new = sq(i)
        ds = nsplit(new)
        if 0 in ds or len(set(ds)) != len(ds): continue
      
        # the old code is the reverse of the new
        old = nrev(new)
      
        # can we apply Martha's algorithm?
        if new in martha(old):
          # output solution
          printf("new = {new}; old = {old}")
      

      Solution: The old code was: 6921.

      We can apply Martha’s technique to this number as follows:

      6921 × 13 = 89973
      8+9+9+7+3 = 36
      36^2 = 1296

      or:

      6921 × 14 = 96894
      9+6+8+9+4 = 36
      36^2 = 1296

      Like

      • Jim Randell's avatar

        Jim Randell 7:20 am on 21 August 2025 Permalink | Reply

        Using the [[ SubstitutedExpression ]] solver from the enigma.py library:

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # suppose the old code was: ABCD
        # and the new code is: DCBA
        # and the multiple in Martha's algorithm is: XY
        --distinct="ABCD"
        --invalid="0,ABCD"
        
        # apply Martha's algorithm:
        # the multiple:
        --macro="@M = (XY * ABCD)"
        # is a 5-digit number:
        "9999 < @M < 100000"
        # and the square of the digit sum is the new code
        "sq(dsum(@M)) = DCBA"
        
        --answer="ABCD"
        

        Like

    • Frits's avatar

      Frits 12:13 pm on 19 August 2025 Permalink | Reply

      # find a multiple of <n> that has 5 digits and a digit sum of <t>
      def multiple(n, t):
        # makes sure of a 5-digit multiple with first digit high enough
        m = 9999 if t - 37 <= 0 else (t - 36) * 11111 - 1
        # reverse looping in order to find a solution sooner 
        for k in range(99999 // n, m // n, -1):
          if sum(int(x) for x in str(k * n)) == t:
            return True
        return False    
      
      # suitable old codes and squares
      sqs = [(int(i2[::-1]), i) for i in range(32, 46) 
              if '0' not in (i2 := str(i * i)) and len(set(i2)) == 4]
      
      for n, t in sqs:
        if multiple(n, t):
          print("answer:", n)
      

      Like

    • GeoffR's avatar

      GeoffR 2:42 pm on 20 August 2025 Permalink | Reply

      
      from itertools import permutations
      from enigma import nsplit
      
      digits = set('123456789')
      
      from math import isqrt
      def is_sq(x):
         return isqrt(x) ** 2 == x 
      
      for n in permutations (digits, 4):
          a, b, c, d = n
          abcd = int(a + b + c + d)
          for m in range(2, 80):  # UB = 98765/1234 = 80
              dig5 = m * abcd
              if dig5 < 10000:continue
              if dig5 > 99999:continue
              # find digits of 5 digit number
              d1, d2, d3, d4, d5 = nsplit(dig5)
              if 0 in (d1, d2, d3, d4, d5):continue
              # find new code
              new_code = (d1 + d2 + d3 + d4 + d5) ** 2
              # new code is reverse  of old code
              if str(new_code) == str(abcd) [::-1]:
                  print(f"Old code was {abcd}.")
                     
      # Old code was 6921.
      
      

      Like

    • Ruud's avatar

      Ruud 7:03 am on 21 August 2025 Permalink | Reply

      import istr
      
      istr.int_format("04")
      for old in istr.concat(istr.permutations(istr.digits("1-9"), 4)):
              for n in range(2,10000):
                  if len(old_n:=old * n) == 5:
                      if sum(old_n) ** 2 == old[::-1]:
                          print(old, n)
                  else:
                      if len(old_n) > 5:
                          break

      Like

    • Ruud's avatar

      Ruud 7:20 am on 21 August 2025 Permalink | Reply

      As a one liner:
      import istr
      
      print(
          {
              old
              for old in istr.concat(istr.permutations(istr.digits("1-9"), 4))
              for old_n in istr.range(int(2 * old), 100000, old)
              if len(old_n) == 5 and sum(old_n) ** 2 == old[::-1]
          }
      )
      

      Like

    • GeoffR's avatar

      GeoffR 10:37 am on 21 August 2025 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9: a; var 1..9:b; var 1..9: c; var 1..9: d;
      var 1..9: d1; var 1..9:d2; var 1..9: d3; var 1..9: d4; var 1..9: d5;
      var 2..80:m;
      
      constraint all_different([a, b, c, d]);
      
      var 10000..99999: dig5 = 10000*d1 + 1000*d2 + 100*d3 + 10*d4 + d5;
      var 1000..9999: old_code = 1000*a + 100*b + 10*c + d;
      var 1000..9999: new_code = 1000*d + 100*c + 10*b + a;
      
      % New code
      constraint (d1 + d2 + d3 + d4 + d5) * (d1 + d2 + d3 + d4 + d5) == new_code;
      constraint m * old_code == dig5;
      
      solve satisfy;
      
      output ["Old Code = " ++ show(old_code) ++ "\n"
      ++ "New Code = " ++ show(new_code) ++ "\n"
      ++ "Multiplier = " ++ show(m) ++ "\n" 
      ++ "5-digit number = " ++ show(dig5)
      ];
      
      % Old Code = 6921
      % New Code = 1296
      % Multiplier = 13
      % 5-digit number = 89973
      % ----------
      % Old Code = 6921
      % New Code = 1296
      % Multiplier = 14
      % 5-digit number = 96894
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 8:07 am on 1 August 2025 Permalink | Reply
    Tags: by: Danny Roth,   

    Teaser 2472: [Running from a train] 

    From The Sunday Times, 7th February 2010 [link]

    George and Martha stood on the platform as the train approached at a steady 20 metres per second. Both platform and train were 100 metres long. The train was programmed to decelerate steadily as it reached the start of the platform. A boy slipped off the platform onto the track. He could run at 2 metres per second. “Run to your left — you’re nearer that end of the platform”, shouted Martha. “No, run to the other end, away from the train”, said George. Either action would just avert disaster.

    How far ahead of the train did he fall?

    This puzzle was originally published with no title.

    Although this puzzle can be solved, the published solution was incorrect.

    [teaser2472]

     
    • Jim Randell's avatar

      Jim Randell 8:08 am on 1 August 2025 Permalink | Reply

      I am assuming the train does not start any kind of emergency braking procedure, and so begins decelerating at the start of the platform, and ends (with the train stationary) aligned with the end of the platform.

      In the first scenario the boy runs towards the train (which hasn’t begun to slow down yet). If he reaches the start of the platform (and dodges around it) after x seconds, just as the train arrives at the start of the platform, then he has run a distance of 2x metres, and the train has travelled 20x metres.

      In the second scenario, the boy runs away from the train. After x seconds the train arrives at the start of the platform and starts to decelerate, such that its speed will be 0 when it arrives at the end of the platform (which is 100 m long).

      The train starts at 20 m/s and after 100 m it reaches 0 m/s, the deceleration of the train is thus:

      [v² = u² + 2as]
      0 = (20)^2 + 2a . 100

      a = −2 [m/s^2]

      And the velocity of the train after time t from the start of the platform is:

      [v = u + at]
      v = 20 − 2t

      So it takes the train 10 s to decelerate to 0 m/s.

      We are interested in the time it takes for the train to decelerate to 2 m/s (after which the boy will outpace it, and get to the end of the platform before the train).

      This happens at:

      v = 2

      t = 9

      i.e. 9 seconds after the train reaches the start of the platform.

      The distance along the platform travelled by the train in these 9 seconds is:

      [s = ut + (1/2)at²]
      d = 20 . 9 + (1/2)(−2)(9^2) = 99 [m]

      So there is 1 m of platform left.

      And the boy has to run a distance of (99 − 2x) m in the time it takes the train to decelerate to 2 m/s.

      The time taken for the boy to do this is:

      t = (99 − 2x) / 2

      And the time taken for the train to arrive at the same point is:

      t = x + 9

      If disaster is just averted, these times are equal:

      (99 − 2x) / 2 = (x + 9)

      x = 81/4 = 20.25

      So the boy falls a distance 2x from the start of the platform = 40.5 m.

      And at that time the train is 20x away from the start of the platform = 405 m.

      Hence:

      Solution: The boy falls at a distance of 445.5 m in front of the train.

      Here is a time/distance graph of the situation:

      The train enters from the top along the solid blue line (constant speed), and then as it passes the start of the platform it follows the dashed blue line (constant deceleration), until it stops at the end of the platform.

      The scenarios for the boy are indicated by the red lines.

      The upper red line is the path taken by the boy running toward the start of the platform (and the oncoming train), to arrive at the start at the same time as the train.

      And the lower red line is the path take by the boy running towards the end of the platform (away from the train).

      Zooming in on the final section we see that at 29.25 seconds the train catches up with the boy, as they are both going 2 m/s. The boy then outpaces the train to reach the end of the platform 0.5 seconds later, and the train finally stops at the end of the platform 0.5 seconds after that.

      Whichever direction the boy chooses to run in, he has to start as soon as he lands, and so he doesn’t have to listen to advice from either Martha or George.


      However, the published answer was 440 m.

      This gives us:

      20x + 2x = 440

      x = 20

      Which means the boy fell 40 m from the start of the platform.

      If we plot the distance/time graph associated with these values we see that although the train and the boy coincide at the end of the platform when the train is stationary, they also coincide 4 m before the end of the platform, when the boy is hit by the moving train. So this does not provide a viable solution to the puzzle.

      Looking closer at the end we see at 28 seconds the train hits the boy, 2 seconds before they would both have arrived at the end of the platform.

      At the point of impact the train would be travelling at 4 m/s (≈ 9 mph).

      If the puzzle had been set so that, instead of trying to escape from the train, the boy was just trying to race the train to get to one end of the platform before the train does, then the published solution would work, and the boy and train would arrive simultaneously regardless of which end of the platform the boy chose to run to.

      I couldn’t find any correction to the published solution in The Sunday Times digital archive.

      Like

  • Unknown's avatar

    Jim Randell 7:10 am on 8 June 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 3272: Festive visitors 

    From The Sunday Times, 8th June 2025 [link] [link]

    George and Martha’s five daughters left home some years ago but have all moved into the same road, Square Avenue, which has 25 normally numbered houses. At Christmas last year, the parents drove to the road to visit but, with age rolling on, George’s memory was beginning to fail him. “Can’t remember the house numbers!” he moaned. “Quite easy!” retorted Martha, “if you remember three things. If you take the house numbers of Andrea, Bertha and Caroline, square each of them and add up the three squares, you get the square of Dorothy’s house number. Furthermore, the average of Andrea’s and Dorothy’s house numbers equals the average of Bertha’s and Caroline’s house numbers. Finally, Elizabeth’s house number is the average of the other four, and could not be smaller”.

    Which five houses got a knock on the door?

    [teaser3272]

     
    • Jim Randell's avatar

      Jim Randell 7:15 am on 8 June 2025 Permalink | Reply

      Here’s a quick solution using the [[ SubstitutedExpression ]] solver from the enigma.py library to implement the requirements directly. (More efficient formulations are available).

      The following Python program executes in 129ms. (Internal runtime is 65ms).

      from enigma import (SubstitutedExpression, Accumulator, irange, printf)
      
      p = SubstitutedExpression(
        [
          # "sum of the squares of A, B, C equals square of D"
          "sq(A) + sq(B) + sq(C) == sq(D)",
          # "mean of A and D equals mean of B and C"
          "A + D == B + C",
          # "E's number is the mean of the other four"
          "A + B + C + D == 4 * E",
        ],
        # allow house numbers from 1-25
        base=26,
        digits=irange(1, 25),
        # return the house numbers
        answer="(A, B, C, D, E)",
        template=""
      )
      
      # minimise the value of E
      r = Accumulator(fn=min, collect=1)
      
      # find candidate solutions
      for (A, B, C, D, E) in p.answers():
        r.accumulate_data(E, (A, B, C, D, E))
      
      # output solution
      printf()
      printf("min(E) = {r.value}")
      for (A, B, C, D, E) in r.data:
        printf("-> A={A} B={B} C={C} D={D} E={E}")
      

      Solution: The house numbers are: 3, 4, 8, 12, 13.

      There are four candidate solutions:

      A=3 B=4 C=12 D=13 E=8
      A=3 B=12 C=4 D=13 E=8
      A=4 B=6 C=12 D=14 E=9
      A=4 B=12 C=6 D=14 E=9

      The minimum value for E is 8, and so the other numbers are: A = 3, (B, C) = (4, 12) (in some order), D = 13.

      Like

    • Ruud's avatar

      Ruud 4:00 pm on 8 June 2025 Permalink | Reply

      import itertools
      
      min_e = 25
      for a, b, c, d in itertools.permutations(range(1, 26), 4):
          if a * a + b * b + c * c == d * d and a + d == b + c and (e := (a + b + c + d) / 4) % 1 == 0:
              if e < min_e:
                  min_numbers = set()
                  min_e = e
              if e <= min_e:
                  min_numbers.add(tuple(sorted((a, b, c, d, int(e)))))
      print(*min_numbers)
      

      Like

    • Ruud's avatar

      Ruud 7:04 am on 9 June 2025 Permalink | Reply

      One liner:

      import itertools
      
      print(
          *next(
              solutions
              for e in range(1, 26)
              if (
                  solutions := [
                      (a, b, c, d, e)
                      for a, b, c, d in itertools.permutations(range(1, 26), 4)
                      if a * a + b * b + c * c == d * d and a + d == b + c and e == (a + b + c + d) / 4
                  ]
              )
          )
      )
      

      Like

  • Unknown's avatar

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

    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: by: Danny Roth   

    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 5:07 pm on 26 February 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2548: Planetary line 

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

    George and Martha are studying a far-off galaxy consisting of 10 planets circling a sun clockwise, all in the same plane. The planets are Unimus (taking one year to circle the sun), Dubious (two years), Tertius (three years), and so on up to Decimus (10 years).

    At one instant today the sun and all 10 planets were in one straight line. Martha said it will be ages before that happens again. “Don’t let’s be greedy,” said George. “How long must we wait until at least nine planets and the sun are in a straight line?”

    How long must they wait?

    In honour of the current planetary parade.

    [teaser2548]

     
    • Jim Randell's avatar

      Jim Randell 5:08 pm on 26 February 2025 Permalink | Reply

      Assuming the planets are in circular orbits, and they can be aligned on opposite sides of the sun.

      If we have a faster planet that makes a half orbit in period x, and a slower planet that makes a half orbit in period y, then, if they are initially in alignment, the time t taken for the fast planet to get one half orbit ahead of the slow planet is given by:

      t/x = t/y + 1

      t = x.y / |x − y|

      For for any collection of planets we can choose one of the planets, calculate the time taken for it to align with each of the other planets separately, and then calculate the LCM of these values to find the earliest time when they all align.

      This Python program runs in 68ms. (Internal runtime is 211µs).

      from enigma import (Rational, Accumulator, mlcm, mgcd, subsets, call, arg, printf)
      
      Q = Rational()
      
      # orbital periods of the planets
      orbits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      
      k = arg(9, 0, int)
      
      # find the earliest (half-orbit) alignment of the planets <ss>
      def align(ss):
        x = ss[0]
        qs = list(Q(x * y, 2 * abs(x - y)) for y in ss[1:])
        # calculate the LCM of the fractions
        n = call(mlcm, (q.numerator for q in qs))
        d = call(mgcd, (q.denominator for q in qs))
        return Q(n, d)
      
      # find minimal times
      r = Accumulator(fn=min, collect=1)
      
      # consider k sized subsets
      for ss in subsets(orbits, size=k):
        # calculate the alignment times for this set
        t = align(ss)
        printf("[{ss} -> {t} ({f})]", f=float(t))
        r.accumulate_data(t, ss)
      
      # output solution
      printf("min time = {r.value} ({f}) years", f=float(r.value))
      for ss in r.data:
        printf("-> {ss}")
      

      Solution: 9 planets and the sun will align after 180 years.

      If all the planets start at an angle of 0°, then in 180 years we have the following alignment:

      Planet 1: 0°
      Planet 2: 0°
      Planet 3: 0°
      Planet 4: 0°
      Planet 5: 0°
      Planet 6: 0°
      Planet 7: 257.14°
      Planet 8: 180°
      Planet 9: 0°
      Planet 10: 0°

      We see that planets 1, 2, 3, 4, 5, 6, 9, 10 are in their original positions, and planet 8 is on the opposite side of the sun (but still in a straight line with the other planets). Planet 7 is the only one that is off the line.

      After 1260 years all the planets will have completed a whole number of half-orbits (all except 8 have returned to their original positions, but 8 is on the opposite side of the sun), and after 2520 years (= LCM of 1 .. 10) all the planets have returned to their original positions and the cycle will start again. The cycle of alignments is as follows:

      0, 2520 years = all planets at 0°
      180, 540, 900, 1620, 1980, 2340 years = planets 1, 2, 3, 4, 5, 6, 9, 10 at 0°; planet 8 at 180°; planet 7 out of alignment
      360, 720, 1080, 1440, 1800, 2160 years = planets 1, 2, 3, 4, 5, 6, 8, 9, 10 at 0°; planet 7 out of alignment
      420, 2100 years = planets 1, 2, 3, 4, 5, 6, 7, 10 at 0°; planet 8 at 180°; planet 9 out of alignment
      630, 1890 years = planets 1, 2, 3, 5, 6, 7, 9, 10 at 0°; planet 4 at 180°; planet 8 out of alignment
      840, 1680 years = planets 1, 2, 3, 4, 5, 6, 7, 8, 10 at 0°; planet 9 out of alignment

      But we can have alignments other than at 0°/180°:

      For example the earliest alignment of the 5 planets 1, 3, 5, 7, 9 is after 78.75 years:

      They are aligned along 90°/270°.

      Like

  • Unknown's avatar

    Jim Randell 7:49 am on 21 January 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2511: [Medallions] 

    From The Sunday Times, 7th November 2010 [link] [link]

    I have a collection of silver medallions, each of which weighs an exact whole number of grams. If I take pairs of them in every possible combination, I can weigh out an almost complete sequence of 16 consecutive weights, with only two weights, 100 grams and 101 grams, missing from the sequence. Even so, I can still manage an unbroken sequence of 13 weights, with only one of them, a prime number of grams, duplicated.

    What, in ascending order, are the weights in grams of my medallions?

    This puzzle was originally published with no title.

    [teaser2511]

     
    • Jim Randell's avatar

      Jim Randell 7:50 am on 21 January 2025 Permalink | Reply

      I reused the code written to solve Enigma 1600, which is a similar puzzle.

      With 6 weights we can combine them in pairs to make C(6, 2) = 15 pairs, so to make exactly 14 values only one of them is repeated.

      So we can either make:

      [13 values], [not 100, 101], [102] = [87..102] \ {100, 101}
      [99], [not 100, 101], [13 values] = [99..114] \ {100, 101}

      This Python 3 program runs in 68ms. (Internal runtime is 1.6ms).

      from enigma import (irange, diff, C, div, multiset, subsets, is_prime, printf)
      
      # extend <ss> with <k> more values to make numbers in <ns>
      def pairs(ns, ss, k):
        # are we done?
        if k == 0 or not ns:
          if k == 0 and not ns:
            yield ss
        elif C(k, 2) + k * len(ss) >= len(ns):
          # add in the next smallest value
          for x in irange(ss[-1] + 1, max(ns) - ss[-1]):
            # and solve for the remaining numbers
            ns_ = diff(ns, set(x + y for y in ss))
            yield from pairs(ns_, ss + [x], k - 1)
      
      # find <k> values that can be used in pairs to make <ns>
      def solve(ns, k):
        # suppose the values start [0, a, ...]
        # then the pairs start [a, ...]
        m = ns[-1] - ns[0] + 5 - 2 * k
        for a in irange(1, m):
          # reduce the numbers so that they start at a
          d = div(ns[0] - a, 2)
          if d is None: continue
          vs = list(n - 2 * d for n in ns)
          # solve the reduced list
          for ss in pairs(vs[1:], [0, a], k - 2):
            # return (<values>, <pair sums>)
            ss = tuple(v + d for v in ss)
            ps = sorted(x + y for (x, y) in subsets(ss, size=2))
            yield (ss, ps)
      
      # consider a sequence of 16 consecutive values that includes 100, 101
      for i in [87, 99]:
        ns = diff(irange(i, i + 15), {100, 101})
        for (ss, ps) in solve(ns, 6):
          # now find what values we can make, and check duplicates
          (ds, xs) = multiset.from_seq(ps).differences(ns)
          if xs or ds.size() != 1: continue
          # the duplicate value is prime
          d = ds.peek()
          if not is_prime(d): continue
          # output solution
          printf("{ss} -> {ns}; duplicate = {d}")
      

      Solution: The weights of the medallions are: 43, 44, 45, 46, 49, 53 grams.

      Taken in pairs these make the values:

      87, 88, 89 (twice), 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 102

      Like

  • Unknown's avatar

    Jim Randell 6:58 am on 19 January 2025 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 3252: Family tree 

    From The Sunday Times, 19th January 2025 [link] [link]

    George and Martha have five daughters; they are, in order of arrival, Andrea [eldest], Bertha, Caroline, Dorothy and Elizabeth [youngest]. They now have ages that are all [different] prime numbers in the range 28 to 60. The average [of the numbers] is also a prime number, different from the other five.

    Each daughter has a son, Adam, Brian, Colin, David and Edward respectively. They are all mathematics students, and are studying how to calculate square roots without using a calculator. One of them was given a perfect square with three or four digits (none repeated) and told to work out the square root. This he did accurately, getting his mother’s age, but he noted that the sum of the digits of that perfect square was also prime.

    Which boy was it, and what is his mother’s age?

    [teaser3252]

     
    • Jim Randell's avatar

      Jim Randell 7:10 am on 19 January 2025 Permalink | Reply

      This Python program runs in 65ms. (Internal runtime is 141us).

      from enigma import (primes, subsets, iavg, nsplit, printf)
      
      # choose the five ages (youngest (E) to eldest (A))
      for ps in subsets(primes.between(28, 60), size=5):
        # the mean is a different prime
        m = iavg(ps)
        if m is None or m in ps or m not in primes: continue
      
        # one of the primes has a square with a prime digit sum
        for (t, p) in zip("EDCBA", ps):
          s = p * p
          ds = nsplit(s)
          if not (len(set(ds)) == len(ds) and sum(ds) in primes): continue
          # output solution
          printf("ages = {ps}, mean = {m} -> {t} = {p}, square = {s}")
      

      Solution: David did the calculation. His mother is 37.

      The ages are:

      Andrea = 59
      Bertha = 47
      Caroline = 41
      Dorothy = 37
      Elizabeth = 31
      → mean = 43

      And the square of Dorothy’s age is 37^2 = 1369, which has a digit sum of 19.

      Like

    • Frits's avatar

      Frits 9:50 am on 19 January 2025 Permalink | Reply

      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for dgt in range(0, 10):
        vs = set()
        if dgt < 2 or dgt > 5: vs.update('ACEGI')
        if dgt % 2 == 0 or dgt == 5: vs.update('BDFHJ')
        d2i[dgt] = vs
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "AB > 28",
          "is_prime(AB)",
          "CD > AB",
          "is_prime(CD)",
          "EF > CD",
          "is_prime(EF)",
          "GH > EF",
          "is_prime(GH)",
          "IJ > GH",
          "is_prime(IJ)",
          # the mean is a different prime
          "is_prime(mean := (AB + CD + EF + GH + IJ) / 5)",
          "mean not in {AB, CD, EF, GH, IJ}",
          
          # the sum of the digits of that perfect square was also prime
          "len(res := [(i, m) for i, m in enumerate([AB, CD, EF, GH, IJ]) \
               if len(set((s := str(m * m)))) == len(s) and \
                  is_prime(sum([int(x) for x in s]))] ) > 0",
        ],
        answer="res",
        d2i=d2i,
        distinct="",
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      boys = "Edward David Colin Brian Adam".split()
      # print answers
      for ans in p.answers():
        for a in ans:
          print(f"the boy was {boys[a[0]]} and his mother’s age was {a[1]}")
      

      Like

      • Frits's avatar

        Frits 10:19 am on 19 January 2025 Permalink | Reply

        @Jim, I just noticed that “is_prime(2.5)” yields “True”.
        Are you going to change the subroutine?

        Like

        • Jim Randell's avatar

          Jim Randell 10:32 am on 19 January 2025 Permalink | Reply

          @Frits: If you don’t know if you are passing a non-negative integer you can turn on argument validation with the [[ validate=1 ]] parameter to check it for you (a ValueError exception will be raised on invalid input).

          Like

    • ruudvanderham's avatar

      ruudvanderham 2:34 pm on 20 January 2025 Permalink | Reply

      import istr
      
      istr.repr_mode("int")
      primes = [n for n in range(28, 61) if istr.is_prime(n)]
      
      for ages in istr.combinations(primes, 5):
          if (average := int(sum(ages)) / 5) in primes and not (average in ages):
              for age, name in zip(ages, "Adam Brian Colin David Edward".split()):
                  if sum(i for i in age**2).is_prime() and (age**2).all_distinct():
                      print(f"Name boy={name} Mother's age={age} [Ages={ages} Average={int(average)}]")

      Like

  • Unknown's avatar

    Jim Randell 12:08 pm on 19 December 2024 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2601: Olympic economics 

    From The Sunday Times, 29th July 2012 [link] [link]

    George and Martha are organising a large sports meeting. Usually a gold medal (costing £36) is given to each winner, silver (£17) to each runner-up, and bronze (£11) to each third. In a minority of events, such as tennis and boxing, both losing semi-finalists get a bronze medal. However, George and Martha are going to economise by having third place play-offs in such events, thus reducing the medals needed by an odd number. George noticed that the total cost of the medals is now a four-figure palindrome, and Martha commented that the same would have been true under the previous system.

    How many events are there?

    [teaser2601]

     
    • Jim Randell's avatar

      Jim Randell 12:09 pm on 19 December 2024 Permalink | Reply

      If there are n events, under the new system each has a 1 gold, 1 silver and 1 bronze medal (assuming each event is for individual competitors). The cost of medals for each event is £36 + £17 + £11 = £64. So the total cost of medals is 64n, and we are told this is a 4-digit palindrome.

      This Python program looks for possible multiples of 64 that give a 4-digit palindrome, and then looks at adding an odd number of extra bronze medals (for less than half of the events), that also gives a 4-digit palindrome for the total cost.

      It runs in 67ms. (Internal runtime is 156µs).

      from enigma import (irange, inf, is_npalindrome, printf)
      
      # consider the number of events
      for n in irange(1, inf):
        # total cost of medals is a 4-digit palindrome
        t = 64 * n  #  (gold + silver + bronze) per event
        if t < 1000: continue
        if t > 9999: break
        if not is_npalindrome(t): continue
      
        # consider an odd number of extra bronze medals
        for x in irange(1, (n - 1) // 2, step=2):
          tx = t + 11 * x
          if tx > 9999: break
          if not is_npalindrome(tx): continue
      
          # output solution
          printf("{n} events -> cost = {t}; +{x} bronze -> cost = {tx}")
      

      Solution: There are 132 events.

      So the total cost of medals under the new system is £64×132 = £8448.

      There are two situations where the cost of additional bronze medals also gives a 4-digit palindromic total cost:

      If there are 51 extra bronze medals required under the old system, then the additional cost is £11×51 = £561, and the total cost would have been £9009.

      If there are 61 extra bronze medals required, then the additional cost is £11×61 = £671, and the total cost would have been £9119.

      Like

  • Unknown's avatar

    Jim Randell 8:10 am on 29 October 2024 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 2540: Extra time 

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

    George and Martha planned a holiday on the south coast. The second-class rail fare each way was a certain whole number of pounds per person and the nightly cost of an inexpensive double room, in pounds, was the same number but with digits reversed. They originally planned to stay 30 nights, but then increased that to 33. “So the total cost will go up by 10%”, said Martha. “No”, replied George, “it will go up by some other whole number percentage”.

    What is the nightly cost of a double room?

    [teaser2540]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 29 October 2024 Permalink | Reply

      I assumed that the rail fare does not have a trailing zero, so that the room rate does not have a leading zero.

      This Python program finds the first solution where the rail fare and room rate are less than £100.

      It runs in 80ms. (Internal runtime is 188µs).

      from enigma import (irange, nrev, div, printf)
      
      # consider possible rail fares
      for F in irange(1, 99):
        # disallow trailing zeros
        if F % 10 == 0: continue
      
        # the room rate is the reverse of this
        R = nrev(F)
      
        # cost for 30 nights and 33 nights
        t30 = 4*F + 30*R
        t33 = 4*F + 33*R
      
        # t33 is a k per cent increase on t30
        r = div(100 * t33, t30)
        if r is None: continue
      
        printf("F={F} R={R} r={r}%")
        break
      

      Solution: The cost of the room was £ 54 per night.

      And so the rail fares were £ 45 per person (single).

      The cost for 30 days is:

      4× 45 + 30× 54 = 1800

      And the cost for 33 days:

      4× 45 + 33× 54 = 1962

      And we have:

      1962 / 1800 = 1.09

      So the increase is 9%.


      There are larger solutions, for example:

      F=45, R=54
      F=495, R=594
      F=4545, R=5454
      F=4995, R=5994
      F=45045, R=54054
      F=49995, R=59994
      F=450045, R=540054
      F=454545, R=545454
      F=495495, R=594594
      F=499995, R=599994

      But the room is described as “inexpensive”, so only the first of these is a viable solution.

      However, they all involve a 9% increase.

      Like

    • GeoffR's avatar

      GeoffR 12:34 pm on 29 October 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 1..9:B; 
      var 1..99: per_cent;
      
      % Assume room cost < £100
      var 12..98: Room == 10*A + B; 
      var 12..98: Fare == 10*B + A;
      
      % Two people with outgoing and return trips = 4 fares
      var 600..6000: Cost30 == 30 * Room + 4 * Fare;
      var 660..6060: Cost33 == 33 * Room + 4 * Fare;
      
      % Calculate percentage increased room cost
      constraint (Cost33 - Cost30) * 100 ==  Cost30 * per_cent;
      
      solve satisfy;
      
      output ["Room cost = " ++ show(Room) ++ " pounds per night."
      ++ "\n" ++ "Cost percentage increase = " ++ show(per_cent) ++ " %"];
      
      % Room cost = 54 pounds per night.
      % Cost percentage increase = 9 %
      % ----------
      % ==========
      

      Like

    • Ruud's avatar

      Ruud 4:15 am on 30 October 2024 Permalink | Reply

      from istr import istr
      
      
      istr.repr_mode("int")
      for train in istr(range(10, 100)):
          room = train[::-1]
          if room[0] != 0:
              total30 = int(train * 4 + room * 30)
              total33 = int(train * 4 + room * 33)
              increase = ((total33 - total30) / total30) * 100
              if increase % 1 == 0:
                  print(f"{train=} {room=} {total30=} {total33=} {increase=:.0f}%")
      

      Like

    • Frits's avatar

      Frits 5:43 am on 3 November 2024 Permalink | Reply

      Objective was to use fewer iterations.

      from fractions import Fraction as RF
      
      # fare = 10 * f + g, room cost = 10 * g + f
      
      # t30 = 4*F + 30*R = 70*f + 304*g  
      # t33 = 4*F + 33*R = 73*f + 334*g
      
      # 100 + p = 100 * (73*f + 334*g) / (70*f + 304*g)     
      
      # integer percentages less than 10
      for p in range(9, 0, -1):
        # (7300*f + 33400*g) - (100 + p) * (70*f + 304*g) = 0
        # ((300 - p*70)*f + (3000 - 304*p)*g) = 0
        
        # calculate - f / g  (g will not be zero)
        r = RF(3000 - 304*p, 300 - p*70)
        if r > 0: continue  
        f, g = abs(r.numerator), abs(r.denominator)
        if not (0 < f < 10 and g < 10): continue 
        
        # double check
        if 100 * (73*f + 334*g) / (70*f + 304*g) - 100 != p: continue
        print(f"answer: {10 * g + f} pounds per night") 
      

      Like

  • Unknown's avatar

    Jim Randell 3:47 am on 6 October 2024 Permalink | Reply
    Tags: by: Danny Roth   

    Teaser 3237: Oranges and lemons 

    From The Sunday Times, 6th October 2024 [link] [link]

    George and Martha attend a local club where there is a “fruit machine”. There are three dials, each with five “fruits” but here the “fruits” are numbers. On the first dial are the numbers 1-5; on the second there are the numbers 5-9. On the third, 5 appears again but with four two-digit numbers.

    You put £1 in the machine, then pull a handle which spins the dials, and the numbers on the dials appear completely at random. The jackpot payout for three 5s is £52. Otherwise the machine pays £2 if the three displayed numbers sum to a prime total.

    If the machine pays out 80% of its intake on average, what are the lowest possible values for the for the four two-digit numbers?

    Note: Although not explicitly stated, the intention for this puzzle is that the four 2-digit numbers on the third reel are all different.

    [teaser3237]

     
    • Jim Randell's avatar

      Jim Randell 3:58 am on 6 October 2024 Permalink | Reply

      I think the puzzle could be clearer on what constitutes the “lowest possible values”. I went for the values with the smallest sum, but you could go for the smallest maximum value (although in the end both these give the same answer).

      I also assumed that numbers could be repeated on a reel (as the puzzle does not state that they are all different, and it is usual for the reels on a fruit machine to have repeated symbols). However we do get a plausible looking answer with the additional constraint that they are all different.

      (I have opened a discussion topic if you have thoughts on these issues [link]).

      There are 5^3 = 125 possible positions of the reels, each equally likely, so the total take is £125. And if the total payout among all positions is 80% of this, it is £100.

      from enigma import (irange, decompose, primes, cproduct, printf)
      
      primes.expand(113)  # max possible prime
      
      # check a set of reels; return (<total-take>, <total-payout>)
      def check(rs):
        t = w = 0
        for ns in cproduct(rs):
          t += 1
          if ns == (5, 5, 5):
            w += 52
          elif sum(ns) in primes:
            w += 2
        return (t, w)
      
      # the first two reels
      r1 = [1, 2, 3, 4, 5]
      r2 = [5, 6, 7, 8, 9]
      
      # consider total sum of the four 2-digit numbers
      for t in irange(40, 396):
        f = 0
        # construct possible third reels
        for ns in decompose(t, 4, increasing=1, sep=0, min_v=10, max_v=99, fn=list):
          r3 = [5] + ns
          (tot, pay) = check([r1, r2, r3])
          if pay == 100:
            # output solution
            printf("{r3} -> ({tot}, {pay})")
            f += 1
        if f > 0: break
      

      If repeated values are allowed:

      Solution: The 4-digit values with the lowest total are: 14, 14, 14, 14. (total = 56)

      If all values have to be different:

      Solution: The 4-digit values with the lowest total are: 14, 15, 16, 24. (total = 69)

      The second one was the published solution.


      To see the solution to the alternative puzzle where the four 2-digit numbers are all different, you just need to pass [[ sep=1 ]] to [[ decompose() ]] at line 24.

      I think it is likely that this is the answer that the setter was after. [This has now been confirmed].

      Liked by 1 person

    • Frits's avatar

      Frits 4:36 am on 6 October 2024 Permalink | Reply

      from itertools import combinations_with_replacement as cwr, product
      
      # determine suitable primes up to 99+5+9
      P = {3, 5, 7}
      P = {x for x in range(11, 114, 2) if all(x % p for p in P)}
      
      score = lambda seq: 52 if seq == (5, 5, 5) else (2 * (sum(seq) in P))
      
      d1 = range(1, 6)
      d2 = range(5, 10)
      # select four 2-digit numbers for the third dial
      for n4 in cwr(range(10, 100), 4):
        d3 = (5, ) + n4
        # play 5^3 games with every possible outcome
        # it should pay out 4 * 5^2 pounds (80%)
        if sum(score(p) for p in product(d1, d2, d3)) == 100:
          print("answer:", n4)
          exit() # lowest 
      

      Like

    • ruudvanderham's avatar

      ruudvanderham 1:12 pm on 6 October 2024 Permalink | Reply

      The program below finds all two digit numbers that lead to a payout of 100:

      import itertools
      from istr import istr
      
      
      for d3 in range(10, 100):
          payout = 52
          for d1 in range(1, 6):
              for d2 in range(5, 10):
                  if istr.is_prime(d1 + d2 + d3):
                      payout += 4 * 2
                  if istr.is_prime(d1 + d2 + 5):
                      payout += 2
      
          if payout == 100:
              print(d3, end=" ")
      

      If the digits may be the same, we pick four times the lowest value.
      If the digits have to be different, we pick the four lowest values.

      Like

    • Frits's avatar

      Frits 8:39 am on 7 October 2024 Permalink | Reply

      Using a different interpretation of “lowest possible values”.

      from itertools import product
      
      # determine suitable primes up to 99+5+9
      P = {3, 5, 7}
      P = {x for x in range(11, 114, 2) if all(x % p for p in P)}
      
      payout = lambda seq: 52 if seq == (5, 5, 5) else (2 * (sum(seq) in P))
      
      d1 = range(1, 6)
      d2 = range(5, 10)
      
      n_rng = [5] + list(range(10, 100)) 
      
      # payouts per number on the 3rd dial
      pay = {n: sum(payout(p) for p in product(d1, d2, [n])) for n in n_rng}
      # retrieve the lowest number on the 3rd dial for a specific payout
      low = {v: min([k for k in pay.keys() if pay[k] == v]) for v in pay.values()}
      
      todo = 100 - pay[5] # total expected payout is 0.80 * 5^3
      
      # select four 2-digit numbers for the third dial so the combined payout is
      # equal to <todo>
      n4s = [[low[x] for x in p] for p in product(low, repeat=4) if sum(p) == todo]
      
      # assume “lowest possible values" means minimal sum
      mn = min([sum(n4) for n4 in n4s])
      print("answer:", ' or '.join(str(sorted(n4)) for n4 in n4s if sum(n4) == mn)) 
      

      Like

  • Unknown's avatar

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

    Teaser 2586: Powerful dice 

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

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

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

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

    [teaser2586]

     
    • Jim Randell's avatar

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

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

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

      Run: [ @replit ]

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

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

      We have:

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

      512 appears as both a row and a column.

      The most likely scenario is:

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

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

      Like

      • ruudvanderham's avatar

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

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

        Like

        • Ruud's avatar

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

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

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

          Like

          • Jim Randell's avatar

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

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

            Like

            • Ruud's avatar

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

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

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

              Like

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