Recent Updates Page 4 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:21 am on 22 May 2026 Permalink | Reply
    Tags:   

    Teaser 2437: [Spending money] 

    From The Sunday Times, 7th June 2009 [link]

    My granddaughter Madeline has been shopping. When I asked her the prices of the three items she had bought, she said that each cost less than £10, and that the three prices between them used all the digits 1 to 9. Furthermore, the middle-priced item cost one penny less than three times the cheapest, and the total amount spent was a whole number of pounds.

    What were the three prices?

    This puzzle was incorrectly published as Teaser 2537.

    This puzzle was originally published with no title.

    [teaser2437]

     
    • Jim Randell's avatar

      Jim Randell 8:23 am on 22 May 2026 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # suppose the prices (in pence) are:
      #
      #  ABC = cheapest
      #  DEF
      #  GHI = most expensive
      #
      --digits="1-9"
      
      # the prices are in order
      "A < D < G"
      
      # the middle price is 1p less than 3 times the cheapest
      "3 * ABC - 1 = DEF"
      
      # the total came to a whole number of pounds (= multiple of 100p)
      "(BC + EF + HI) % 100 = 0"
      
      # answer is the 3 prices
      --answer="(ABC, DEF, GHI)"
      

      Solution: The prices were: 239p, 716p, 845p.

      The total amount spent being 1800p = £18.

      Like

    • ruudvanderham's avatar

      ruudvanderham 9:52 am on 22 May 2026 Permalink | Reply

      import peek
      import istr
      
      set123456789=set(istr.range(1, 10))
      for price2 in istr.range(1, 1000):
          price1 = 3 * price2 - 1
          if price2 < 1000 and (price1 | price2).all_distinct():
              for price0 in istr.range(price1 + 1, 1000):
                  if set(price0 | price1 | price2) == set123456789 and (price0 + price1 + price2)[-2:]=="00":
                      peek(price0, price1, price2, price0 + price1 + price2)
      

      Like

    • ruudvanderham's avatar

      ruudvanderham 1:37 pm on 22 May 2026 Permalink | Reply

      Here’s a different (significantly faster) version:

      import peek
      import istr
      
      set123456789 = set(istr.range(1, 10))
      for price2 in istr.range(1, 1000):
          price1 = 3 * price2 - 1
          if price2 < 1000:
              for total in istr.range((price2 + price1 * 2 + 1).ceil(100), 3000, 100):
                  price0 = total - price1 - price2
                  if price0 >= 1000:
                      break
                  if set(price0 | price1 | price2) == set123456789:
                      peek(price0, price1, price2, price0 + price1 + price2)
      

      Like

    • Frits's avatar

      Frits 5:22 pm on 2 June 2026 Permalink | Reply

      # prices p1 < p2 < p3
      for p2 in range(123 * 3 - 1, 876 + 1, 3):
        if len(s3 := set(str(p2))) != 3: continue
        if '0' in s3: continue
        # calculate and check p1
        if len(s6 := set(str(p1 := (p2 + 1) // 3)) | s3) != 6: continue
        if '0' in s6: continue
        # last 2 digits of p3 in order to get a whole number of pounds
        if not (11 < (l2_p3 := 100 - ((p1 + p2) % 100)) < 99): continue
        # different digits
        if not s6.isdisjoint(s2 := set(str(l2_p3))): continue
        # calculate first digit of p3
        if len(f_p3 := set("123456789") - s6 - s2) != 1: continue
        if (p3 := 100 * int(f_p3.pop()) + l2_p3) <= p2: continue
        print(f"answer: {p1}, {p2} and {p3}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:01 am on 20 May 2026 Permalink | Reply
    Tags: ,   

    Teaser 2418: [Square park] 

    From The Sunday Times, 25th January 2009 [link]

    Our park is square, with sides of length 104 metres. In the centre is a circular garden with a diameter of 38 metres.

    When walking around the edge of the garden, I can reach a point that is a whole number of metres from the nearest corner of the park, and is also a whole number of metres from the furthest corner of the park.

    What are those two distances?

    This puzzle was originally published with no title.

    [teaser2418]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 20 May 2026 Permalink | Reply

      We place the corners of the park at (±52, ±52), and the circle is centred on (0, 0) with a radius of 19.

      If the point on the circumference of the circle is (x, y), and the distance to the nearest corner (+52, +52) is a, and the distance to the furthest corner (−52, −52) is b, then we have the following equations:

      x² + y² = 19
      (52 − x)² + (52 − y)² = a²
      (52 + x)² + (52 + y)² = b²

      And we can eliminate x and y to get:

      a² + b² = 4 × 52² + 2 × 19² = 11538

      So we can look for two squares that sum to give the required value.

      The following Python program runs in 70ms. (Internal runtime is 51µs).

      from enigma import (
        sq, sum_of_squares, circle_intersect_circle, peek, point_dist, printf
      )
      
      S = 4 * sq(52) + 2 * sq(19)
      for (a, b) in sum_of_squares(S, 2):
        printf("a={a} b={b} [S={S}]")
      
        # find one of the intersection points
        p = peek(circle_intersect_circle(((52, 52), a), ((-52, -52), b)), default=None)
        if p is None: continue
        printf("-> p={p}")
      
        # determine distance to the corners
        for q in [(52, 52), (-52, -52), (-52, 52), (52, -52)]:
          d = point_dist(p, q)
          printf("-> dist to {q} = {d:.6f}")
        printf()
      

      Solution: The distances are 63 m and 87 m.

      However there is a problem with this answer.

      If we look at the distances to each of the corners, we find that the integer distances are not the distances to the nearest and furthest corners (at least not the nearest and furthest from (x, y)).

      (The integer distances are shown in red).

      The distances are:

      AX = 63 m
      BX = 88.92 m
      CX = 87 m
      DX = 60.26 m

      The shortest distance is DX and the longest distance is BX.

      But the puzzle can be saved if the radius of the circle is changed from 19 m to 18 m, then the integer distances are 58 m and 90 m, and these are the nearest and furthest distances:

      In this case the distances are:

      AX = 58 m
      BX = 83.16 m
      CX = 90 m
      DX = 67.44 m

      In fact for N = 52 there are solutions when R = 18, 27, 48.

      And there are solutions with integer distances that are not the nearest/furthest distances when R = 19, 26, 33, 39, 51.

      Like

  • Unknown's avatar

    Jim Randell 6:24 am on 17 May 2026 Permalink | Reply
    Tags:   

    Teaser 3321: The more you buy, the more you save 

    From The Sunday Times, 17th May 2026 [link] [link]

    … or so we are often encouraged to believe.

    I needed to buy exactly 42 coloured pens and I could buy them in packs of 1, 4, 7, 11 or 17. A pack of 1 cost £2, a pack of 17 cost £23, and the prices of the other packs were whole numbers of pounds. The average price per pen always decreased going to larger packs. Given the prices I was faced with, I found a suitable and unique combination of the packs that gave me the lowest possible total cost, and this involved buying at least one pack of 11.

    In ascending order of pack size, how many of each pack did I buy, and what were the pack prices (e.g., 5 at £2 each, 3 at £8 each, etc.)?

    [teaser3321]

     
    • Jim Randell's avatar

      Jim Randell 6:39 am on 17 May 2026 Permalink | Reply

      We can use the more sophisticated [[ Denominations() ]] class from the enigma.py library to solve this puzzle.

      The following Python program runs in 70ms. (Internal runtime is 1.6ms).

      from enigma import (
        Denominations, Accumulator,
        irange, choose, fdiv, vec_dot, singleton, seq2str, fmts, printf
      )
      
      # denominations
      ds = [1, 4, 7, 11, 17]
      # find ways to express 42 using the given denominations
      qss = list(Denominations(ds).express(42))
      
      # find prices for 4, 7, 11 packs (cost per item decreases)
      (f1, f17) = (fdiv(2, 1), fdiv(23, 17))
      fns = [
        (lambda p4: f1 > fdiv(p4, 4) > f17),
        (lambda p4, p7: fdiv(p4, 4) > fdiv(p7, 7) > f17),
        (lambda p4, p7, p11: fdiv(p7, 7) > fdiv(p11, 11) > f17),
      ]
      for (p4, p7, p11) in choose(irange(3, 22), fns):
        ps = (2, p4, p7, p11, 23)
      
        # find ways to buy exactly 42 for the lowest total cost
        r = Accumulator(fn=min, collect=1)
        for qs in qss:
          t = vec_dot(qs, ps)
          r.accumulate_data(t, qs)
        # there is a single way to achieve the lowest total cost
        # which involves buying at least 1 pack of 11 (index 3)
        (t, qs) = (r.value, singleton(r.data))
        if qs is None or qs[3] < 1: continue
      
        # output solution
        fmt = lambda vs: seq2str(vs, fn=fmts("2d"), enc="[]")
        printf("total cost = {t}")
        printf("-> packs      = {ds}", ds=fmt(ds))
        printf("-> prices     = {ps}", ps=fmt(ps))
        printf("-> quantities = {qs}", qs=fmt(qs))
        printf()
      

      Solution: 3 at £2 each; 2 at £15 each; 1 at £23.

      i.e. the pack prices were:

      1-pack = £2 (= £2.00/item)
      4-pack = £7 (= £1.75/item)
      7-pack = £12 (= £1.71/item)
      11-pack = £15 (= £1.36/item)
      17-pack = £23 (= £1.33/item)

      And the 42 pens were bought as:

      3× 1-pack (= 3 items, £6)
      2× 11-pack (= 22 items, £30)
      1× 17-pack (= 17 items, £23)
      total = 42 items, £59 (avg = £1.40/item)

      Like

    • Ruud's avatar

      Ruud 5:07 pm on 17 May 2026 Permalink | Reply

      import collections
      import itertools
      
      p17 = 23
      p1 = 2
      for p4 in range(23):
          if not (p17 / 17) < p4 / 4 < (p1 / 1):
              continue
          for p7 in range(23):
              if not (p17 / 17) < p7 / 7 < (p4 / 4):
                  continue
              for p11 in range(23):
                  if not (p17 / 17) < p11 / 11 < (p7 / 7):
                      continue
      
                  n = 42
                  c = collections.defaultdict(list)
                  for n17 in itertools.count():
                      a17 = n - n17 * 17
                      if a17 < 0:
                          break
                      for n11 in itertools.count():
                          a11 = a17 - n11 * 11
                          if a11 < 0:
                              break
                          for n7 in itertools.count():
                              a7 = a11 - n7 * 7
                              if a7 < 0:
                                  break
      
                              for n4 in itertools.count():
                                  a4 = a7 - n4 * 4
                                  if a4 < 0:
                                      break
                                  n1 = a4
      
                                  sum = n17 * p17 + n11 * p11 + n7 * p7 + n4 * p4 + n1 * p1
                                  c[sum].append((n1, n4, n7, n11, n17))
                  if len(lowest := c[min(c)]) == 1 and lowest[0][3]:
                      print(f"total={min(c)} quantities={lowest[0]} prices={p1, p4, p7, p11, p17}")
      

      Like

    • Hugo's avatar

      Hugo 8:02 am on 24 May 2026 Permalink | Reply

      I reckoned two 7-packs at £10 each, one 11-pack at £15, and one 17-pack at £23,
      making £58 in all. Where did I go wrong?

      Like

      • Jim Randell's avatar

        Jim Randell 9:04 am on 24 May 2026 Permalink | Reply

        For example, with prices of:

        1-pack = £2
        4-pack = £7
        7-pack = £10
        11-pack = £15
        17-pack = £23

        The lowest possible cost for 42 pens is £58 (as you say).

        But there are two ways this minimum price can be achieved:

        2× 7-packs (@ £10) = £20
        1× 11-pack (@ £15) = £15
        1× 17-pack (@ £23) = £23
        total (42 items) = £58

        1× 1-pack (@ £2) = £2
        1× 7-pack (@ £10) = £10
        2× 17-pack (@ £23) = £46
        total (42 items) = £58

        So this cannot be scenario, as there must be only one way to achieve the minimum price.

        In fact there are 9 possible sets of prices that allow the condition that the prices per item decreases as the pack price increases. Of these only 3 have a unique way to achieve the minimum price (which, in each case, is £59). And only one of these 3 candidates uses 11-packs in the unique arrangement that achieves the minimum price, and so this provides the answer to the puzzle.

        Like

  • Unknown's avatar

    Jim Randell 8:21 am on 15 May 2026 Permalink | Reply
    Tags: by: Rupert Segar   

    Brain teaser 975: Ambiguous birthdays 

    From The Sunday Times, 29th March 1981 [link]

    Recently I attended the Numerical Astrologers Conference, held annually in Brighton. At this meeting, members wore badges on which were written not only their names, but also their dates of birth. These dates were written numerically in the usual English manner of first putting the day, then the month, then the year (e.g. 7th March 1946 would be represented by 7/3/46, and the 31st December 1954 by 31/12/54). The year always uses two digits.

    I had just entered the meeting when I was accosted by an internationally acclaimed cabalist, who, I noticed, shared with me the month of his birthday, though he was born in a year eight years prior to the year of my birth. He had been looking at my badge, and now told me that the digits of my date of birth were, in sequence, in exactly the reverse order to his own.

    We were eagerly discussing this coincidence when an old acquaintance of mine introduced himself to the cabalist, who then made the same claim to my friend about their dates of birth. At first this puzzled me, as I knew my friend to be younger than myself, but, upon inspection, both the claims proved to be correct.

    Strangely, both of us had dates of birth whose digits, when taken in completely the reverse order, were exactly the same as those of the cabalist.

    Numerically, what was the cabalist’s date of birth?

    The puzzle is included in the book The Sunday Times Book of Brainteasers (1994).

    [teaser975]

     
    • Jim Randell's avatar

      Jim Randell 8:22 am on 15 May 2026 Permalink | Reply

      The cabalist’s DOB cannot be 6 digits or 4 digits, as there is only one way to reverse these:

      UV/WX/YZ → ZY/XW/VU
      W/X/YZ → Z/Y/XW

      But if it had 5 digits, there are 2 ways it can be reversed:

      V/WX/YZ or VW/X/YZ → Z/YX/VW or ZY/X/VW

      As the friend is younger than the setter they must be born later in the year, so:

      setter = ZY/X/VW
      friend = Z/YX/VW

      And the cabalist and the setter share their birth month, so:

      cabalist = VW/X/YZ
      setter = ZY/X/VW
      friend = Z/YX/VW

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

      It runs in 86ms. (Internal runtime of the generated code is 2.2ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # convert a 2-digit year
      --code="year = lambda y: (1900 + y if y < 70 else 1800 + y)"
      # check for a valid date
      --code="from datetime import date"
      --code="valid = lambda d, m, y: catch(date, year(y), m, d) is not None"
      
      # the cabalist's DOB is VW/X/YZ
      # and can be reversed as setter = ZY/X/WV, friend = Z/YX/WV
      --distinct=""
      --invalid="0,VYZ" # dates do not have components with leading zeros
      
      # check the three dates are valid
      "valid(VW, X, YZ)"
      "valid(ZY, X, WV)"
      "valid(Z, YX, WV)"
      
      # the [2-digit] years are 8 apart
      "year(YZ) + 8 == year(WV)"
      
      --template="VW/X/YZ -> ZY/X/WV and Z/YX/WV"
      --solution=""
      

      Solution: The cabalist’s date of birth was 12/1/13 (12th January 1913).

      And so the setter’s date of birth is 31/1/21 (31st January 1921) and the friend’s date of birth is 3/11/21 (3rd November 1921).

      Like

    • ruudvanderham's avatar

      ruudvanderham 2:01 pm on 15 May 2026 Permalink | Reply

      import datetime
      
      date0 = datetime.datetime(1880, 1, 1)
      date1 = datetime.datetime(1980, 1, 1)
      
      day = {}
      month = {}
      date = date0
      while date < date1:
          date = date + datetime.timedelta(1)
          as_str = f"{date.day}{date.month}{date.year % 100:02d}"[::-1]
          if len(as_str) == 5 and "0" not in as_str[:3]:
              year = int(as_str[3:])
              year += 1800 if year > 80 else 1900
              if year - date.year == 8:
                  for i in range(2):
                      month[i] = int(as_str[i + 1 : 3])
                      day[i] = int(as_str[: i + 1])
                      if month == date.month:
                          i_equal = i
                      try:
                          datetime.datetime(year, month[i], day[i])
                      except ValueError:
                          break
                  else:
                      for i in range(2):
                          if month[i] == date.month:
                              if datetime.datetime(year, month[i], day[i]) < datetime.datetime(year, month[1 - i], day[1 - i]):
                                  print(
                                      f"Cabalist: {date:%Y-%m-%d} / Me: {datetime.datetime(year, month[i], day[i]):%Y-%m-%d} / Friend:{datetime.datetime(year, month[1 - i], day[1 - i]):%Y-%m-%d}"
                                  )
      

      Like

  • Unknown's avatar

    Jim Randell 8:05 am on 13 May 2026 Permalink | Reply
    Tags:   

    Teaser 2417: [Flower bed] 

    From The Sunday Times, 18th January 2009 [link]

    Old Mellors, the estate gardener, has designed a large flower bed. He has marked out two overlapping circles whose radii differ by one metre: the bed consists of the area within one or both of the circles. He wants to plant various straight lines of seeds in the flower bed. The longest such possible is 25 metres long; if he wants the line to touch the perimeter of the bed in three places, then the longest possible is 22 metres.

    What are the radii of the circles?

    This puzzle was originally published with no title.

    [teaser2417]

     
    • Jim Randell's avatar

      Jim Randell 8:08 am on 13 May 2026 Permalink | Reply

      If we suppose the circle have radii R and r, where R ≥ r, then:

      For the circles to overlap the distance d between their centres must be:

      R − r < d < R + r

      In this case we have R = r + 1, so:

      1 < d < 2r + 1

      The longest possible line is 25, so:

      R + d + r = 25
      ⇒
      d = 24 − 2r

      Hence:

      d > 1
      ⇒
      r < 11.5

      and:

      d < 2r + 1
      ⇒
      r > 5.75

      So we have bounds on possible values for r, we can look for situations where the maximum line through an intersection is 22 m.


      First I defined some additional 2D geometry functions to help with circles:

      Circle = namedtuple('Circle', 'centre radius')
      
      # find intersection points of circle <c>
      # with the line defined by points <p1>, <p2>
      # may return 0, 1, 2 points
      def circle_intersect_line(c, p1, p2):
        (((x0, y0), r), (x1, y1), (x2, y2)) = (c, p1, p2)
        (xd, yd) = (x2 - x1, y2 - y1)
        # construct a polynomial for the intersection
        f = sq(Polynomial([x1 - x0, xd])) + sq(Polynomial([y1 - y0, yd]))
        ts = f.roots(sq(r), domain='F')
        return list(P2(x1 + t * xd, y1 + t * yd) for t in ts)
      
      # parameterised circle t = -1 to +1 for a full circle
      # 0 = 3 o'clock; 0 -> 1 = anticlockwise to 9 o'clock; 0 -> -1 = clockwise to 9 o'clock
      def circle_param(c, t=None):
        ((x, y), r) = c
        f = lambda t, x=x, y=y, r=r: P2(x + r * math.cos(t * pi), y + r * math.sin(t * pi))
        return (f if t is None else f(t))
      
      # find <t> parameter for point <p> on circle centre <c> radius <r>
      def circle_param_t(c, p, t=1e-9):
        (o, r) = c
        if not (abs(point_dist(o, p) - r) < t): return None
        ((x0, y0), (x, y)) = (o, p)
        t = math.acos(fdiv(x - x0, r)) / pi
        return min(t, -t, key=(lambda t: abs(point_dist(circle_param(c, t), p))))
      

      (These functions are in the latest version of enigma.py).

      The following Python program solves the problem constructively. Given a value for r (the radius of the smaller circle) it first derives the values of R (the radius of the larger circle) and d (the separation between the centres), and then finds P (one of the intersection points of the circle). It then extends the tangent to the smaller circle at P to intersect with the larger circle at Q.

      We then look for a point A on the circumference of the larger circle between P and Q, and extend AP to a point B on the smaller circle. And we choose A so to maximise the length of AB (using the [[ find_max() ]] function from the enigma.py library.

      We can then use the [[ find_values() ]] function from the enigma.py library to determine what value of r gives a maximum AB of 22.

      It runs in 220ms. (Internal runtime is 151ms).

      from enigma import (
        Circle, circle_intersect_line, circle_param, circle_param_t,
        triangle_point, point_dist, line_param, line_bisect,
        find_max, find_values, call, item, printf
      )
      
      # extract leftmost and rightmost points from a sequence
      leftmost = lambda pts: (min(pts, key=item(0)) if pts else None)
      rightmost = lambda pts: (max(pts, key=item(0)) if pts else None)
      
      # find maximum length line through intersection point P, given <r>
      def solve(r):
        # larger circle radius is 1 m larger
        R = r + 1
        # distance between circle centres
        d = 24 - 2*r
      
        # specify the two circles (centre, radius)
        (CR, Cr) = (Circle((0, 0), R), Circle((d, 0), r))
      
        # find the upper intersection point = P
        P = triangle_point(d, R, r)
        # and the corresponding start <t> parameter on the larger circle
        tP = circle_param_t(CR, P)
      
        # calculate the tangent to the smaller circle at P
        f = line_param(Cr.centre, P)
        (p1, p2) = line_bisect(f(0), f(2))
        # and where it intersects the larger circle at Q
        Q = leftmost(circle_intersect_line(CR, p1, p2))
        # and the finish <t> parameter on the larger circle
        tQ = circle_param_t(CR, Q)
        if tQ < tP: tQ += 2
      
        # points on the larger circle parameterised by <t>
        fR = circle_param(CR)
        # find the A, B intersection points for A at parameter <t>
        def fAB(t):
          # A is on the larger circle
          A = fR(t)
          # the line from A extends through P and intersects the smaller circle at B
          B = rightmost(circle_intersect_line(Cr, A, P))
          return (A, B)
        # find the maximum length AB line for <t> parameters between P and Q
        m = find_max((lambda t: call(point_dist, fAB(t))), tP, tQ)
        # return the maximal value
        return m.fv
      
      # find <r> when the max line through an intersection point is 22
      for s in find_values(solve, 22.0, 5.75, 11.5):
        # determine parameters
        r = s.v
        R = r + 1
        d = 24 - 2*r
        printf("r = {r:.2f}; R = {R:.2f}; d = {d:.2f}")
      

      Solution: The radii of the circles are 6.5 m and 7.5 m.

      And the distance between the centres of the circles is 11 m.


      Manually:

      Suppose the centres of the circles are separated by a distance d.

      Then we can plot the circles as follows:

      centre of the larger circle (radius R) = (0, 0)
      centre of the smaller circle (radius r) = (d, 0)
      upper intersection point of the circles, P = (x, y)

      We can draw a straight line L through P at an angle of 𝛉.

      At a distance t (negative to the left and positive to the right) it has the following parametric equation:

      L(t) = (x + t cos(𝛉), y + t sin(𝛉))

      The intersection with the larger circle (A) is given when t ≠ 0 and:

      (x + t cos(𝛉))² + (y + t sin(𝛉))² = R²
      ⇒
      t = −2(x cos(𝛉) + y sin(𝛉))

      So the distance AP is given by:

      AP = 2(x cos(𝛉) + y sin(𝛉))

      Similarly the intersection with the small circle (B) is given by:

      (x + t cos(t) − d)² + (y + t sin(t))² = r²
      ⇒
      t = 2(d cos(𝛉) − x cos(𝛉) − y sin(𝛉))

      So the distance BP is given by:

      BP = 2(d cos(𝛉) − x cos(𝛉) − y sin(𝛉))

      And so the total distance AB is:

      AB = 2(x cos(𝛉) + y sin(𝛉)) + 2(d cos(𝛉) − x cos(𝛉) − y sin(𝛉))
      AB = 2d cos(𝛉)

      From which we see the distance AB achieves a maximum when 𝛉 = 0 (i.e. when the line is horizontal [*]), and the maximum value is twice the distance between the centres of the circles.

      We can now apply this finding to the puzzle.

      The maximum length line through the intersection is 22 m, hence the distance between the circles is 11 m.

      And the maximum length line between two points on the perimeter of the bed is 25 m.

      Hence:

      R + d + r = 25
      (r + 1) + 11 + r = 25
      ⇒
      2r = 13
      r = 6.5

      And the solution follows.


      [*] However, not all configurations permit a horizontal line to be drawn, in which case the maximum length line is given by the tangent to the smaller circle (PQ), with the angle increased infinitesimally to permit the line to intercept the perimeter of the combined shape at 3 distinct points.

      For example:

      Like

  • Unknown's avatar

    Jim Randell 12:07 am on 10 May 2026 Permalink | Reply
    Tags:   

    Teaser 3320: Penblwydd Hapus 

    From The Sunday Times, 10th May 2026 [link] [link]

    I’m now in my thirties and for many years I’ve treated myself to some bottles of beer on my birthday. To compensate for my increasing age, I buy the same number of bottles each year as my age. On my recent birthday, after storing my bottles, I noticed that, on the beer receipt from the supermarket, all of the nine digits 1 to 9 appeared once, and only once, in the number of bottles, the price per bottle in pence and the total cost in pence.

    What was the total cost of the beer in pence?

    [teaser3320]

     
    • Jim Randell's avatar

      Jim Randell 12:20 am on 10 May 2026 Permalink | Reply

      I think the wording on this puzzle could be tightened up a little. (Specifically to clear up: “what constitutes a recent birthday?”, and “how many times can the digit 0 appear?”).

      I assumed we are looking for a sum consisting of 9 digits, using each of the digits 1-9 exactly once, but I find multiple solutions. (And if we allow the digit 0 to appear then there are even more solutions).

      Using birthdays in the last 5 years, but disallowing 0 digits, I get 3 candidate solutions. If we take “recent birthday” to mean “most recent birthday”, then we can eliminate two of these, so maybe that is what the setter intended.

      This Python program runs in 88ms. (Internal runtime is 19ms).

      from enigma import (irange, inf, nsplit, flatten, printf)
      
      # consider possible ages for a "recent" birthday
      for n in irange(26, 39):
        # consider price per bottle
        for p in irange(100, inf):
          # calculate the total cost
          t = n * p
          # find the digits used in the sum
          ds = flatten(nsplit(x) for x in (n, p, t))
          # look for 9 different non-zero digits
          if len(ds) > 9: break
          if 0 not in ds and len(set(ds)) == 9:
            # output solution
            printf("{n} * {p} = {t}")
      

      Solution: The total cost of the beer was 7254p.

      If we suppose the “recent birthday” is the setter’s most recent birthday (that perhaps happened within the last few weeks), then the birthday is 30 .. 39, and the only solution using just the 9 digits 1-9 exactly once is:

      39 × 186 = 7254

      i.e. the beer cost 186p per bottle.

      However, if the recent birthday is one of the last (say) 5 (the puzzle text does say the practice has been going on “for many years”), then it could be between 26 .. 39, and this permits the following additional solutions:

      27 × 198 = 5346
      28 × 157 = 4396

      Also, the puzzle text does not place restrictions on how many occurrences of the digit 0 there may be, so we can get the following reasonable solutions:

      27 × 594 = 16038
      36 × 495 = 17820
      39 × 402 = 15678

      And if very expensive bottles of beer were purchased there are further solutions with more than one 0 digit, e.g.:

      39 × 1284 = 50076

      In fact any solution can have the price per bottle and the total price multiplied by 10 to give a further solution if no restrictions are placed on the digit 0.


      So, I think the intended puzzle is:

      I am now in my thirties. On my most recent birthday I bought a number of identical bottles of beer. That number was the same as my age in years.

      The number of bottles, the price per bottle in pence, and the total cost in pence, when taken together comprised 9 different non-zero digits, with each of the digits 1–9 occurring exactly once.

      What (in pence) was the total cost of the beer?

      This revised puzzle is easily solved directly from the command line using the [[ SubstitutedExpression ]] solver from the enigma.py library:

      % python3 enigma.py SubstitutedExpression "AB * CDE = FGHI" --digits="1-9" --assign="A,3"
      (AB * CDE = FGHI)
      (39 * 186 = 7254) / A=3 B=9 C=1 D=8 E=6 F=7 G=2 H=5 I=4
      [1 solution]
      

      (The internal runtime of the generated code is 550µs).

      Like

    • Ruud's avatar

      Ruud 6:48 am on 10 May 2026 Permalink | Reply

      It is easy to see that the price should have 3 digits and the total cost 4 digits.

      import peek
      import istr
      
      for age in istr.range(31, 40):
          for rest in istr.permutations({*istr.range(1, 10)} - {*age}):
              if (price := istr.join(rest[:3])) * age == (total := istr.join(rest[3:])):
                  peek(age, price, total)
      

      Like

    • Frits's avatar

      Frits 11:04 am on 10 May 2026 Permalink | Reply

      # forbidden unit digits (3 is reserved for the tens digit of the age) 
      fb = {0, 3}
      
      # dictionary of price unit digits per n_bottles unit digit
      d = {b: {p for p in range(10) if p not in fb and p != b and 
                                      (u := (p * b) % 10) not in fb | {b, p}} 
              for b in range(10) if b not in fb}
      
      # the number of bottles (equal to age)
      for b in [n for n in range(30, 40) if n % 10 in d]:
        # unit digit
        u = b % 10
        # minimum/maximum price, price/cost should have resp. 3/4 digits
        mn, mx = 124, 9876 // b
        #pr(b, (mn, mx), (b * mn, b * mx), d[u])
        # the price per bottle in pence
        for p in [n for n in range(mn, mx + 1) if n % 10 in d[u]]:
          if '0' in (dgts := set(str(b) + str(p) + str((c := b * p)))): continue
          if len(dgts) == 9:
            print(f"answer: {c}p")
      

      Like

    • Hugo's avatar

      Hugo 12:29 pm on 12 May 2026 Permalink | Reply

      Keeping the pattern AB × CDE = FGHI but without the condition that A = 3, I found
      18 × 297 = 5346
      27 × 198 = 5346
      28 × 157 = 4396
      42 × 138 = 5796
      48 × 159 = 7632

      I didn’t try any other patterns, with or without the inclusion of 0.
      No doubt Jim can fill that gap for us.

      Like

  • Unknown's avatar

    Jim Randell 8:35 am on 8 May 2026 Permalink | Reply
    Tags:   

    Teaser 2441: [Letter grid] 

    From The Sunday Times, 5th July 2009 [link]

    Your task today is to place nine different letters of the alphabet in a 3-by-3 grid.

    The letters must include S, A, M, E. You must do this in such a way that, when the letters are given their numerical value (A = 1, B = 2, C = 3, etc.) then each row, column and diagonal of the grid has the same total.

    You will find that the other five letters used can swiftly be rearranged into a common word.

    What is that word?

    This puzzle was originally published with no title.

    [teaser2441]

     
    • Jim Randell's avatar

      Jim Randell 8:36 am on 8 May 2026 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # assign values from 1 - 26 to the grid:
      #
      #   A B C
      #   D E F
      #   G H I
      #
      # to make a magic square with sum 3E
      --base="27"
      --digits="1-26"
      
      # rows sum to 3E
      "A + B + C == 3 * E"
      "D + F == 2 * E"
      "G + H + I == 3 * E"
      
      # cols sum to 3E
      "A + D + G == 3 * E"
      "B + H == 2 * E"
      "C + F + I == 3 * E"
      
      # diags sum to 3E
      "A + I == 2 * E"
      "C + G == 2 * E"
      
      # required values (S, A, M, E)
      "{1, 5, 13, 19}.issubset({A, B, C, D, E, F, G, H, I})"
      
      # remove symmetric arrangements
      "A < C" "C < G" "A < I"
      
      --template=""
      --answer="ordered(A, B, C, D, E, F, G, H, I)"
      

      The completed grid is:

      [ C  (3) | U (21) | I  (9) ]      [ S (19) | A  (1) | M (13) ]
      [ Q (17) | K (11) | E  (5) ]  ->  [ E  (5) | K (11) | Q (17) ]
      [ M (13) | A  (1) | S (19) ]      [ I  (9) | U (21) | C  (3) ]

      Each row, column and diagonal has a sum of 33.

      And the corresponding letters are: SAME (= 19, 1, 13, 5) + KQIUC (= 11, 17, 9, 21, 3).

      Solution: The word is: QUICK.

      Like

      • Jim Randell's avatar

        Jim Randell 3:04 pm on 23 July 2026 Permalink | Reply

        Here is a wrapper that runs my previous code, and finds words from a word list that can be made using the identified letters.

        I used a list of words in frequency order (so the most common words appear first), and then select the 5000 most frequently occurring 5-letter words to consider.

        We then run my previous program to find the numbers in the grid, convert them to letters, remove SAME, and find what words can be made by rearranging the remaining letters.

        from enigma import (SubstitutedExpression, readlines, group, join, diff, printf)
        
        # collect 5-letter words by letter content
        path = "words/words_by_frequency.txt"
        st = (lambda w: str.isalpha(w) and len(w) == 5)
        by = (lambda w: join(w, sort=1))
        with open(path, "r") as fh:
          words = group(readlines(fh, fn=str.upper, st=st, first=5000), by=by)
        
        # solve the puzzle
        p = SubstitutedExpression.from_file(["{dir}/teaser2441.run"])
        for ns in p.answers(verbose=0):
          printf("numbers = {ns}")
          # convert the numbers to letters, and exclude SAME
          k = diff((chr(n + 64) for n in ns), "SAME", fn=join)
          # output possible words
          for w in words.get(k, ()):
            printf("-> word = {w}")
          printf()
        

        Like

    • Frits's avatar

      Frits 8:55 pm on 21 July 2026 Permalink | Reply

      Only 4 values for the center cell have to be considered.

      from math import ceil
      
      # decompose <t> into <k / 2> pairs (m, t - x) from <ns>
      def decompose_pairs(t, k, ns, m, s=[]):
        if k == 0:
          yield s
        else:
          for n in ns:
            if 2 * n > m: break
            if (m - n) not in ns or (s and n < s[-2]): continue
            yield from decompose_pairs(t - m, k - 2, ns - {n, m - n}, m, s + [n, m - n])
      
      # decompose <t> into <k> numbers from <ns>
      def decompose(t, k, ns, s=[]):
        if k == 1:
          if t in ns:
            yield tuple(s + [t])
        else:
          for (i, n) in enumerate(ns):
            if t - n < 0: continue
            yield from decompose(t - n, k - 1, ns[i + 1:], s + [n])
      
      SAME = {1 + ord(c) - ord("A") for c in "SAME"}
      
      # a b c  
      # d e f  sum of all numbers = 9.e 
      # g h i
      
      # available numbers 
      av = set(range(1, 27)) - SAME
      # the center cell can't be too high or too low
      min_e = max(ceil((sum(SAME) + sum((s := sorted(av))[:5])) / 9), 
                  ceil((max(SAME) + 1) / 2))
      max_e = min((sum(SAME) + sum(s[-5:])) // 9, 
                  (min(SAME) + 26) // 2)
      
      for e in range(min_e, max_e + 1):
        # every non-center cell x has to have a corresponding mirror 2.e - x
        cs = {2 * e - x for x in SAME if x != e and x < 2 * e}
        ns = SAME | cs | {e}
      
        # find remaining numbers by looking for mirror pairs
        for r in decompose_pairs(9 * e - sum(ns), 9 - len(ns), av - ns, 2 * e):
          n9 = ns | set(r)
          # select 3 numbers than can form an edge, we need 4 edges
          if len(es := list(decompose(3 * e, 3, sorted(n9 - {e})))) < 4:
            continue
          
          assert len(es) == 4 # not sure if we can demand this
          non_center = [*x for x in es]   
          corners = sorted(x for x in set(non_center) if non_center.count(x) > 1)
          if len(corners) != 4: continue
          
          w5 = n9 - SAME
          print("answer: some rearrangement of", 
                ''.join(chr(ord("A") + n - 1) for n in w5))
          
          a, c, g, i = corners
          b = 3 * e - a - c
          d = 3 * e - a - g
          f = 3 * e - c - i
          h = 3 * e - g - i
          print(f"\n{a:>2} {b:>2} {c:>2}")
          print(f"{d:>2} {e:>2} {f:>2}")
          print(f"{g:>2} {h:>2} {i:>2}")
      

      Like

  • Unknown's avatar

    Jim Randell 8:44 am on 5 May 2026 Permalink | Reply
    Tags:   

    Teaser 2433: [Date of birth] 

    From The Sunday Times, 10th May 2009 [link]

    I was looking at the dates of birth of three of my friends. When written in the format DDMMYY, each date of birth uses six consecutive digits in some order.

    Today (just taking each of their ages on their last birthday) the difference between Alex’s age and Bernard’s age is 40 or more, and the difference between Alex’s age and Charlie’s age is 14.

    What, in the format DDMMYY, is Charlie’s date of birth?

    This puzzle was originally published with no title.

    [teaser2433]

     
    • Jim Randell's avatar

      Jim Randell 8:45 am on 5 May 2026 Permalink | Reply

      Note that the puzzle was originally set on 10th May 2009.

      The first digit of the month can only be 0 (for Jan – Sep) or 1 (for Oct – Dec).

      So the consecutive digits can only be 0-5 or 1-6. And if they are 1-6 then the month can only be 12 and then it is not possible for there to be a date in the range 01-31 using digits 3-6, so the digits used must be 0-5.

      This Python program runs in 64ms. (Internal runtime is 1.5ms).

      from datetime import date
      from enigma import (irange, subsets, catch, printf)
      
      # date the puzzle was set
      dS = date(2009, 5, 10)
      
      # calculate age
      def age(dX, dY):
        return (dY.year - dX.year) - ((dY.month, dY.day) < (dX.month, dX.day))
      
      # collect possible dates and ages
      ds = set()
      for (u, v, w, x, y, z) in subsets(irange(0, 5), size=6, select='P'):
        (d, m, y) = (10*u + v, 10*w + x, 1900 + 10*y + z)
        t = catch(date, y, m, d)
        if t is not None:
          ds.add((t, age(t, dS)))
      
      ss = set()
      # consider DOB for A
      for (dA, aA) in ds:
        # choose DOB for C
        for (dC, aC) in ds:
          if not (abs(aA - aC) == 14): continue
          # chose DOB for B
          for (dB, aB) in ds:
            if not (abs(aA - aB) >= 40): continue
            printf("[A={dA} ({aA}); B={dB} ({aB}); C={dC} ({aC})]")
            ss.add(dC)
      
      # output solutions
      for dC in ss:
        printf("C = {dC:%d%m%y} [{dC}]")
      

      Solution: Charlie’s date of birth is 250341 (25th March 1941).

      So Charlie was 68 when the puzzle was set.

      Alex was born in 1954, and his date of birth (and age) is one of the following:

      23/10/54 (54)
      03/12/54 (54)
      30/12/54 (54)

      Whichever it is his age is 54 and he is 14 years younger than Charlie.

      Bernard’s date of birth (and age) is one of the following:

      25/04/13 (96)
      24/05/13 (95)
      25/03/14 (95)
      23/05/14 (94)
      24/03/15 (94)
      23/04/15 (94)

      And he is 94, 95, or 96.

      But whichever it is he is at least 40 years older than Alex.


      Note: The puzzle only works when set between 25th March 2009 and 22nd May 2009.

      Like

  • Unknown's avatar

    Jim Randell 7:57 am on 3 May 2026 Permalink | Reply
    Tags:   

    Teaser 3319: Watch out! 

    From The Sunday Times, 3rd May 2026 [link] [link]

    One January a few years ago I was given a new watch that also displayed the date. It did this by turning two wheels, one with 0-3 on it and the other with 0-9. So, if left running, it displayed in turn 00, 01, 02, … 29, 30, 31, … 39, 00, 01, … . Therefore it needed resetting each month, but I never bothered. The date was correctly displayed when I received the watch but then was incorrect for every other month of that year. However, on my birthday that year the display showed an odd number that was in fact the correct date but with the digits reversed! I had to console myself with the fact that, before the watch was 30 years old, on one occasion my birthday would be correctly displayed!

    When is my birthday (day and month)?

    [teaser3319]

     
    • Jim Randell's avatar

      Jim Randell 8:13 am on 3 May 2026 Permalink | Reply

      We only need to consider 4 candidate start years (a leap year, +1, +2, +3).

      This Python program runs in 63ms. (Internal runtime is 755µs).

      from datetime import (date, timedelta)
      from enigma import (irange, repeat, inc, nrev, printf)
      
      # start in year y
      def solve(y):
        # look for possible birthdays in the first year
        bds = set()
        # consider odd displays
        n = 1
        for t in repeat(inc(timedelta(days=2)), date(y, 1, 1)):
          if t.year > y: break
          # display must be incorrect for months other than January
          if n == t.day:
            if t.month > 1: return
          elif n == nrev(t.day, 2):
            # display is the reverse of the day
            bds.add((t, n))
          # advance the display
          n = (n + 2) % 40
      
        # consider birthdays up to 29 years later
        for (t, n) in bds:
          for i in irange(1, 29):
            t1 = t.replace(year=y + i)
            n1 = (n + (t1 - t).days) % 40
            if n1 == t1.day:
              printf("{t} ({n:02d}) -> {t1} ({n1:02d}) [+{i} years]")
      
      # consider 4 candidate years ("a few years ago")
      for y in [2020, 2021, 2022, 2023]:
        solve(y)
      

      Solution: Your birthday is 30th August.

      The setter was given the watch in a leap year (2020 for example), when it showed the correct date for January (but not in any other months).

      On 2020-08-30 the watch displays “03” (instead of “30”). And 28 years later on 2048-08-30 the watch will display “30”.

      There are other birthdays in the first year when the watch will display a date that is incorrect, but the actual date reversed:

      10th February (displays “01” in any year; displays “10” after 55 years)
      10th June (displays “01” in a non-leap year)
      31st July (displays “13” in a leap year; displays “31” after 34 years)

      Like

    • Ruud's avatar

      Ruud 12:57 pm on 3 May 2026 Permalink | Reply

      import datetime
      import istr
      import calendar
      
      istr.int_format("02")
      
      
      def display_time(start_date, year, month, day):
          diff = (datetime.datetime(year, month, day) - start_date).days
          return istr((diff + 1) % 40)
      
      
      for year in (2021, 2022, 2023, 2024):
          start_date = datetime.datetime(year, 1, 1)
          for month in range(2, 13):
              if display_time(start_date, year, month, 1) == 1:
                  break
          else:
              for month in range(1, 13):
                  for day in range(1, calendar.monthrange(year, month)[1]):
                      displayed = display_time(start_date, year, month, day)
                      if displayed.is_odd() and day == displayed.reversed() and displayed.reversed() != displayed:
                          correct_years = [year for year in range(year, year + 30) if display_time(start_date, year, month, day) == day]
                          if len(correct_years) == 1:
                              print(year, correct_years[0], month, day)
      

      Like

      • Jim Randell's avatar

        Jim Randell 4:38 pm on 3 May 2026 Permalink | Reply

        @Ruud: Python’s [[ range(a, b) ]] builtin excludes the final value (b in this case). So your code at line 20 will miss checking the final day of the month.

        The enigma.py library has the [[ irange(a, b) ]] function, if you want to include both endpoints.

        Like

        • Ruud's avatar

          Ruud 4:47 pm on 3 May 2026 Permalink | Reply

          Well spotted. It should read

          import datetime
          import istr
          import calendar
          
          istr.int_format("02")
          
          
          def display_time(start_date, year, month, day):
              diff = (datetime.datetime(year, month, day) - start_date).days
              return istr((diff + 1) % 40)
          
          
          for year in (2021, 2022, 2023, 2024):
              start_date = datetime.datetime(year, 1, 1)
              for month in range(2, 13):
                  if display_time(start_date, year, month, 1) == 1:
                      break
              else:
                  for month in range(1, 13):
                      for day in range(1, calendar.monthrange(year, month)[1] + 1):
                          displayed = display_time(start_date, year, month, day)
                          if displayed.is_odd() and day == displayed.reversed() and displayed.reversed() != displayed:
                              correct_years = [year for year in range(year, year + 30) if display_time(start_date, year, month, day) == day]
                              if len(correct_years) == 1:
                                  print(year, correct_years[0], month, day)
          

          Like

  • Unknown's avatar

    Jim Randell 9:21 am on 1 May 2026 Permalink | Reply
    Tags:   

    Teaser 2445: [Divisible by 9] 

    From The Sunday Times, 2nd August 2009 [link]

    I started with a list of six numbers (with no leading zeros, of course). Five of the six numbers were divisible by 9.

    Then I coded the numbers by consistently using letters for digits, with different letters representing different digits.

    In this way, the list became:

    SETTER
    SENT
    PRETTY
    EASY
    SUNDAY
    TEASER

    Which of those words represented the number that was not divisible by 9?

    This puzzle was originally published with no title.

    [teaser2445]

     
    • Jim Randell's avatar

      Jim Randell 9:22 am on 1 May 2026 Permalink | Reply

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

      It runs in 106ms. (Internal runtime of the generated code is 21ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --distinct="ADENPRSTUY"
      --invalid="0,EPST"
      
      # exactly five of the six numbers are divisible by 9
      # so just 1 of them isn't, identify it as X = 1..6
      --invalid="0|7-9,X"
      
      "(X == 1) == (SETTER % 9 != 0)"
      "(X == 2) == (SENT % 9 != 0)"
      "(X == 3) == (PRETTY % 9 != 0)"
      "(X == 4) == (EASY % 9 != 0)"
      "(X == 5) == (SUNDAY % 9 != 0)"
      "(X == 6) == (TEASER % 9 != 0)"
      
      --answer="X"
      
      --template="(SETTER) (SENT) (PRETTY) (EASY) (SUNDAY) (TEASER) (X)"
      

      Solution: TEASER is not divisible by 9.

      There are 36 ways to assign the digits to the letters, but in all cases TEASER is the odd number out.


      If the puzzle had been set so that just one of the words is not divisibly by 29, then there is a single way to assign digits to the letters.

      Like

      • Ruud's avatar

        Ruud 11:07 am on 1 May 2026 Permalink | Reply

        Brute force reveals that there are 36 different solutions, all resulting in TEASER to be the one that’s not divisible by 9.

        import peek
        import istr
        
        count = 0
        for s, e, t, r, n, p, y, a, u, d in istr.permutations(range(10)):
            if s * p * e * t: # may not start with 0
                not_divisible_by_9 = ""
                for name in "setter sent pretty easy sunday teaser".split():
                    if not istr(f":={name}").is_divisible_by(9):
                        if not_divisible_by_9:
                            break
                        not_divisible_by_9 = name
                else:
                    if not_divisible_by_9:
                        count += 1
                        peek(count, not_divisible_by_9, setter, sent, pretty, easy, sunday, teaser)
        

        Like

  • Unknown's avatar

    Jim Randell 9:04 am on 28 April 2026 Permalink | Reply
    Tags:   

    Brainteaser 1049: A question of colour 

    From The Sunday Times, 5th September 1982 [link]

    A frame at snooker, as every player — and every watcher of “Pot Black” — will know, consists of fifteen red balls, worth one point each, and six “colours”: yellow = 2, green = 3, brown = 4, blue = 5, pink = 6 and black = 7.

    The reds must all be potted first: after each red the player chooses a single colour to pot (immediately replaced on the table) before addressing the next red. Any miss (or potting of the wrong colour) causes the other player to have his turn. After the last red has been potted, with an attempt at a colour, all the colours are on the table, and they must be potted in ascending order of value.

    The game ends when all the colours have been potted, or earlier if the current player has reached a score that cannot be equalled or exceeded by his opponent. For our present purposes, there are no penalty points or snookers.

    Unlike the champions we see on television, Arthur and Bertie recently played a frame which was as  undistinguished in quality (though neither actually incurred any penalty points) as it was remarkable in other ways. Each of them potted the same number of balls; the scores were level immediately before Bertie potted the last of the reds; and the winner — Arthur by a single point —was not decided until the table was finally cleared. They then discovered that Arthur’s score was the lowest possible for such a close win.

    What was Arthur’s score? Which player potted each of the final six “colours” (e.g. B, B, B, B, B, A).

    This puzzle is included in the book The Sunday Times Book of Brainteasers (1994).

    [teaser1049]

     
    • Jim Randell's avatar

      Jim Randell 9:04 am on 28 April 2026 Permalink | Reply

      We are looking for the lowest possible scores.

      The scores are level with 1 red remaining. So 14 of the reds have been potted. For the lowest possible scores the associated colours must have been missed. So each player has potted 7 reds (and no colours) for a total score of 7.

      The remaining balls are: the final red, a possible additional colour, and the 6 colours in order.

      So, if the table is cleared and each player pots the same number of balls: B pots the final red, and a colour, to put him on 8 + x points (and has now potted 9 balls). So of the 6 remaining colours B pots 2 and A pots 4.

      This Python program examines possible ends to the match.

      It runs in 70ms. (Internal runtime is 143µs).

      from enigma import (Enumerator, printf)
      
      colours = [2, 3, 4, 5, 6, 7]
      
      # play the remaining colours
      def play(A, B, cols, As=[], Bs=[]):
        # are we done?
        if not cols:
          yield (A, B, cols, As, Bs)
        else:
          rest = sum(cols)
          # has someone won?
          if A > B + rest or B > A + rest:
            yield (A, B, cols, As, Bs)
          else:
            # someone pots the next colour
            x = cols[0]
            yield from play(A + x, B, cols[1:], As + [x], Bs)
            yield from play(A, B + x, cols[1:], As, Bs + [x])
      
      # solve the puzzle, where B pots the final red with colour <x>
      def solve(x):
        for (A, B, cols, As, Bs) in play(7, 8 + x, colours):
          # A wins by 1 point
          if not (A == B + 1): continue
          # the table is cleared
          if cols: continue
          # A and B potted the same number of balls
          if not (len(As) == 4 and len(Bs) == 2): continue
          # return the final scores and colours potted
          yield (A, B, As, Bs)
      
      # choose a colour for A to pot with the final red
      for x in colours:
        ss = Enumerator(solve(x))
        for (A, B, As, Bs) in ss:
          # output solution
          printf("A pots colours {As}; total = {A} pts")
          printf("B pots final red + {x}; pots colours {Bs}; total = {B} pts")
          printf()
        # we only need the smallest solution
        if ss.count > 0: break
      

      Solution: Arthur’s score was 23. The final six colours were potted as: A, A, A, B, B, A.

      The match started with A and B each potting 7 reds and no colours, and then:

      → A=7, B=7, [1 red remaining]
      B pots the final red and the green (3)
      → A=7, B=11, [27 points remaining]
      A pots the yellow (2)
      → A=9, B=11, [25 points remaining]
      A pots the green (3)
      → A=12, B=11, [22 points remaining]
      A pots the brown (4)
      → A=16, B=11, [18 points remaining]
      B pots the blue (5)
      → A=16, B=16, [13 points remaining]
      B pots the pink (6)
      → A=16, B=22, [7 points remaining]
      A pots the black (7)
      → A=23, B=22 [final scores]

      B has potted 7 + 2 + 2 = 11 balls. And A has potted 7 + 4 = 11 balls.

      Like

  • Unknown's avatar

    Jim Randell 6:24 am on 26 April 2026 Permalink | Reply
    Tags:   

    Teaser 3318: Squaring the circle 

    From The Sunday Times, 26th April 2026 [link] [link]

    Liam is designing a logo as part of a school project. He has drawn a circle that touches all the sides of an octagon. The side lengths of the octagon are all different two-digit numbers of mm; in fact, they are all triangular numbers (i.e. numbers of the type 1+2+3+4+…). Taken together these numbers use all the digits 0 to 9 (at least once each). If I told you the value of one of the lengths that is an odd number, then you would be able to answer the question below.

    What (in mm) is the perimeter of the octagon?

    [teaser3318]

     
    • Jim Randell's avatar

      Jim Randell 7:01 am on 26 April 2026 Permalink | Reply

      I used the characterisation of a tangential polygon given here [@wikipedia].

      Namely:

      If the sides are (a0, …, a7), then a tangential octagon exists if and only if the following equation set has positive real solutions for (x0, …, x7):

      x0 + x1 = a0
      x1 + x2 = a1
      …
      x7 + x0 = a7

      For positive solutions to exist this means:

      x0 > 0
      x0 < a0
      x0 > a0 − a1
      x0 < a0 − a1 + a2
      …
      x0 < a0 − a1 + a2 − a3 + a4 − a5 + a6
      x0 > a0 − a1 + a2 − a3 + a4 − a5 + a6 − a7 = 0

      So we can bracket possible x0 values by constructing the alternating sum of the candidate side lengths, and then look for non-empty intervals at the end of the process. Any x0 value in this interval will give a viable tangential octagon.

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

      from enigma import (
        defaultdict, irange, inf, tri, first, lt, div,
        diff, subsets, cproduct, union, nsplit, printf
      )
      
      # 2-digit triangular numbers
      ns = first((tri(n) for n in irange(1, inf)), count=lt(100), skip=lt(10))
      
      # is there a tangential octagon with sides <ss>?
      def is_tangential(ss):
        xs = None
        # for a valid arrangement, the sum of the even numbered sides
        # is the same as the sum of the odd numbered sides
        s = div(sum(ss), 2)
        if s is None: return
        # choose the even numbered sides
        a0 = min(ss)
        ssx = diff(ss, {a0})
        for ss0 in subsets(ssx, size=3, fn=list):
          if a0 + sum(ss0) != s: continue
          # arrange the sides
          ss1 = diff(ssx, ss0)
          for ((a2, a4, a6), (a1, a3, a5, a7)) in cproduct(subsets(xs, size=len, select='P') for xs in [ss0, ss1]):
            if not (a1 < a7): continue
            # bracket possible values for x0 by constructing the alternating sum of the sides
            lo = max(0, a0 - a1, a0 - a1 + a2 - a3, a0 - a1 + a2 - a3 + a4 - a5)
            hi = min(a0, a0 - a1 + a2, a0 - a1 + a2 - a3 + a4, a0 - a1 + a2 - a3 + a4 - a5 + a6)
            # is the bracket non-empty?
            if lo < hi:
              return (a0, a1, a2, a3, a4, a5, a6, a7)
      
      # map odd side lengths to perimeter
      d = defaultdict(set)
      
      # choose 8 of the numbers
      for ss in subsets(ns, size=8):
        # all 10 digits should be used
        ds = union(nsplit(n) for n in ss)
        if len(ds) < 10: continue
        # is there a tangential octagon?
        rs = is_tangential(ss)
        if rs is None: continue
        # record perimeter by side length
        p = sum(ss)
        printf("[{rs} -> {p}]")
        for n in ss:
          d[n].add(p)
      
      # look for odd side lengths with only one possible perimeter
      for n in sorted(d.keys()):
        vs = d[n]
        if n % 2 == 1 and len(vs) == 1:
          printf("{n} -> {vs}")
      

      Solution: The perimeter of the octagon is 358 mm.

      There are three candidate sets of sides that permit a tangential octagon to be constructed. Example arrangements for these sets are:

      (10, 15, 36, 28, 55, 91, 78, 45) → perimeter = 358
      (10, 21, 36, 28, 45, 55, 91, 78) → perimeter = 364
      (10, 21, 45, 36, 55, 66, 91, 78) → perimeter = 402

      (Note that these are not the only arrangements of the sides that permit a tangential octagon to be constructed. For the first and second set there are 20 possible arrangements, and for the third set there are 24. So not all candidate arrangements are viable).

      15 only appears in the first of these, and is the only odd valued side that uniquely identifies a candidate (the one with perimeter 358).

      There is also one even valued side which only appears in one of the candidates, namely 66, which identifies the candidate with perimeter 402.

      Like

      • Jim Randell's avatar

        Jim Randell 11:13 pm on 26 April 2026 Permalink | Reply

        Here is my program modified to use the [[ find_max() ]] solver from the enigma.py library to ensure the equations have a positive solution, and to find the corresponding tangent lengths, which allows candidate tangential octagons to be constructed.

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

        from enigma import (
          defaultdict, irange, inf, tri, first, lt, div, diff,
          subsets, cproduct, find_max, union, nsplit, printf
        )
        
        # 2-digit triangular numbers
        ns = first((tri(n) for n in irange(1, inf)), count=lt(100), skip=lt(10))
        
        # is there a tangential octagon with sides <ss>?
        def is_tangential(ss):
          # for a valid arrangement, the sum of the even numbered sides
          # is the same as the sum of the odd numbered sides
          s = div(sum(ss), 2)
          if s is None: return
          # choose the even numbered sides
          a0 = min(ss)
          ssx = diff(ss, {a0})
          for ss0 in subsets(ssx, size=3, fn=list):
            if a0 + sum(ss0) != s: continue
            # arrange the sides
            ss1 = diff(ssx, ss0)
            for ((a2, a4, a6), (a1, a3, a5, a7)) in cproduct(subsets(xs, size=len, select='P') for xs in [ss0, ss1]):
              if not (a1 < a7): continue
        
              # can we solve the equations for positive <x> values?
              def solve(x0):
                x1 = a0 - x0
                x2 = a1 - x1
                x3 = a2 - x2
                x4 = a3 - x3
                x5 = a4 - x4
                x6 = a5 - x5
                x7 = a6 - x6
                return min(x0, x1, x2, x3, x4, x5, x6, x7)
        
              # look for the maximum smallest <x> value
              r = find_max(solve, 0, a0)
              if r.fv > 0:
                # return viable solution
                return (a0, a1, a2, a3, a4, a5, a6, a7)
        
        # map odd side lengths to perimeter
        d = defaultdict(set)
        
        # choose 8 of the numbers
        for ss in subsets(ns, size=8):
          # all 10 digits should be used
          ds = union(nsplit(n) for n in ss)
          if len(ds) < 10: continue
          # is there a tangential octagon?
          rs = is_tangential(ss)
          if rs is None: continue
          # record perimeter by side length
          p = sum(ss)
          printf("[{rs} -> {p}]")
          for n in ss:
            d[n].add(p)
        
        # look for odd side lengths with only one possible perimeter
        for n in sorted(d.keys()):
          ps = d[n]
          if n % 2 == 1 and len(ps) == 1:
            printf("{n} -> {ps}")
        

        If the equations admit a positive solution, then there are infinitely many, so the program finds the polygon where the closest tangent point to a vertex is as far as possible.

        We can then use the data to plot viable tangential octagons for candidate arrangements.

        Here are the three viable candidate arrangements, each side of the octagon is shown with its tangent point and side length. (Some of the angles at the vertices are very close to 180°).

        Liked by 2 people

    • Frits's avatar

      Frits 10:59 am on 26 April 2026 Permalink | Reply

      from itertools import combinations
      
      # collect possible triangular side lengths
      n, t, sides = 0, 0, []
      while True:
        t = n * (n + 1) // 2
        if t > 99: break
        if t > 9:
          sides.append(t)
        n += 1
      
      # claim from Google AI:
      # a convex octagon with the sum of its odd-numbered sides' lengths equal to  
      # the sum of its even-numbered sides' lengths has an inscribed circle.
      # thus s1 + s3 + s5 + s7 must be equal to s2 + s4 + s6 + s8
      
      sols = []
      # select eight sides of the octagon
      for c8 in combinations(sides, 8):
        h, r = divmod(sum(c8), 2)
        if r: continue
        # all digits should be present
        if len(set("".join(str(n) for n in c8))) != 10: continue
        # pick four sides s(i) from <c8> with sum <h>
        c1 = c8[0]
        for c3 in combinations(c8[1:], 3):
          if sum(c3) + c1 != h: continue
          sols.append(c8)
          
      # collect possible solutions per odd number
      cands = [[s for s in sols if n in s] for n in range(11, 100, 2)]
      # select odd numbers that only occur in one solution
      sols = [str(sum(vs[0])) for vs in cands if len(vs) == 1]
      print("answer:", ' or '.join(sols))
      

      Liked by 1 person

      • Frits's avatar

        Frits 12:02 pm on 26 April 2026 Permalink | Reply

        Wikipedia mentions that in a tangential polygon with an even number of sides, the sum of the odd numbered sides’ lengths is equal to the sum of the even numbered sides’ lengths. Google AI claims that the reverse is also true (at least for a convex octagonal).

        Like

        • Jim Randell's avatar

          Jim Randell 5:10 pm on 26 April 2026 Permalink | Reply

          @Frits: Yes. I can use that condition to reduce the number of side arrangements that are considered by my program. (Although Wikipedia does not claim it is an “if and only if” condition, and I would require a more trustworthy source than Google AI to verify the sufficiency condition).

          Like

          • Jim Randell's avatar

            Jim Randell 5:33 pm on 26 April 2026 Permalink | Reply

            This paper [link] seems to suggest that the condition is not sufficient for 2n-gons larger than quadrilaterals.

            I think a (2, 2, 2, 2, 100, 3, 3, 100) octagon would be a counterexample.

            Like

            • Frits's avatar

              Frits 7:28 pm on 26 April 2026 Permalink | Reply

              @Jim, Thanks for debunking the claim.
              I haven’t looked at your program yet (beside the use of Matrix.linear).

              Probably you can use your method to check if a sequence of 8 octagon side lengths (with property sum(odd) = sum(even)) has an inscribed circle.

              Liked by 1 person

              • Jim Randell's avatar

                Jim Randell 4:01 pm on 4 May 2026 Permalink | Reply

                Instead of attempting to solve the equations (and looking for non-inconsistent sets) I’ve rejigged my program to determine the interval for x0 where the equations give positive x_i values, and if this interval is non-empty then it is possible to construct a tangential octagon using the given side arrangement.

                [I am yet to see another solution that checks if a candidate arrangement of sides can actually form a tangential octagon].

                Like

    • Brian Gladman's avatar

      Brian Gladman 10:28 pm on 27 April 2026 Permalink | Reply

      @Jim Nice work on the three candidates shapes. The radius of the incircle is 2.A / sum(sides) where A is the polygon’s area but is there a direct route to this radius from the a and x values?

      Like

      • Jim Randell's avatar

        Jim Randell 7:58 am on 28 April 2026 Permalink | Reply

        I took the x values determined by my second program, and for a given radius r you can calculate the angles at each vertex of the octagon.

        You can then vary r until the sum of the vertex angles is 1080° (or 6𝝅 radians).

        I already had a program (written for Teaser 2438) to plot a cyclic polygon (given the angles subtended at the centre by the sides), and then construct a tangential polygon from that.

        So I just needed to calculate the angles at the centre of the octagon, these are supplementary to the angles at the vertices.

        This is the program I used to plot the diagrams:

        from math import (atan2, fsum, pi, degrees)
        from enigma import (find_value, run, arg, printf)
        
        # possible tangents
        xss = {
          1: [6.5, 3.5, 11.5, 24.5, 3.5, 51.5, 39.5, 38.5],  # arrangement 1
          2: [5, 5, 16, 20, 8, 37, 18, 73],  # arrangement 2
          3: [5, 5, 16, 29, 7, 48, 18, 73],  # arrangement 3
        }
        xs = xss[arg(1, 0, int)]
        printf("xs = {xs}")
        
        # calculate vertex angles for radius r
        def angles(r): return list(2 * atan2(r, x) for x in xs)
        
        # find radius that gives an angle sum of 6.pi
        r = find_value((lambda r: fsum(angles(r))), 6 * pi, 0, 1000).v
        printf("inradius = {r}")
        
        # angles at the centre are supplementary to the vertex angles
        args = list(degrees(pi - a) for a in angles(r))
        run("cyclic-polygon.py", *args)
        

        Liked by 1 person

        • Brian Gladman's avatar

          Brian Gladman 8:40 am on 28 April 2026 Permalink | Reply

          Thanks Jim, I hoped that you might have found a direct method to avoid the search for the inradius.

          Like

        • Frits's avatar

          Frits 9:44 am on 28 April 2026 Permalink | Reply

          @Jim, where can we find the program “cyclic-polygon.py”?

          Like

    • Ruud's avatar

      Ruud 7:55 am on 28 April 2026 Permalink | Reply

      Based on @Frits’s solution:

      import istr
      
      sides = [i for i in istr.range(10, 100) if i.is_triangular()]
      
      from itertools import combinations
      
      occurs = {istr(i): [] for i in range(10, 100)}
      sides = [i for i in istr.range(10, 100) if i.is_triangular()]
      for sel_sides in istr.combinations(sides, 8):
          if len(set(istr.join(sel_sides))) == 10:
              for sides3 in istr.combinations(sel_sides[1:], 3):
                  if (sel_sides[0] + sum(sides3)) * 2 == sum(sel_sides):
                      for i in sel_sides:
                          occurs[i].append(sum(sel_sides))
      
      for i, sum_sides in occurs.items():
          if i.is_odd() and len(sum_sides) == 1:
              print(*sum_sides)
      

      Like

  • Unknown's avatar

    Jim Randell 7:14 am on 24 April 2026 Permalink | Reply
    Tags: ,   

    Teaser 2444: [Pat’s lawn] 

    From The Sunday Times, 26th July 2009 [link]

    Pat’s lawn is a 600 sq metre rectangle with a tree at each corner. Working around the perimeter, these are apple, beech, cherry and damson. Pat has marked four straight lines on the grass: one from the apple to a point 75% of the way from the beech to the cherry; the second from the beech to 75% of the way from the cherry to the damson; the third from the cherry to 75% of the way from the damson to the apple; the fourth from the damson to 75% of the way from the apple to the beech. He plans to turn the area enclosed by the four lines into a rose garden.

    What will the rose garden’s area be?

    This puzzle was originally published with no title.

    [teaser2444]

     
    • Jim Randell's avatar

      Jim Randell 7:15 am on 24 April 2026 Permalink | Reply

      This is similar to Routh’s Theorem but in a rectangle instead of a triangle. (See: Teaser 3021).

      If the lines are drawn to a fraction f along the appropriate side, then the area of the central quadrilateral is (as a fraction of the overall parallelogram):

      A = (1 − f)² / (1 + f²)

      Specifically if f is represented as a fraction a/b we get:

      A = (b − a)² / (a² + b²)

      And in our case a = 3, b = 4, hence:

      A = (4 − 3)² / (3² + 4²)
      A = 1/25

      from enigma import (Rational, sq, printf)
      
      Q = Rational()
      
      # fraction along the sides
      f = Q(75, 100)
      printf("[f = {f}]")
      
      # calculate the area of the central quadrilateral
      A = Q(sq(1 - f), 1 + sq(f))
      printf("[A = {A}]")
      
      # output solution (area of central region)
      R = A * 600
      printf("area = {R} sq m")
      

      Solution: The area of the rose garden is 24 sq m.

      Like

  • Unknown's avatar

    Jim Randell 8:13 am on 22 April 2026 Permalink | Reply
    Tags: by: Lachlan MacKinnon,   

    Teaser 2447: [Number grid] 

    From The Sunday Times, 16th August 2009 [link]

    In this variation on a sudoku theme, your task is to find a particular 5-by-5 array of digits in which each row and each column use the same five consecutive digits. When completed, you can read five 5-digit numbers across the rows and a further five down the columns. These 10 numbers should all be different, and their product should be divisible by the fourth powers of 11, 10 and 9. The product should also be divisible by the fourth (but not the fifth) power of 8.

    What are the lowest and highest of your 10 numbers?

    This puzzle was originally published with no title.

    [teaser2447]

     
    • Jim Randell's avatar

      Jim Randell 8:14 am on 22 April 2026 Permalink | Reply

      The overall product is divisible by 9^4 (= 3^8), so at least one of the numbers is divisible by 3, and as the numbers are rearrangements of each other, they must all be divisible by 3. (And so the final product will have a divisor of at least 3^10).

      We can fill out a grid using the digits 0-4 (allowing leading zeros), and than add the same value (between 0 and 5) to each digit to form a candidate grid.

      The sum of the digits 0-4 is 10, and if this combines with 5 copies of the additional digit to give a multiple of 3 then the additional digit can only be 1 or 4.

      This is the approach taken with the following run file, using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It executes in 661ms. (Internal runtime of the generated code is 448ms (using PyPy 7.3.21)).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C D E
      #  F G H I J
      #  K L M N O
      #  P Q R S T
      #  U V W X Y
      #
      # and each number is increased by: ZZZZZ
      
      --macro="@rows = ABCDE, FGHIJ, KLMNO, PQRST, UVWXY"
      --macro="@cols = AFKPU, BGLQV, CHMRW, DINSX, EJOTY"
      --macro="@nums = [@rows, @cols]"
      
      # rows and cols are rearrangements of the same 5 digits
      --distinct="ABCDE,FGHIJ,KLMNO,PQRST,UVWXY,AFKPU,BGLQV,CHMRW,DINSX,EJOTY"
      
      # numbers are formed from 5 consecutive digits
      # so the numbers in the grid are formed from 0-4
      # and the base (Z) is 1 or 4
      --digits="0-4"
      --invalid="0|2|3,Z"
      
      # the numbers are all different
      "seq_all_different(@nums)"
      
      # check divisibility
      --code="""
      D = mlcm(8**4, 9**4, 10**4, 11**4)  # product is divisible by D
      X = 8**5  # but not X
      def check(ns, b=0):
        p = multiply(n + b for n in ns)
        return (p % D == 0) and (p % X != 0)
      """
      "check(@nums, ZZZZZ)"
      
      # remove duplication
      "ABCDE < AFKPU"
      
      --template=""
      --denest=-30  # CPython workaround
      

      Solution: The lowest of the numbers is: 45768. And the highest is: 86547.

      The prime factors are:

      74856 = (2^3)(3)(3119)
      68475 = (3)(5^2)(11)(83)
      86547 = (3)(17)(1697)
      57684 = (2^2)(3)(11)(19)(23)
      45768 = (2^3)(3)(1907)

      76854 = (2)(3)(12809)
      48675 = (3)(5^2)(11)(59)
      84567 = (3)(7)(4027)
      57486 = (2)(3)(11)(13)(67)
      65748 = (2^2)(3)(5479)

      These combine to give an overall product with the following repeated prime factors:

      (2^12)(3^10)(5^4)(11^4)


      In the published solution the lowest number was given as 45678, but this is likely to have been a typo.

      Like

  • Unknown's avatar

    Jim Randell 6:32 am on 19 April 2026 Permalink | Reply
    Tags:   

    Teaser 3317: Hitting all the spots 

    From The Sunday Times, 19th April 2026 [link] [link]

    A board game uses three coloured tetrahedral dice (red, green and blue) to determine which numbers are hit when the three dice are rolled. The 12 faces of the three dice each have different numbers, comprising a 1 and the primes from 2 to 31. A hit is made when it is the number rolled on any of the dice, the sum of any two numbers rolled or the sum of all three numbers rolled.

    The arrangement of the numbers on each die made it possible to hit every number in a range and this range was as large as it could be. The green die has numbers 2, 5, 7, 11 and the blue die also has three consecutive primes.

    In ascending order, what are the numbers on the red die?

    [teaser3317]

     
    • Jim Randell's avatar

      Jim Randell 6:32 am on 19 April 2026 Permalink | Reply

      The most interesting part was writing the [[ maxcss() ]] function to find the maximum continuous subsequence of consecutive integers contained in the parent sequence.

      This Python program runs in 70ms. (Internal runtime is 3.3ms).

      from enigma import (
        Accumulator, primes, irange, tuples, trim,
        diff, cproduct, union, subsets, printf
      )
      
      # calculate the hits for a set of dice
      def hits(ds):
        # sum of any (non-empty) subset of a throw
        hs = union(subsets(vs, min_size=1, fn=sum) for vs in cproduct(ds))
        # return as a sorted list
        return sorted(hs)
      
      # generate possible sets of dice
      def generate():
        # numbers used on the dice
        ns = list(primes.between(2, 31))
        ns.insert(0, 1)
      
        # green die
        G = {2, 5, 7, 11}
      
        # choose 3 consecutive primes for the blue die
        for b3 in tuples(trim(ns, head=6), 3, fn=set):
          rs = diff(ns, G, b3)
          # and one of the remaining numbers
          for bx in subsets(rs, size=1, fn=set):
            B = b3.union(bx)
            # red die is what's left
            R = diff(rs, bx, fn=set)
            yield (G, B, R)
      
      # find the largest continuous subsequence (length >= m)
      # in the list of integers <vs>, the list should be strictly increasing
      # return (<max-length>, (<lowest-value>, <highest-value>))
      def maxcss(vs, m=0):
        r = Accumulator(fn=max, value=m)
        # consider possible starts
        n = len(vs)
        for (i, v) in enumerate(vs):
          if i + r.value > n: break
          for j in irange(n - 1, i + r.value, step=-1):
            if vs[j] - v == j - i:
              r.accumulate_data(j - i + 1, i)
              break
        if r.data is None: return
        (k, i) = (r.value, r.data)
        return (k, (vs[i], vs[i + k - 1]))
      
      # find maximal length range of hits
      r = Accumulator(fn=max, value=1, collect=1)
      for ds in generate():
        hs = hits(ds)
        z = maxcss(hs, r.value)
        if z is None: continue
        (n, (a, b)) = z
        r.accumulate_data(n, ds)
      
      # output solution
      printf("max range = {r.value}")
      for (G, B, R) in r.data:
        printf("-> G={G} B={B} R={R}", G=sorted(G), B=sorted(B), R=sorted(R))
      

      Solution: The numbers on the red die are: 1, 13, 17, 31.

      The dice are:

      Green = (2, 5, 7, 11)
      Blue = (3, 19, 23, 29)
      Red = (1, 13, 17, 31)

      All hits from 1 – 57 can be made. (58 is the first number that cannot be hit using these dice).

      In fact, using these dice we can make: [1 – 57; 59 – 62; 65; 67; 71].

      Like

      • Jim Randell's avatar

        Jim Randell 4:51 pm on 19 April 2026 Permalink | Reply

        Here is a simpler implementation of [[ maxcss() ]], that just returns the length of a maximal subsequence:

        def maxcss(vs):
          if not vs: return 0
          return max(map(len, clump(v - i for (i, v) in enumerate(vs))))
        

        We calculate a derived sequence by subtracting the index of each element of the original sequence from the value of that element. We then look for a maximal length run of equal values in the derived sequence, and this corresponds to a maximal length continuous subsequence of consecutive integers in the original sequence.

        [I am posting this code because although I have seen several other solutions posted for this puzzle, none of them have managed to implement this logic correctly].

        Like

      • Ruud's avatar

        Ruud 5:10 am on 20 April 2026 Permalink | Reply

        @Frits
        Now, I see what you mean. i had interpreted the problem wrongly.
        Here’s a revised version:

        import itertools
        
        max_max_length = 0
        faces = [n for n in range(1, 32) if all(n % i for i in range(2, int(n**0.5) + 1))]  # 1, 2, 3, ...,31
        green = {2, 5, 7, 11}
        for primes3 in zip(faces[0:], faces[1:], faces[2:]):
            if not any(x in green for x in primes3):
                for prime4 in set(faces) - green - {primes3}:
                    blue = {*primes3, prime4}
                    red = set(faces) - green - blue
                    hits = sorted({sum(x) for a, b, c in itertools.product(green, blue, red) for x in itertools.combinations((0, 0, a, b, c), 3)})
                    max_length = 0
                    length = 1
                    for d0, d1 in zip(hits, hits[1:] + [0]):
                        if d1 - d0 == 1:
                            length += 1
                        else:
                            max_length = max(max_length, length)
                            length = 1
                    if max_length >= max_max_length:
                        if max_length > max_max_length:
                            solutions = []
                            max_max_length = max_length
                        solutions.append((green, blue, red))
        print("maximum range = ", max_max_length)
        for solution in solutions:
            for i, color in enumerate("green blue red".split()):
                print(color, sorted(solution[i]))
        

        Like

    • Ruud's avatar

      Ruud 10:14 am on 19 April 2026 Permalink | Reply

      import itertools
      
      max_max_hit = 0
      faces = [n for n in range(1, 32) if all(n % i for i in range(2, int(n**0.5) + 1))]  # 1, 2, 3, ...,31
      green = {2, 5, 7, 11}
      for primes3 in zip(faces[0:], faces[1:], faces[2:]):
          if not any(x in green for x in primes3):
              for prime4 in set(faces) - green - {primes3}:
                  blue = {*primes3, prime4}
                  red = set(faces) - green - blue
                  hits = {sum(x) for a, b, c in itertools.product(green, blue, red) for x in itertools.combinations((0, 0, a, b, c), 3)}
                  for max_hit in itertools.count():
                      if max_hit + 1 not in hits:
                          break
      
                  if max_hit >= max_max_hit:
                      if max_hit > max_max_hit:
                          solutions = []
                          max_max_hit = max_hit
                      solutions.append((green, blue, red))
      print("maximum range = ", max_max_hit)
      for solution in solutions:
          for i, color in enumerate("green blue red".split()):
              print(color, sorted(solution[i]))
      

      Like

      • Frits's avatar

        Frits 11:26 am on 19 April 2026 Permalink | Reply

        @Ruud, why do you assume the range has to start with 1?

        Like

        • Ruud's avatar

          Ruud 3:18 pm on 19 April 2026 Permalink | Reply

          Because 1 is also one of the numbers on the dice. So faces is really 1, 2, 3, …, 31 .

          Like

          • Frits's avatar

            Frits 9:32 pm on 19 April 2026 Permalink | Reply

            @Ruud,

            The collection of hits might be (if 1 and 3 belong to the same colour):

              
            [1, 2, 3, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 55, 57, 59, 61]
            

            In this case the biggest range isn’t 3 but 37 (17-53) and number 1 isn’t included in this range.

            Like

    • Ellen Napier's avatar

      Ellen Napier 1:21 pm on 20 April 2026 Permalink | Reply

      I found two different answers to this puzzle.

      Like

      • Jim Randell's avatar

        Jim Randell 6:53 am on 21 April 2026 Permalink | Reply

        @Ellen: I found there were 20 possible sets of dice. And one of them gave the single longest continuous subsequence of consecutive hits.

        However there are two sets which give the second longest continuous subsequence of consecutive hits.

        Like

    • Ellen Napier's avatar

      Ellen Napier 11:13 pm on 21 April 2026 Permalink | Reply

      Got it, thanks. Had a sorting error.

      Like

  • Unknown's avatar

    Jim Randell 7:56 am on 17 April 2026 Permalink | Reply
    Tags:   

    Teaser 2448: [Contractors] 

    From The Sunday Times, 23rd August 2009 [link]

    Zachary, a contractor, wants to place a stone wall round a new development. He knows he can hire two masons for as long as it takes: Angus, who could do the job on his own in 21 days, and Bruce who could complete it on his own in 28 days. Together, they could finish the job in a certain number of days, but Zachary would like it done in three fewer days. So, as well as hiring Angus and Bruce for the whole time, he hires Chuck, who could do it on his own in 24 days, for the few days necessary to achieve his aim.

    For how many days does he employ Chuck?

    This puzzle was originally published with no title.

    [teaser2448]

     
    • Jim Randell's avatar

      Jim Randell 7:57 am on 17 April 2026 Permalink | Reply

      If we say there is 1 unit of work to do, then:

      A does 1/21 units of work per day
      B does 1/28 units of work per day
      C does 1/24 units of work per day

      We can calculate how long it would take A + B to do the job by combining their work rates:

      1/21 + 1/28 = 1/12

      So together A + B could complete the job in 12 days.

      But Z wants the work to be completed in 9 days.

      In 9 days A + B can do 9/12 (= 3/4) of the job, leaving C to do the remaining 1/4.

      And: 1/4 = 6/24, so C would have to work for 6 days.

      Solution: Chuck works for 6 days.


      This Python program runs in 73ms. (Internal runtime is 64µs).

      from enigma import (Rational, as_int, printf)
      
      Q = Rational()
      
      # work rates for A, B, C
      (A, B, C) = (Q(1, x) for x in [21, 28, 24])
      
      # calculate number of days for A and B together
      d = as_int(Q(1, A + B))
      printf("A + B = {d} days")
      
      # target number of days
      t = d - 3
      printf("target = {t} days")
      
      # calculate number of extra days = x
      # (A + B) * 9 + C * x = 1
      # => x = (1 - 9(A + B)) / C
      x = Q(1 - 9 * (A + B), C)
      printf("C works for {x} days")
      

      Like

  • Unknown's avatar

    Jim Randell 1:21 pm on 15 April 2026 Permalink | Reply
    Tags:   

    Teaser 2440: [Safe combination] 

    From The Sunday Times, 28th June 2009 [link]

    George and Martha’s safe was broken into. The combination had six digits: they remembered it as two three-figure numbers, the second being the first multiplied by their single-digit house number. All seven digits were different and non-zero. The burglar knew these facts and worked out the code. Their new combination has eight digits, remembered as four-figure numbers, the second being the first multiplied by their lucky digit. All nine digits are different and non-zero. The last three digits are the same as in the original.

    What is their new combination?

    This puzzle was originally published with no title.

    [teaser2440]

     
    • Jim Randell's avatar

      Jim Randell 1:23 pm on 15 April 2026 Permalink | Reply

      We don’t know the house number, but the burglar does, and we can look for situations where knowing the house number allows you to work out a unique combination.

      The following Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library to generate candidate combinations are house numbers, and then uses the [[ filter_unique() ]] function to identify viable situations.

      We can then look for possible second combinations where the final 3 digits are the same as those in the first combination.

      It runs in 74ms. (Internal runtime is 7.2ms).

      from enigma import (SubstitutedExpression, irange, filter_unique, item, nsplit, printf)
      
      # suppose the first combination is: ABC DEF, and the house number G
      p1 = SubstitutedExpression(
        [ "{ABC} * {G} = {DEF}" ],
        digits=irange(1, 9),
        answer="({ABC}, {DEF}, {G})",
      )
      
      # knowing the house number, the burglar could work out the code
      rs = filter_unique(p1.answers(verbose=0), f=item(2))
      for (ABC, DEF, G) in rs.unique:
      
        # find the second combination: HIJK LMNP, and the lucky number Q
        # and the last 3 digits are the same as in the first combination
        p2 = SubstitutedExpression(
          [ "{HIJK} * {Q} = {LMNP}" ],
          digits=irange(1, 9),
          s2d=dict(zip("MNP", nsplit(DEF))),
          answer="({HIJK}, {LMNP}, {Q})",
        )
      
        for (HIJK, LMNP, Q) in p2.answers(verbose=0):
          # output solution
          printf("code 1 = {ABC} {DEF} (house = {G}); code 2 = {HIJK} {LMNP} (lucky = {Q})")
      

      Solution: The new combination is: 1738 6952.

      And their lucky number is 4.

      1738 × 4 = 6952

      The original code was: 136 952. And the house number is 7.

      136 × 7 = 952

      There is another candidate for the old combination that can be determined knowing the house number. This is: 157 942 (house number = 6), but this does not permit a second combination to be made sharing the final 3 digits.

      Like

    • Ruud's avatar

      Ruud 2:30 pm on 15 April 2026 Permalink | Reply

      import peek
      import istr
      
      collect = {i: [] for i in istr.range(1, 10)}
      for digit, f0, f1, f2 in istr.permutations(range(1, 10), 4):
          first = f0 | f1 | f2
          second = first * digit
          if len(second) == 3 and (first | second | digit).all_distinct():
              collect[digit].append(second)
      for digit, second3s in collect.items():
          if len(second3s) == 1:
              house_number = digit
              second3 = second3s[0]
      
      for lucky_digit, f0, f1, f2, f3 in istr.permutations(range(1, 10), 5):
          first = f0 | f1 | f2 | f3
          second = first * lucky_digit
          if len(second) == 4 and (first | second | lucky_digit).all_distinct():
              if second[1:] == second3:
                  peek(first, second, lucky_digit, house_number)
      

      Like

      • Jim Randell's avatar

        Jim Randell 3:41 pm on 15 April 2026 Permalink | Reply

        @Ruud: You need to ensure: “All seven digits were different and non-zero”.

        Like

        • Ruud's avatar

          Ruud 3:49 pm on 15 April 2026 Permalink | Reply

          @Jim
          Yes, I missed the non zero condition. This one does:

          import peek
          import istr
          
          collect = {i: [] for i in istr.range(1, 10)}
          for digit, f0, f1, f2 in istr.permutations(range(1, 10), 4):
              first = f0 | f1 | f2
              second = first * digit
              if len(second) == 3 and not '0' in second and (first | second | digit).all_distinct():
                  collect[digit].append(second)
          for digit, second3s in collect.items():
              if len(second3s) == 1:
                  house_number = digit
                  second3 = second3s[0]
          
          for lucky_digit, f0, f1, f2, f3 in istr.permutations(range(1, 10), 5):
              first = f0 | f1 | f2 | f3
              second = first * lucky_digit
              if len(second) == 4 and not '0' in second and (first | second | lucky_digit).all_distinct():
                  if second[1:] == second3:
                      peek(first, second, lucky_digit, house_number)
          

          Like

        • Jim Randell's avatar

          Jim Randell 4:53 pm on 15 April 2026 Permalink | Reply

          And I think the final loop (lines 15-20) should fall under the if statement at line 11, otherwise you are only checking the final candidate second3 to be found (or the program will abort if none are found).

          Like

          • Ruud's avatar

            Ruud 4:40 am on 16 April 2026 Permalink | Reply

            Ok.

            import peek
            import istr
            
            collect = {i: [] for i in istr.range(1, 10)}
            for digit, f0, f1, f2 in istr.permutations(range(1, 10), 4):
                first = f0 | f1 | f2
                second = first * digit
                if len(second) == 3 and not '0' in second and (first | second | digit).all_distinct():
                    collect[digit].append(second)
            
            for digit, second3s in collect.items():
                if len(second3s) == 1:
                    house_number = digit
                    second3 = second3s[0]
            
                    for lucky_digit, f0, f1, f2, f3 in istr.permutations(range(1, 10), 5):
                        first = f0 | f1 | f2 | f3
                        second = first * lucky_digit
                        if len(second) == 4 and not '0' in second and (first | second | lucky_digit).all_distinct():
                            if second[1:] == second3:
                                peek(first, second, lucky_digit, house_number)
            

            Like

  • Unknown's avatar

    Jim Randell 6:42 am on 12 April 2026 Permalink | Reply
    Tags:   

    Teaser 3316: The odd one out 

    From The Sunday Times, 12th April 2026 [link] [link]

    George drew a 3 × 3 grid and wrote an odd digit in each square. Martha did the same and her grid was identical to George’s except for the digit in one square. Both grids contained eight different three-digit prime numbers, reading across the three rows, down the three columns and down the two diagonals. Remarkably, all eight of George’s primes were also primes when read backwards but only seven of Martha’s primes had this property.

    Which prime number in Martha’s grid was the odd one out?

    [teaser3316]

     
    • Jim Randell's avatar

      Jim Randell 7:33 am on 12 April 2026 Permalink | Reply

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

      It runs in 134ms. (Internal runtime of the generated code is 41ms).

      Run: [@codepad]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # use a prime sieve to test for 3-digit primes
      --code="is_prime = primes.is_prime"
      --code="primes.expand(999)"
      
      # grids are:
      #
      # George = A B C   Martha = J K L
      #          D E F            M N P
      #          G H I            Q R S
      #
      # and they differ in one position
      
      # cell values are not distinct
      --distinct=""
      # but are all odd
      --digits="1,3,5,7,9"
      
      # for George's grid:
      # the grid forms 8 different bi-directional primes:
      # rows
      "is_prime(ABC)" "is_prime(CBA)"
      "is_prime(DEF)" "is_prime(FED)"
      "is_prime(GHI)" "is_prime(IHG)"
      # cols
      "is_prime(ADG)" "is_prime(GDA)"
      "is_prime(BEH)" "is_prime(HEB)"
      "is_prime(CFI)" "is_prime(IFC)"
      # diagonals
      "is_prime(AEI)" "is_prime(IEA)"
      "is_prime(CEG)" "is_prime(GEC)"
      # different
      "all_different(ABC, DEF, GHI, ADG, BEH, CFI, AEI, CEG)"
      
      # for Martha's grid:
      # it differs from George's in exactly 1 position
      "sum(x != y for (x, y) in zip([A, B, C, D, E, F, G, H, I], [J, K, L, M, N, P, Q, R, S])) == 1"
      
      # the grid forms 8 different primes:
      # rows
      "is_prime(JKL)"
      "is_prime(MNP)"
      "is_prime(QRS)"
      # cols
      "is_prime(JMQ)"
      "is_prime(KNR)"
      "is_prime(LPS)"
      # diagonals
      "is_prime(JNS)"
      "is_prime(LNQ)"
      # different
      "all_different(JKL, MNP, QRS, JMQ, KNR, LPS, JNS, LNQ)"
      
      # exactly 7 of them are primes when reversed
      --macro="@Mrevs = (LKJ, PNM, SRQ, QMJ, RNK, SPL, SNJ, QNL)"
      "sum(is_prime(x) for x in @Mrevs) == 7"
      
      # answer is the one that is not a prime when reversed
      --code="ans = lambda ns: singleton(nrev(n) for n in ns if not is_prime(n))"
      --answer="ans(@Mrevs)"
      
      --template=""
      

      Solution: The prime number in Martha’s grid that is not a prime when reversed is 173.

      As: 173 is prime, but 371 = 7 × 53.

      Here are the grids:

      George:    Martha:
      [ 1 7 9 ]  [ 1 7 9 ]
      [ 1 5 7 ]  [ 7 5 7 ]
      [ 3 1 1 ]  [ 3 1 1 ]

      The numbers in the grids are:

      George: 179, 157, 311, 113, 751, 971, 151, 953 (all bi-directional primes)
      Martha: 179, 757, 311, 173, 751, 971, 151, 953 (all bi-directional primes except 173)

      We can get a second set of grids by writing the rows of these grids as columns and vice versa.

      Like

      • Jim Randell's avatar

        Jim Randell 2:51 pm on 12 April 2026 Permalink | Reply

        Splitting out the condition that George and Martha’s grids differ in exactly one position, into multiple expressions, allows us to get a run-file with an internal run time of just 1.8ms.

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # use a prime sieve to test for 3-digit primes
        --code="is_prime = primes.is_prime"
        --code="primes.expand(999)"
        
        # grids are:
        #
        # George = A B C   Martha = J K L
        #          D E F            M N P
        #          G H I            Q R S
        #
        # and they differ in position X (0 - 8)
        
        # cell values are not distinct
        --distinct=""
        # but are all odd
        --invalid="0|2|4|6|8,ABCDEFGHIJKLMNPQRS"
        # changed cell position is 0-8
        --invalid="9|X"
        
        # for George's grid:
        # the grid forms 8 different bi-directional primes:
        # rows
        "is_prime(ABC)" "is_prime(CBA)"
        "is_prime(DEF)" "is_prime(FED)"
        "is_prime(GHI)" "is_prime(IHG)"
        # cols
        "is_prime(ADG)" "is_prime(GDA)"
        "is_prime(BEH)" "is_prime(HEB)"
        "is_prime(CFI)" "is_prime(IFC)"
        # diagonals
        "is_prime(AEI)" "is_prime(IEA)"
        "is_prime(CEG)" "is_prime(GEC)"
        # different
        "all_different(ABC, DEF, GHI, ADG, BEH, CFI, AEI, CEG)"
        
        # for Martha's grid:
        # it differs from George's in exactly 1 position (position = X)
        "(X == 0) == (A != J)"
        "(X == 1) == (B != K)"
        "(X == 2) == (C != L)"
        "(X == 3) == (D != M)"
        "(X == 4) == (E != N)"
        "(X == 5) == (F != P)"
        "(X == 6) == (G != Q)"
        "(X == 7) == (H != R)"
        "(X == 8) == (I != S)"
        
        # the grid forms 8 different primes:
        # rows
        "is_prime(JKL)"
        "is_prime(MNP)"
        "is_prime(QRS)"
        # cols
        "is_prime(JMQ)"
        "is_prime(KNR)"
        "is_prime(LPS)"
        # diagonals
        "is_prime(JNS)"
        "is_prime(LNQ)"
        # different
        "all_different(JKL, MNP, QRS, JMQ, KNR, LPS, JNS, LNQ)"
        
        # exactly 7 of them are primes when reversed
        --macro="@Mrevs = (LKJ, PNM, SRQ, QMJ, RNK, SPL, SNJ, QNL)"
        "sum(is_prime(x) for x in @Mrevs) == 7"
        
        # answer is the one that is not a prime when reversed
        --code="ans = lambda ns: singleton(nrev(n) for n in ns if not is_prime(n))"
        --answer="ans(@Mrevs)"
        
        --template=""
        --denest=-1  # CPython workaround
        

        The run time can be further reduced (to 1.3ms) with the observation that 5 cannot occur at the end of a prime, or at the beginning of a bi-directional prime.

        --invalid="5,ABCDFGHILPQRS"
        

        Liked by 1 person

    • Ruud's avatar

      Ruud 8:31 am on 12 April 2026 Permalink | Reply

      import istr
      
      primes13579 = {i for i in istr.primes(100, 1000) if all(c in [1, 3, 5, 7, 9] for c in i)}
      reversible_primes13579 = {i for i in primes13579 if i.reversed().is_prime()}
      
      
      def grid(number_reversible):
          for rows in istr.permutations(primes13579, 3):
              all8 = list(rows)
              for i in range(3):
                  all8.append(rows[0][i] | rows[1][i] | rows[2][i])
              all8.append(rows[0][0] | rows[1][1] | rows[2][2])
              all8.append(rows[0][2] | rows[1][1] | rows[2][0])
              if len(set(all8)) == 8:  # all different?
                  if all(n in primes13579 for n in all8) and sum(n in reversible_primes13579 for n in all8) == number_reversible:
                      yield all8
      
      
      georges = list(grid(number_reversible=8))
      
      for martha in grid(number_reversible=7):
          for george in georges:
              diff = sum(g != m for g, m in zip(george[0] | george[1] | george[2], martha[0] | martha[1] | martha[2]))
              if diff == 1:
                  for p in martha:
                      if p not in reversible_primes13579:
                          print(p)
                          for g, m in zip(george[:3], martha[:3]):
                              print("    ", g, m)
                          print()
      

      Liked by 1 person

    • GeoffR's avatar

      GeoffR 5:20 pm on 12 April 2026 Permalink | Reply

      
      # ST 3316 by Claude AI
      
      from itertools import product
      
      sieve = bytearray([1]) * 1000
      
      sieve[0] = sieve[1] = 0
      for i in range(2, 32):
        if sieve[i]: sieve[i*i::i] = bytes(len(sieve[i*i::i]))
      
      ODD = (1,3,5,7,9)
      P = {t: sieve[100*t[0]+10*t[1]+t[2]] for t in product(ODD, repeat=3)}
      R = {t: sieve[100*t[2]+10*t[1]+t[0]] for t in product(ODD, repeat=3)}
      E = {t: P[t] and R[t] for t in product(ODD, repeat=3)}
      
      def search(check):
        found = []
        for a,b,c in product(ODD, repeat=3):
          if not check[(a,b,c)]: continue
          for d,e,f in product(ODD, repeat=3):
            if not check[(d,e,f)]: continue
            for g,h,i in product(ODD, repeat=3):
              if (check[(g,h,i)] and check[(a,d,g)] and check[(b,e,h)]
                  and check[(c,f,i)] and check[(a,e,i)] and check[(c,e,g)]):
                nums = {100*a+10*b+c, 100*d+10*e+f, 100*g+10*h+i,
                        100*a+10*d+g, 100*b+10*e+h, 100*c+10*f+i,
                        100*a+10*e+i, 100*c+10*e+g}
                if len(nums) == 8:
                  found.append((a,b,c,d,e,f,g,h,i))
        return found
      
      george = search(E)
      
      seen = set()
      for gg in george:
        for pos in range(9):
          for digit in ODD:
            if digit == gg[pos]: continue
            mg = list(gg); mg[pos] = digit
            a,b,c,d,e,f,g,h,i = mg
            tris = [(a,b,c),(d,e,f),(g,h,i),(a,d,g),(b,e,h),(c,f,i),(a,e,i),(c,e,g)]
            if not all(P[t] for t in tris): continue
            nums = [100*t[0]+10*t[1]+t[2] for t in tris]
            if len(set(nums)) != 8: continue
            flags = [R[t] for t in tris]
            if sum(flags) == 7:
              ans = nums[flags.index(False)]
              if ans not in seen:
                seen.add(ans)
                print(f"Grid: {mg[:3]} / {mg[3:6]} / {mg[6:]}")
                print(f"  Odd prime out: {ans}  (reversal {int(str(ans)[::-1])} is not prime)")
      
      print(f"\nAnswer: {sorted(seen)}")
      
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 6:27 pm on 12 April 2026 Permalink | Reply

        Claude doesn’t seem to believe that comments are useful in code.

        Like

      • Frits's avatar

        Frits 9:08 am on 19 April 2026 Permalink | Reply

        Also a minor issue is that “mg = list(gg)” could/should have been done at a higher level.

        Like

    • GeoffR's avatar

      GeoffR 6:34 pm on 12 April 2026 Permalink | Reply

      It depends on the prompt – I asked Claude AI to make the code short and fast, and it chose to leave the comments out.

      Like

      • Brian Gladman's avatar

        Brian Gladman 3:23 pm on 13 April 2026 Permalink | Reply

        Perhaps you shouldn’t encourage its bad habits!

        Like

    • Frits's avatar

      Frits 7:01 pm on 12 April 2026 Permalink | Reply

      Not considering reflections/rotations.

      from itertools import permutations
      
      # 3-digit primes with odd digits
      P = [3, 5, 7]
      P += [x for x in range(11, 33, 2) if all(x % p for p in P)]
      P = {s for x in range(101, 1000, 2) if all(x % p for p in P) and
           set("13579").issuperset(s := str(x))}
      
      # reversable primes for George
      G = {p for p in P if p[::-1] in P}
        
      # return sequence of 3 rows, 3 columns and 2 diagonals
      def nums8(rs):
        seq = set(''.join(r) for r in rs)
        # add columns
        seq.update(list(''.join(c) for c in zip(*[list(r) for r in rs])))
        # add diagonals
        seq.update((rs[0][0] + rs[1][1] + rs[2][2], rs[0][2] + rs[1][1] + rs[2][0]))
        return seq  
      
      sols = set()
      # select reversable primes for George
      for rs in permutations(G, 3):
        rowsG = [list(r) for r in rs]
        # check the two diagonals for George
        d1, d2 = rs[0][0] + rs[1][1] + rs[2][2], rs[0][2] + rs[1][1] + rs[2][0]
        if any(x not in G for x in (d1, d2)):  continue
        # check the columns for George
        colsG = list(''.join(c) for c in zip(*rowsG))
        if any(c not in G for c in colsG): continue
        # check for 8 different numbers for George
        if len(nums8(rowsG)) != 8: continue
        
        # change one digit in George's grid in cell [r][c]
        for r in range(3):
          for c in range(3):
            rowsM = list(row.copy() for row in rowsG)
            # choose new digit value
            for v in "13579":
              if v == rowsG[r][c]: continue
              rowsM[r][c] = v
              # check for 8 different numbers for Martha
              if len(numsM := nums8(rowsM)) != 8: continue
              # all Martha's 8 numbers must also be prime
              if any(n not in P for n in numsM): continue 
              # count number of reversed numbers not being prime
              np = [x for x in numsM if x[::-1] not in P]
              if len(np) != 1: continue  # seven were prime
              
              sols.add(np[0])
      
      print(f"answer: {' or '.join(sols)}")
      

      Like

  • Unknown's avatar

    Jim Randell 7:56 am on 10 April 2026 Permalink | Reply
    Tags:   

    Teaser 2436: [It’s all Greek…] 

    From The Sunday Times, 31st May 2009 [link]

    In this puzzle I have consistently replaced digits by letters. In this way an addition sum has become:

    ETA + ZETA + THETA = DELTA.

    And the three unused digits form the number SUM.

    Even if I told you the value of Z you, could not work out the values of ETA, ZETA and THETA. But if I now told you whether SUM was prime or not, then you could work out the three summands.

    What are they?

    This puzzle was originally published with no title.

    [teaser2436]

     
    • Jim Randell's avatar

      Jim Randell 7:57 am on 10 April 2026 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to find possible solutions to the alphametic sum, and then uses the [[ filter_unique() ]] function to eliminate non-viable candidates.

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

      from enigma import (SubstitutedExpression, filter_unique, item, unpack, is_prime, printf)
      
      # the alphametic sum
      p = SubstitutedExpression.split_sum(
        ["ETA + ZETA + THETA = DELTA"],
        # record (0 = Z, 1 = SUM, 2 = summands)
        answer="(Z, SUM, (ETA, ZETA, THETA))",
      )
      
      # consider possible solutions, where the value of Z (= item(0))
      # does not uniquely determine the summands (= item(2))
      (f, g) = (item(0), item(2))
      rs = filter_unique(p.answers(verbose=0), f, g).non_unique
      
      # from the remaining candidates, knowing whether SUM (= item(1)) is prime
      # does allow us to determine the summands (= item(2))
      (f, g) = (unpack(lambda Z, SUM, ts: is_prime(SUM)), item(2))
      rs = filter_unique(rs, f, g).unique
      
      # output solution
      for (Z, SUM, terms) in rs:
        printf("Z={Z}, SUM={SUM} [prime={p}] -> (ETA, ZETA, THETA) = {terms}", p=is_prime(SUM))
      

      Solution: The summands are: ETA = 250; ZETA = 8250; THETA = 54250.


      There are 8 possible assignments of digits to letters that solve the alphametic sum:

      Z=2; A=0 D=6 E=1 H=9 L=4 T=5; {S, U, M} = {3, 7, 8}
      
      Z=3; A=0 D=6 E=1 H=8 L=4 T=5; {S, U, M} = {2, 7, 9}
      Z=3; A=0 D=6 E=2 H=9 L=7 T=5; {S, U, M} = {1, 4, 8}
      
      Z=4; A=0 D=6 E=2 H=8 L=7 T=5; {S, U, M} = {1, 3, 9}
      
      Z=8; A=0 D=6 E=1 H=3 L=4 T=5; {S, U, M} = {2, 7, 9}
      Z=8; A=0 D=6 E=2 H=4 L=7 T=5; {S, U, M} = {1, 3, 9}
      
      Z=9; A=0 D=6 E=1 H=2 L=4 T=5; {S, U, M} = {3, 7, 8}
      Z=9; A=0 D=6 E=2 H=3 L=7 T=5; {S, U, M} = {1, 4, 8}

      Z = 2 or 4 would uniquely identify the values in the sum, so these are discarded to leave the 6 ambiguous cases Z = 3, 8, 9.

      Of the possible values for SUM (6 arrangements for each case) only 139 and 193 are prime.

      So, if we are told SUM was not prime, we would not be able to identify the values of the summands.

      But if we are told SUM is prime, we know that the value must be 139 or 193 (so S = 1), and so the only possible candidate solution is:

      Z=8; A=0 D=6 E=2 H=4 L=7 T=5; S=1 {U, M} = {3, 9}

      Hence: Z = 8; ETA = 250; ZETA = 8250; THETA = 54250; DELTA = 62750; SUM = 139 or 193.

      Like

    • Ruud's avatar

      Ruud 2:01 pm on 10 April 2026 Permalink | Reply

      import istr
      
      
      z_collect = {z: [] for z in istr.range(10)}
      for e, t, a, z, h, d, l in istr.permutations(range(10), r=7):
          if (e | t | a) + (z | e | t | a) + (t | h | e | t | a) == d | e | l | t | a:
              z_collect[z].append(istr("=etazhdl"))
      
      for z, v in z_collect.items():
          if len(v) > 1:  # we need at least two so;ution with z=, this can't be the solution
              for s in v:
                  outcomes = {sum_.is_prime() for sum_ in map(istr.join, istr.permutations(set(istr.range(10)) - set(s)))}
                  if len(outcomes) == 2:  # False and True
                      s.decompose("etazhdl")
                      print(f"eta={istr('=eta')} zeta={istr('=zeta')} theta={istr('=theta')} delta={istr('=delta')}")
      

      Like

  • Unknown's avatar

    Jim Randell 10:41 am on 8 April 2026 Permalink | Reply
    Tags: ,   

    Teaser 2438: [Nested quadrilaterals] 

    From The Sunday Times, 14th June 2009 [link]

    Quadrilateral A has its vertices on a circle, and no two angles are the same. Another circle is drawn inside A that touches its sides at four points, and these points form the corners of Quadrilateral B. Then Quadrilateral C is formed inside B in the same way, and so on.

    Quadrilateral K’s angles are whole numbers of degrees.

    What (in increasing order) are Quadrilateral A’s angles?

    This puzzle was originally published with no title.

    [teaser2438]

     
    • Jim Randell's avatar

      Jim Randell 10:42 am on 8 April 2026 Permalink | Reply

      A quadrilateral that has a circumcircle (i.e. a circle that passes through each of the vertices) is called a cyclic quadrilateral.

      A quadrilateral is cyclic if (and only if) opposite internal angles are supplementary (i.e. if the angles at the vertices are (𝛂, 𝛃, 𝛄, 𝛅) we have: 𝛂 + 𝛄 = 𝛃 + 𝛅 = 180°).

      A quadrilateral that has a incircle (i.e. a circle inscribed inside the quadrilateral that touches all four sides) is called a tangential quadrilateral.

      A quadrilateral is tangential if (and only if) opposite sides sum to the same total length (i.e. if the length of the sides are (a, b, c, d) we have: a + c = b + d).

      Note that the condition for a tangential quadrilateral is more restrictive than the condition for a cyclic quadrilateral. For example, all rectangles (all internal angles = 90°) are cyclic, but only squares are tangential.

      A quadrilateral that is both cyclic and tangential is called bicentric.

      This puzzle concerns a series of nested bicentric quadrilaterals.


      Consider a bicentric quadrilateral ABCD. The tangential points on the incircle form the contact quadrilateral WXYZ. (Such that W is on AB, X on BC, Y on CD, Z on DA).

      Considering the internal angles of the ABCD quadrilateral (as a, b, c, d), the tangent lengths from a vertex to the vertices of the the contact quadrilateral are equal (see: Pitot Theorem), and so the angle subtended at the incentre of the contact quadrilateral (I) is supplementary to the internal angle of the original quadrilateral. (And so it is the same as the opposite internal angle of the original quadrilateral):

      ∠ZIW = 180° − a = c
      ∠WIX = 180° − b = d
      ∠XIY = 180° − c = a
      ∠YIZ = 180° − d = b

      (Note that knowing the angles subtended at the centre of the incircle means we can fully determine the shape of the contact quadrilateral, and we can then draw the tangents to the vertices of the contact quadrilateral to find the original quadrilateral (which has opposite angles supplementary, and so is necessarily cyclic). [*]

      And so, each internal angle of the contact quadrilateral is the mean of the internal angles at the vertices on the original quadrilateral at either end of the tangent side:

      w = (a + b) / 2
      x = (b + c) / 2
      y = (c + d) / 2
      z = (d + a) / 2

      So, if the original angles are (a, b, c, d), then the final angles (a′, b′, c′, d′) after 10 applications of this transformation are:

      a′ = (16a + 17b + 16c + 15d) / 64
      b′ = (15a + 16b + 17c + 16d) / 64
      c′ = (16a + 15b + 16c + 17d) / 64
      d′ = (17a + 16b + 15c + 16d) / 64

      And remembering a + c = 180°, and b + d = 180°, we get:

      a′ = 87 + (6 + b)/32
      b′ = 92 + (26 − a)/32
      c′ = 92 + (26 − b)/32
      d′ = 87 + (6 + a)/32

      And these are integers.

      So, if we assume a and b are angles where 0° < a < b < 90°, we get:

      a = 26°, b = 58°, c = 154°, d = 122°
      ⇒
      a′ = 89°, b′ = 92°, c′ = 91°, d′ = 88°

      Solution: The angles in the original quadrilateral were: 26°, 58°, 122°, 154°.

      Calculating successive internal angles we get:

      A: 26°, 58°, 154°, 122°.
      B: 42°, 106°, 138°, 74°.
      C: 74°, 122°, 106°, 58°.
      D: 98°, 114°, 82°, 66°.
      E: 106°, 98°, 74°, 82°.
      F: 102°, 86°, 78°, 94°.
      G: 94°, 82°, 86°, 98°.
      H: 88°, 84°, 92°, 96°.
      I: 86°, 88°, 94°, 92°.
      J: 87°, 91°, 93°, 89°.
      K: 89°, 92°, 91°, 88°.

      Note that the angles are approaching 90°, and a square forms a stable scenario, which can be extended in either direction indefinitely.

      The sequence would continue:

      L: 90.5°, 91.5°, 89.5°, 88.5°.
      M: 91°, 90.5°, 89°, 89.5°.
      N: 90.75°, 89.75°, 89.25°, 90.25°.
      O: 90.25°, 89.5°, 89.75°, 90.5°.
      P: 89.875°, 89.625°, 90.125°, 90.375°.
      Q: 89.75°, 89.875°, 90.25°, 90.125°.
      R: 89.8125°, 90.0625°, 90.1875°, 89.9375°.
      …

      by which time the angles are all within 0.2° of 90°.


      However if we attempt to actually construct the series of nested quadrilaterals we run into a problem.

      From [*] we know that if the internal angles of quadrilateral A are: (26°, 58°, 154°, 122°), then the angles subtended at the centre of the contact quadrilateral (= quadrilateral B) are: (122°, 26°, 58°, 154°), and this is enough information to draw quadrilateral B inside a circle, and then construct quadrilateral A outside the circle.

      If we do so we get this:

      (quadrilateral A is red; quadrilateral B is black).

      Similarly, knowing the internal angles of quadrilateral B are: (42°, 106°, 138°, 74°), then the angles subtended at the centre of quadrilateral C are: (74°, 42°, 106°, 138°), and drawing quadrilaterals C and B we get this:

      (quadrilateral B is red; quadrilateral C is black).

      And, although the two quadrilaterals B in the diagrams look superficially similar (as they have the same angles), they are not mathematically similar, so the second cannot be uniformly scaled to fit onto the first. (The easy way to see this is that the distance between the 106° and 138° angles in the second diagram is longer than if the first diagram was scaled so as to fit the opposite side over the corresponding side in the first diagram. It is as if a strip parallel to that side has been removed (keeping the angles the same)).

      Here are the two quadrilaterals B superimposed:

      (Note the extra red line where one side of the red (smaller) quad B does not overlap the black quad B, and the inscribed circle does not touch all four sides of the black quad B).

      So, practically it is not possible to construct such a sequence of quadrilaterals.

      Hence the puzzle itself is flawed.

      Like

      • Jim Randell's avatar

        Jim Randell 2:44 pm on 8 April 2026 Permalink | Reply

        Although I have not found a correction printed in The Sunday Times, this puzzle did prompt an article by Victor Bryant (editor of Teaser puzzles at the time) and John Duncan, showing that if a nest of 3 bicentric quadrilaterals can be made, then they are necessarily all squares.

        “Wheels within wheels”, Victor Bryant, John Duncan
        The Mathematical Gazette, Volume 94, Issue 531, November 2010, pp. 502 – 505
        https://www.jstor.org/stable/25759740

        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
Create your own website, free on WordPress.com.Create your own website.
Start free