Updates from June, 2026 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 2:19 pm on 26 June 2026 Permalink | Reply
    Tags:   

    Teaser 2427: [Birth products] 

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

    A mathematics professor asked his students to look at their date of birth (dd/mm/yyyy) and calculate their “birth product”: the product of dd and mm. He asked them to number the days of the year of their birth (1 = 1st Jan;
    2 = 2nd Jan; 32 = 1st Feb; and so on) and work out the number of the day of their birth date. One student said that his answers were the same, and that multiplying that number by a smaller whole number gave an answer equal to his birth year. The professor, an older man, said the same was true for him.

    What (using dd/mm/yyyy) are the student’s and professor’s birth dates?

    This puzzle was originally published with no title.

    [teaser2427]

     
    • Jim Randell's avatar

      Jim Randell 2:20 pm on 26 June 2026 Permalink | Reply

      This Python program considers possible birth years between 1900 and 2000.

      It factorises the year into candidate values for the “birth product” and the “smaller number”. It then finds the day of the year that corresponds to the candidate “birth product” and verifies that the day × month product gives the same value. If this test succeeds then we have found a date with the required properties.

      It runs in 66ms. (Internal runtime is 941µs).

      from datetime import (date, timedelta)
      from enigma import (irange, divisors_pairs, printf)
      
      # consider years
      for y in irange(1900, 2000):
      
        # divide year in <smaller number> and <birth product>
        for (sn, bp) in divisors_pairs(y):
          if bp > 366 or sn == bp: continue
      
          # check day bp in year y
          d = date(y, 1, 1) + timedelta(days=bp - 1)
          # does it have the correct product?
          if not (d.month * d.day == bp and d.year == y): continue
      
          # output viable date
          printf("year {y} = {sn} * {bp}; day {bp} = {d}")
      

      Solution: The student’s DOB is: 30/03/1980. The professor’s DOB is: 30/05/1950.

      For the student we have:

      30 × 3 = 90
      and 30th March is the 90th day of 1980
      also:
      90 × 22 = 1980

      For the professor we have:

      30 × 5 = 150
      and 30th May is the 150th day of 1950
      also:
      150 × 13 = 1950

      Like

    • Ruud's avatar

      Ruud 6:39 pm on 26 June 2026 Permalink | Reply

      import datetime
      
      
      day = datetime.datetime(1900, 1, 1)
      
      while day < datetime.datetime(2000, 1, 1):
          if day.day * day.month == (daynr := (day - datetime.datetime(day.year, 1, 1)).days + 1):
              multiplier, rest = divmod(day.year, daynr)
              if rest == 0 and multiplier < daynr:
                  print(f"{day.day}/{day.month}/{day.year}")
          day += datetime.timedelta(days=1)
      

      Like

  • Unknown's avatar

    Jim Randell 2:27 pm on 24 June 2026 Permalink | Reply
    Tags:   

    Teaser 2419: [Measuring jugs] 

    From The Sunday Times, 1st February 2009 [link]

    The teacher had four containers of different two-digit capacities. She explained that any two together were “useful” because they could be used to measure out exactly any whole-number volume up to the capacity of the larger. She gave two to Sam and two to Oliver. Each noted that the sum of his pair of capacities was a perfect square and that his two capacities, written side by side, formed a four-figure perfect square.

    What were the capacities of the four containers?

    This puzzle was originally published with no title.

    [teaser2419]

     
    • Jim Randell's avatar

      Jim Randell 2:28 pm on 24 June 2026 Permalink | Reply

      If we have two capacities x and y, where gcd(x, y) = g then we can only ever make volumes that are multiples of g.

      And if we need to be able to make 1 unit volume, then it follows g = 1, so the capacities must be co-prime.

      And if we can make a volume of 1, then we can make any required volume, by just repeating the process to make 1 as many times are required (although this won’t necessarily be the most efficient method).

      The following Python program runs in 61ms. (Internal runtime is 190µs).

      from enigma import (powers, is_square, gcd, subsets, printf)
      
      # generate possible pairs of capacities
      def pairs():
        # start by considering 4-digit squares
        for n in powers(32, 99, 2):
          # split into 2 (different) capacities
          (x, y) = divmod(n, 100)
          if y < 10: continue
          # that sum to a square
          if not is_square(x + y): continue
          # and are "useful"
          if gcd(x, y) != 1: continue
          printf("[{n} -> {x}:{y}]")
          yield (x, y)
      
      # choose 2 pairs
      for (p1, p2) in subsets(pairs(), size=2):
        # and check all pairs of capacities are useful
        if all(gcd(x, y) == 1 for (x, y) in subsets(p1 + p2, size=2)):
          printf("{p1} and {p2}")
      

      Solution: The containers had capacities of 17, 21, 64 and 79.

      Being derived from the squares:

      1764 = 42²; 17 + 64 = 81 = 9²
      7921 = 89²; 79 + 21 = 100 = 10²

      Like

  • Unknown's avatar

    Jim Randell 7:27 am on 21 June 2026 Permalink | Reply
    Tags:   

    Teaser 3326: Interesting figures 

    From The Sunday Times, 21st June 2026 [link]

    At a cricket match Tom Leggie bowled fewer than 100 overs (O), of which fewer than 10 were maidens (M), took more than one but fewer than 11 wickets (W), and had an average of runs conceded per over (R/O) that was less than 6.

    I noticed something interesting about his bowling analysis (O-M-R-W): writing the numbers together with no gaps or overlap, then reading from right to left, they comprised a perfect square followed immediately by a prime with the same number of digits, and they could also be read (again in their entirety, right to left) as a prime number followed by a perfect square number with more digits. None of the perfect squares or primes was a single digit, and no zeros were involved.

    What was Tom’s bowling analysis (O-M-R-W)?

    [teaser3326]

     
    • Jim Randell's avatar

      Jim Randell 7:41 am on 21 June 2026 Permalink | Reply

      A brute force solution determines the numbers involved in an acceptable time.

      The following Python program runs in 765ms.

      from enigma import (irange, concat, rev, div, is_square, is_prime, peek, printf)
      
      # split the string of digits at <k> and do the reverses of the
      # two sides satisfy fn1 and fn2?
      # if so, return the numbers as (n2, n1)
      def check(ds, k, fn1, fn2):
        (n1, n2) = (int(rev(ds[:k])), int(rev(ds[k:])))
        if fn1(n1) and fn2(n2):
          return (n2, n1)
      
      # choose number of overs bowled (= O)
      for O in irange(1, 99):
        if O % 10 == 0: continue  # no zeros
      
        # number of maiden overs (= M)
        for M in irange(1, min(O, 9)):
      
          # number of wickets taken (= W)
          for W in irange(2, 9):
      
            # number of runs conceded (= R) < 6 * O
            for R in irange(1, 6 * O - 1):
      
              # form the bowling analysis
              b = concat(O, M, R, W)
              if '0' in b: continue
      
              # can it be read (from right to left) as a square followed by
              # a prime, both having the same number of digits
              k = div(len(b), 2)
              if k is None or k < 3: continue
              r1 = check(b, k, is_prime, is_square)
              if r1 is None: continue
      
              # can it also be read (from right to left) as a prime followed by
              # a square with more digits
              rs = (check(b, j, is_square, is_prime) for j in irange(k + 1, 2*k - 2))
              r2 = peek((r for r in rs if r is not None), default=None)
              if r2 is None: continue
      
              # output solution
              printf("b={b}, (sq, pr) = {r1}, (pr, sq) = {r2} [O={O} M={M} R={R} W={W}]")
      

      Solution: [To Be Revealed]

      Like

      • Jim Randell's avatar

        Jim Randell 8:49 am on 21 June 2026 Permalink | Reply

        By considering the number of digits in the constituent parts, we can show that the bowling analysis must be 6 digits, and therefore splits are 3:3 and 2:4. And this allows a straightforward solution.

        The following run file executes in 79ms (and has an internal runtime of 765µs).

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # O-M-R-W = ABCDEF
        # then:
        #   O = AB, M = C, R = DE, W = F
        
        --distinct=""
        --digits="1-9"
        
        # R/O < 6
        "DE < 6 * AB"
        
        # W > 1
        "F > 1"
        
        # FED:CBA are a square and a prime
        "is_square(FED)"
        "is_prime(CBA)"
        
        # FE:DCBA are a prime and a square
        "is_prime(FE)"
        "is_square(DCBA)"
        
        --template="(AB C DE F)"
        --solution=""
        

        Like

    • Ruud's avatar

      Ruud 10:42 am on 21 June 2026 Permalink | Reply

      It is obvious that the length of the numbers combined can’t be 4 as that can’t be divided into two parts which are not the same length and have more than one digit. Therefore, the length has to 6 and the sublength will be 3-3 and 2-4. Then it is just brute force:

      import peek
      import istr
      
      
      for o, m, w in istr.product(range(1, 100), range(1, 10), range(2, 11)):
          for r in istr.range(6 * o):
              if (
                  "0" not in (omrw_reversed:= (omrw := istr("=omrw")).reversed())
                  and len(omrw_reversed) == 6
                  and omrw_reversed[:3].is_square()
                  and omrw_reversed[3:].is_prime()
                  and omrw_reversed[:2].is_prime()
                  and omrw_reversed[2:].is_square()
              ):
                  peek(omrw, omrw_reversed, o, m, r, w)
      

      Like

    • Frits's avatar

      Frits 10:44 am on 21 June 2026 Permalink | Reply

      Less brute force.

      from enigma import is_prime
      
      # O-M-R-W should have an even number of digits, M and W are both single digits
      # so O and R must both have the same number of digits
      # O-M-R-W must have at least 6 digits as prime number followed by a perfect 
      # square number containing more digits (both no single digis)
      # thus O and R are both 2-digit numbers
      
      # O-M-R-W = t_o, u_o, m, t_r, u_r, w
      
      # squares in range 100-9999 with no zeroes
      sqs = {n2 for n in range(11, 100) if '0' not in str(n2 := n * n)}
      
      # runs, first digit must also be the end digit of a square 
      for t_r in (1, 4, 5, 6, 9):
        # second digit must also be the end digit of a prime
        for u_r in (1, 3, 7, 9):
          sr = str(r := 10 * t_r + u_r)
          # wickets
          for w in range(2, 10):
            rw = sr + (sw := str(w))
            # a perfect square followed immediately by a prime
            if not int(wr := rw[::-1]) in sqs: continue
            # also as prime number followed by a perfect square 
            if not is_prime(int(wr[:2])): continue
      
            # overs, first digit of o must be:
            # 1, 3, 7, or 9 and also the end digit of a square (0, 1, 4, 5, 6, 9)
            for t_o in (1, 9):
              for u_o in range(1, 10):
                if r >= 6 * (o := 10 * t_o + u_o): continue
                so = str(o)
                # maidens
                for m in range(1, 10):
                  omrw = so + str(m) + rw
                  rtol = omrw[::-1]
                  # prime number followed by a perfect square 
                  if not int(rtol[2:]) in sqs: continue
      
                  # also a perfect square followed immediately by a prime
                  if not is_prime(int(rtol[3:])): continue
                  print(f"answer: {o}-{m}-{r}-{w}")
      

      Like

    • Alex.T.Sutherland's avatar

      Alex.T.Sutherland 3:47 pm on 24 June 2026 Permalink | Reply

      Method:-
      I concentrated on what was the 6 digit number particularly the 3d square.
      If the following constraints are placed on the set of 3d squares there is only
      one square left.
      (No zeros,first 2 digits make a 2d prime,first digit > 1].
      The third digit of this unique 3d square is now the starting digit of the 4d square.

      The set of 4d numbers (approx 5 or 6) with the known starting number is tested
      for no zeros and that the following 3 digits form a 3d prime.
      There is only one answer .Hence the complete 6 digit number is now known.
      The 6d number is segmented into a pattern of 2,1,2,1 to give O,M,R,W;
      The answer to OMRW satisfies the constraints.
      My answer has :- sum of the 6d-number digits = 26. R/O ~ 4.8.
      Time : < 6ms

      Like

  • Unknown's avatar

    Jim Randell 3:11 pm on 19 June 2026 Permalink | Reply
    Tags:   

    Teaser 2428: [Bags of sweets] 

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

    For years, I have been into our local sweetshop for bags of my favourite sweets. I always buy as many bags as possible for less than a fiver.

    On each occasion, had I bought one more bag, then the total cost in pence would have been like the amount I actually paid, but with the digits in a different order. The cost of a bag of my favourite sweets has gone up three times during this period.

    What have been the four prices of a bag of my sweets?

    “A fiver” is £ 5.

    This puzzle was originally published with no title.

    [teaser2424]

     
    • Jim Randell's avatar

      Jim Randell 3:11 pm on 19 June 2026 Permalink | Reply

      This Python program runs in 66ms. (Internal runtime is 2.0s).

      from enigma import (irange, divf, nsplit, printf)
      
      # consider possible prices for a bag
      for b in irange(1, 499):
        # number of bags for less than 500
        n = divf(499, b)
        # total cost
        t = b * n
        # cost of n + 1 bags
        t1 = t + b
        # do <t> and <t1> contain the same digits?
        if not (sorted(nsplit(t)) == sorted(nsplit(t1))): continue
        # output solution
        printf("b={b} n={n}; costs {t} -> {t1}")
      

      Solution: The four prices were: 90p, 99p, 135p, 162p.

      We have:

      price = 90p: 5 bags = 450p, 6 bags = 540p
      price = 99p; 5 bags = 495p, 6 bags = 594p
      price = 135p; 3 bags = 405p, 4 bags = 540p
      price = 162p; 3 bags = 486p, 4 bags = 648p

      Like

      • Frits's avatar

        Frits 3:28 pm on 19 June 2026 Permalink | Reply

        You can argue that there is no valid solution (“… gone up three times …”).

        Like

        • Jim Randell's avatar

          Jim Randell 5:30 pm on 19 June 2026 Permalink | Reply

          @Frits: Would it be better if it was phrased:

          The cost of a bag of my favourite sweets has increased on three separate occasions during this period.

          ?

          Like

    • Ruud's avatar

      Ruud 7:03 pm on 19 June 2026 Permalink | Reply

      For once without istr:

      import peek
      
      for price in range(1, 500):
          q0 = 499 // price
          cost0 = q0 * price
          cost1 = (q0 + 1) * price
          if set(str(cost0)) == set(str(cost1)):
              peek(price, q0, cost0, cost1)
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:30 am on 20 June 2026 Permalink | Reply

        @Ruud: The test at line 7 doesn’t check that two numbers use the same digits but in a different order.

        e.g.

        >>> set(str(122)) == set(str(211))
        True
        

        Like

        • Ruud's avatar

          Ruud 9:26 am on 20 June 2026 Permalink | Reply

          @Jim
          I’m not so sure about that. In your example 122 uses the digits 1 and 2. As does 211 !

          Of a permutations is meant, simply replaving set by sorted would do the works.

          Like

        • Ruud's avatar

          Ruud 9:30 am on 20 June 2026 Permalink | Reply

          Forget my remark. I misread the original text.

          Like

  • Unknown's avatar

    Jim Randell 8:26 am on 17 June 2026 Permalink | Reply
    Tags:   

    Teaser 2430: [Tall, dark and handsome] 

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

    Between 200 and 300 men were asked to class themselves tall, dark or handsome, or any combination of these. A quarter felt they had none of these properties. The number including tall as a quality equalled the number including dark and the number including handsome. The number claiming to be tall, dark and handsome was half the number saying they were only dark and handsome. One more than this latter number said they were tall and dark; one more again claimed to be tall and handsome. This last group was exactly half the size of that saying they were only dark.

    How many claimed to be tall, dark and handsome?

    This puzzle was originally published with no title.

    [teaser2430]

     
    • Jim Randell's avatar

      Jim Randell 8:28 am on 17 June 2026 Permalink | Reply

      We can identify the groups as:

      T = just tall
      D = just dark
      H = just handsome
      TD = tall and dark (but not handsome)
      TH = tall and handsome (but not dark)
      DH = dark and handsome (but not tall)
      TDH = tall, dark and handsome
      X = none of them

      The total number of participants is N ∈ [200, 300].

      And we have:

      X = N/4
      T + D + H + TD + TH + DH + TDH = 3N/4 = 3X

      The remaining conditions (after some disambiguation) amount to:

      T + TD + TH + TDH = D + TD + DH + TDH = H + TH + DH + TDH
      TDH = DH/2
      DH + 1 = TD
      TD + 1 = TH
      TH = D/2

      We can choose a value for X (and hence N), and that leaves us with a system of 7 equations in 7 variables, which can be solved using the [[ Matrix.linear() ]] solver from the enigma.py library.

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

      from enigma import (Matrix, Rational, irange, as_int, printf)
      
      Q = Rational()
      
      eqs = Matrix([
        # for the groups: T, D, H, TD, TH, DH, TDH
        # we have the following equations:
        # T   D   H  TD  TH  DH TDH
        ( 1,  1,  1,  1,  1,  1,  1), # = 3*X; # total is 3X
        (-1,  1,  0,  0, -1,  1,  0), # = 0; T + TD + TH + TDH = D + TD + DH + TDH
        (-1,  0,  1, -1,  0,  1,  0), # = 0; T + TD + TH + TDH = H + TH + DH + TDH
        ( 0,  0,  0,  0,  0, -1,  2), # = 0; TDH = DH/2
        ( 0,  0,  0,  1,  0, -1,  0), # = 1; DH + 1 = TD [DH/2 + 1 = TD -> multiple solutions]
        ( 0,  0,  0, -1,  1,  0,  0), # = 1; TD + 1 = TH
        ( 0, -1,  0,  0,  2,  0,  0), # = 0; TH = D/2
      ], field=Q)
      
      # consider possible values for X (= N/4)
      for X in irange(50, 75):
        N = 4*X
        vs = [3*X, 0, 0, 0, 1, 1, 0]
        try:
          # solve the equations, find non-negative integer solutions
          (T, D, H, TD, TH, DH, TDH) = eqs.linear_solve(vs, valid=(lambda x: as_int(x, "0+")))
        except ValueError:
          continue
      
        # output solution
        printf("X={X} -> N={N}, T={T} D={D} H={H} TD={TD} TH={TH} DH={DH} TDH={TDH}")
      

      Solution: 9 participants claimed to be tall, dark and handsome.

      The values are:

      N = 244
      X = 61
      T = 38
      D = 40
      H = 39
      TD = 19
      TH = 20
      DH = 18
      TDH = 9

      Like

    • Frits's avatar

      Frits 10:31 am on 18 June 2026 Permalink | Reply

      Expressing variables in DH leads to 3.N = 38.DH + 48. So DH is a multiple of 3 (and even as well). The only multiple of 6 for DH that leads to a N value between 200 and 300 is 18 giving the answer of 9 men.

      Like

  • Unknown's avatar

    Jim Randell 6:00 am on 14 June 2026 Permalink | Reply
    Tags:   

    Teaser 3325: Olive and Don 

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

    Olive lives at the southwest corner of a city with a grid of streets (as shown with north at the top). She wants to visit her father Don, who lives at the northeast corner. Olive is trying to work out how many possible routes she can use along the streets, not visiting any intersection more than once, but thinks that there are too many to count. To make the calculation easier, she has decided that she will only travel in the following directions: NW, N, NE, E or SE.

    She tells Don the number of possible routes, and he works out that the number is the product of their ages (both of which have two digits).

    How old are Olive and Don?

    [teaser3325]

     
    • Jim Randell's avatar

      Jim Randell 6:17 am on 14 June 2026 Permalink | Reply

      This Python 3 program generates all possible paths between O and D using the specified directions. And then factorises the number of paths found into two 2-digit numbers.

      It runs in 71ms. (Internal runtime is 7.9ms).

      from enigma import (icount, divisors_pairs, printf)
      
      # the grid of intersections is composed of those points (x, y) = [0..6] * [0..4]
      # where x and y have the same parity
      
      # possible moves (<delta-x>, <delta-y>) which preserve parity
      moves = [
        # parity 0: NW, N, NE, E, SE
        [(-1, +1), (+0, +2), (+1, +1), (+2, 0), (+1, -1)],
        # parity 1: NW, NE, SE
        [(-1, +1), (+1, +1), (+1, -1)],
      ]
      
      # extend path <vs> to <dst> without revisiting a vertex
      def paths(vs, dst):
        (x, y) = vs[-1]
        # are we done?
        if (x, y) == dst:
          yield vs
        else:
          # consider possible moves
          for (dx, dy) in moves[x % 2]:
            v = (vx, vy) = (x + dx, y + dy)
            if not (vx < 0 or vy < 0 or vx > 6 or vy > 4 or v in vs):
              yield from paths(vs + [v], dst)
      
      # count the number of paths between (0, 0) and (6, 4)
      n = icount(paths([(0, 0)], (6, 4)))
      printf("{n} possible paths")
      
      # factor n into two 2-digit numbers
      for (a, b) in divisors_pairs(n):
        if 9 < a < b < 100:
          printf("-> {n} = {a} * {b}")
      

      Solution: Olive and Don are 49 and 81.

      There are 3969 possible routes, and 3969 = 49 × 81.

      There is a second candidate solution 3969 = 63 × 63, but I assumed that Olive’s age is less than that of her father.

      Like

      • Jim Randell's avatar

        Jim Randell 10:11 am on 22 June 2026 Permalink | Reply

        Here is a manual solution (suggested by John Crabtree [link]):

        We draw the diagram like this, so that it consists of 6 levels (L0, …, L5), with one-way roads between the levels, and two-way roads within each level.

        To make a path from O to D is to ascend through the 6 levels. Once we leave a level we can never return to it, so a path from O to D is uniquely defined by the choice of blue roads between levels, and every choice defines exactly one path (as there is only one collection of red roads that can link any collection of blue roads).

        The number of paths (nP) between O and D is therefore calculated from the product of the number of roads between each level:

        nP = 3 × 7 × 9 × 7 × 3 = 3969

        And factorising this into a pair of 2-digit numbers we get:

        3969 = 49 × 81 = 63 × 63

        Only the first of which gives a viable answer to the puzzle.

        Like

    • Ruud's avatar

      Ruud 11:37 am on 14 June 2026 Permalink | Reply

      def search(pos, visited):
          if pos == (6, 4):
              yield 1
          else:
              for dir in ((-1, 1), (1, 1), (1, -1)) if pos[0] % 2 else ((-1, 1), (0, 2), (1, 1), (2, 0), (1, -1)):
                  pos_next = tuple(v + d for v, d in zip(pos, dir))
                  if pos[0] in range(7) and pos[1] in range(5) and pos_next not in visited:
                      yield from search(pos_next, visited | {pos_next})
      
      
      number_of_routes = sum(search((0, 0), {(0, 0)}))
      
      for olive in range(10, 100):
          if (number_of_routes % olive) == 0 and olive < (don := number_of_routes // olive) < 100:
              print(f"{number_of_routes=} {olive=} {don=}")
      

      Like

    • Alex.T.Sutherland's avatar

      Alex.T.Sutherland 8:01 pm on 17 June 2026 Permalink | Reply

      Method.
      1. Create an Adjacency Matrix (18*18).
      This takes care of the no-go directions (S SW W) .
      2. I have a ‘graph’ function which will accept the AM
      and return the number of paths (with the points
      on each path if required).Python has something similar.
      In this case the number of paths was a 4 digit number.
      3. Using the divisors of this number gave a unique pair
      of 2 digit numbers whose product equalled the number
      and which can fit the father’s and daughter’s ages
      4. My Answer : They are both ‘old-fashioned’.
      5. Approx. Times :-
      Adjacency Matrix < 1ms; Graph < 3ms ;All Paths < 3ms
      Ages determination < 2ms;

      Like

  • Unknown's avatar

    Jim Randell 8:44 am on 12 June 2026 Permalink | Reply
    Tags:   

    Teaser 2421: [Silver pennies] 

    From The Sunday Times, 15th February 2009 [link]

    The winning team at a seven-a-side football tournament was awarded a four-figure perfect square number of silver pennies. This was shared as equally as possible between the players, the remaining pennies being placed in the Benevolent Fund. The same procedure, with an equal prize, was followed for the cricket and rugby league tournaments. In each case, more than one penny went into the fund. And one sport’s contribution was the sum of the other two.

    How much did the wicket keeper win?

    The prize for the football tournament is split between 7 players. For the cricket team the prize is split between 11 players (including the wicket keeper), and for the rugby league team the prize is split between 13 players.

    This puzzle was originally published with no title.

    [teaser2421]

     
    • Jim Randell's avatar

      Jim Randell 8:44 am on 12 June 2026 Permalink | Reply

      We are to assume that the in each case the entire quantity of pennies is evenly distributed to the 7 players in the football team, the 11 players in the cricket team, and the 13 players in the rugby league team. And the that wicket keeper is part of the cricket team (and does not play in any of the other teams).

      If we have 3 numbers, and one of them is the sum of the other two, then by adding them all together we get twice the largest number.

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

      from enigma import (powers, div, printf)
      
      # sizes of the groups
      teams = (7, 11, 13)
      
      # consider 4-digit squares
      for n in powers(32, 99, 2):
        rs = tuple(n % k for k in teams)
        if any(r < 2 for r in rs): continue
        if div(sum(rs), 2) not in rs: continue
        # output solution
        printf("n={n}: {rs}")
        for k in teams:
          (d, r) = divmod(n, k)
          printf("-> /{k} = {d} rem {r}")
        printf()
      

      Solution: The wicket keeper won 820 pennies.

      In each case the prize was 9025 pennies.

      The football team (7 players) won 1289 pennies each, with the remaining 2 pennies going to the Benevolent Fund.

      The cricket team (11 players) won 820 pennies each, with the remaining 5 pennies going to the Benevolent Fund.

      The rugby league team (13 players) won 694 pennies each, with the remaining 3 pennies going to the Benevolent Fund.

      So the Benevolent Fund received 10 of the 27075 pennies (= 0.33% of the total amount).

      Like

    • Ruud's avatar

      Ruud 11:06 am on 12 June 2026 Permalink | Reply

      for total in (i * i for i in range(32, 100)):
          if all(total % n > 1 for n in (7, 11, 13)) and any(x == y + z for x, y, z in istr.permutations(total % n for n in (7, 11, 13))):
              print(total // 11)
      

      Like

  • Unknown's avatar

    Jim Randell 8:36 am on 10 June 2026 Permalink | Reply
    Tags:   

    Teaser 2424: [Five-pointed star] 

    From The Sunday Times, 8th March 2009 [link]

    I have drawn a five-pointed star with points A, B, C, D, E clockwise (i.e., the star is formed by the lines AC, CE, EB, BD and DA). Measured in degrees, the star’s angles at A, B, C, D and E, respectively, form an arithmetic progression of whole numbers. Also, within that star, the lines have created a small pentagon. Altogether the five angles in the star and the five angles of the small pentagon include four prime numbers.

    What are those primes?

    This puzzle was originally published with no title.

    [teaser2424]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 10 June 2026 Permalink | Reply

      (See also: Teaser 1885, also set by Nick MacKinnon).

      If we denote the angles at the vertices of the star as a, b, c, d, e, and the opposite angles in the pentagon as a′, b′, c′, d′, e′, then from the triangles formed by the star we have:

      a′ + b + e = 180°
      b′ + c + a = 180°
      c′ + d + b = 180°
      d′ + e + c = 180°
      e′ + a + d = 180°

      And the internal angles of the pentagon sum to 540°, hence:

      (a′ + b′ + c′ + d′ + e′) + 2(a + b + c + d + e) = 900°

      a + b + c + d + e = 180°

      And these form an arithmetic progression, so we can write:

      (a, b, c, d, e) = (c − 2x, c − x, c, c + x, c + 2x)

      c = 36
      x ≤ 17

      If x is even then all the angles involved will also be even, so we are not going to get 4 different prime numbers among them. So we can skip even x.

      This program considers possible x values to find a viable angles for the internal vertices of the star and for the smaller pentagon.

      It runs in 74ms. (Internal runtime is 91µs).

      from enigma import (irange, icount, is_prime, printf)
      
      # consider possible common differences in the arithmetic progression
      for x in irange(1, 17, step=2):
        # construct the progression
        ns = list(irange(36 - 2*x, 36 + 2*x, step=x))
        # count the number of primes involved
        pn = icount(ns, is_prime)
      
        # calculate the internal angles of the pentagon
        (a, b, c, d, e) = ns
        Ns = (
          180 - (b + e),
          180 - (c + a),
          180 - (d + b),
          180 - (e + c),
          180 - (a + d),
        )
        # count the primes in these
        pN = icount(Ns, is_prime)
      
        # check for exactly 4 prime angles
        if pn + pN == 4:
          # output solution
          printf("a={a} x={x} -> ns={ns} ({pn} prime); Ns={Ns} ({pN} prime)")
      

      Solution: The primes are: 31, 41, 103, 113.

      The angles at the vertices of the star form the following progression: (26°, 31°, 36°, 41°, 46°).

      And the corresponding angles at the vertices of the pentagon are: (103°, 118°, 108°, 98°, 113°).

      Here is a possible construction of the star:

      Like

    • Ruud's avatar

      Ruud 1:31 pm on 10 June 2026 Permalink | Reply

      import peek
      import istr
      
      for start in range(2, 35, 2):
          inc = (180 - 5 * start) // 10
          outer_angles = [start + i * inc for i in range(5)]
          inner_angles = [180 - angle1 - angle2 for angle1, angle2 in zip(outer_angles, outer_angles[2:] + outer_angles[:2])]
          if len(prime_angles := [angle for angle in outer_angles + inner_angles if istr.is_prime(angle)]) == 4:
              peek(prime_angles)
      

      Like

  • Unknown's avatar

    Jim Randell 6:56 am on 7 June 2026 Permalink | Reply
    Tags:   

    Teaser 3324: Prime tournament 

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

    Our squash club recently organised a round-robin tournament (i.e., each participant played each of the others once), with each game resulting in a win for one of the players. An even number of players, fewer than 28, took part. At the end of the tournament a player’s score was the number of games he or she had won.

    It turned out that each player’s score was a prime number, and each prime number less than the number of players occurred as somebody’s score. Furthermore each such score was achieved by a prime number of players! The players who tied bottom shared equally the £10 booby prize.

    How many players were there, and what was the most common score?

    [teaser3324]

     
    • Jim Randell's avatar

      Jim Randell 9:23 am on 7 June 2026 Permalink | Reply

      (See also: Enigma 1045).

      I split the puzzle into two parts.

      The first finds candidate score distributions:

      from enigma import (irange, primes, C, express, multiset, run, printf)
      
      # consider the number of players = n
      for n in irange(2, 26, step=2):
      
        # find primes less than n
        ps = list(primes.between(2, n - 1))
        if not ps: continue
      
        # count the number of matches in the tournament
        # this is the total number of points allocated
        t = C(n, 2)
      
        printf("n={n}: ps={ps} t={t}")
      
        # express the points total using the prime numbers
        # each prime total must appear a prime number of times
        for qs in express(t, ps, qs=ps):
          # must be totals for n players
          if not (sum(qs) == n): continue
          # and the number of players with the lowest points must divide into 1000
          if not (1000 % qs[0] == 0): continue
      
          # generate the list of totals
          m = multiset.from_pairs(zip(ps, qs))
          ts = list(m.sorted(reverse=1))
          printf("-> {ts}")
          assert sum(ts) == t
      
          # check to see if this list of totals is viable
          run("teaser3324mzn.py", *ts)
      

      And the second part (which is called from the first program) uses a MiniZinc model to check if the given score distribution is viable:

      from enigma import (multiset, sprintf, args, printf)
      from minizinc import MiniZinc
      
      # collect required points
      pts = args(None, 0, int)
      n = len(pts)
      
      # construct a MiniZinc model to make the table
      model = sprintf("""
      
      % indices for the players
      set of int: T = 1..{n};
      
      % points to allocate
      array[T] of int: points = {pts};
      
      % wins for A against B
      array [T, T] of var {{0, 1}}: x;
      
      % no player plays themselves
      constraint forall (i in T) (x[i, i] = 0);
      
      % symmetry constraints
      constraint forall (i, j in T where i < j) (x[i, j] + x[j, i] = 1);
      
      % points for each player
      constraint forall (i in T) (points[i] = sum (j in T where j != i) (x[i, j]));
      
      solve satisfy;
      """)
      
      # solve the model
      for [table] in MiniZinc(model).solve(result='x'):
        # output the table
        printf()
        for row in table:
          printf("{row} -> {t}", t=sum(row))
        printf()
        # output solution
        m = multiset.from_seq(pts)
        for (s, k) in m.multiplicity(max(m.values())):
          printf("num players = {n}; most common score = {s} (appears {k} times)")
        printf()
        # one example is enough
        break
      

      It turns out only one of the candidate distributions of points allows a table to be constructed, and this gives the answer.

      Solution: There were 16 players in the tournament. The most common score was 11 points.

      With 16 players there are C(16, 2) = 120 matches, and so 120 points are allocated.

      The scores were:

      2 players with 13 points
      5 players with 11 points
      2 players with 7 points
      3 players with 5 points
      2 players with 3 points
      2 players with 2 points
      total = 120 points

      The bottom two players (with 2 points each) get to share the £ 10 booby prize, so they get £ 5 each.

      There are many possible tables that correspond to this score sequence, but here is one example to show it is possible:

      % python3 teaser3324mzn.py 13 13 11 11 11 11 11 7 7 5 5 5 3 3 2 2
      
      [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0] -> 13
      [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0] -> 13
      [0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1] -> 11
      [0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1] -> 11
      [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] -> 11
      [0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] -> 11
      [0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1] -> 11
      [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1] -> 7
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1] -> 7
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1] -> 5
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1] -> 5
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1] -> 5
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1] -> 3
      [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1] -> 3
      [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] -> 2
      [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -> 2
      
      num players = 16; most common score = 11 (appears 5 times)
      

      So, although we don’t need the MiniZinc program to check that a score sequence is viable, it is handy to generate an example table for a given score sequence.


      The first program finds 16 candidate score sequences:

      12 players, 1 candidate, starting [11, 11, …]
      16 players, 1 candidate, starting [13, 13, …]
      20 players, 3 candidates, starting [19, 19, …]
      24 players, 11 candidates, starting [23, 23, …]

      Each sequence starts with 2 copies of the largest prime less than n (and this is the smallest number of copies of the largest prime that can appear, as it must appear a prime number of times).

      But none of the cases where the largest prime is (n − 1) are possible, as the first player (A) has to win all of their matches, and so does the second player (B). But they cannot both win the A v B match.

      This means we can immediately eliminate all candidate numbers of players that are one more than a prime number.

      This leaves a single candidate score sequence:

      16 players, scores = [13, 13, 11, 11, 11, 11, 11, 7, 7, 5, 5, 5, 3, 3, 2, 2]

      So if the puzzle has a solution it must be derived from this score sequence, and the sequence is enough to give the required answer without finding a viable table.

      However for a complete solution we need to demonstrate that the table can be constructed. In my first solution (above) I used a MiniZinc model to search for a viable table, and in my second solution (below) I used Landau’s Theorem to check the score sequence. The Landau condition is both necessary and sufficient for a valid table to exist, but it does not build a constructive example.

      Like

      • Jim Randell's avatar

        Jim Randell 12:47 pm on 7 June 2026 Permalink | Reply

        Instead of using MiniZinc to look for a viable table we can use Landau’s Theorem [@wikipedia] to check for a valid sequence of scores (although it does not provide us with an example table):

        Landau’s Theorem (1953)

        A score sequence for n players:

        0 ≤ s[1] ≤ s[2] ≤ …≤ s[n] ≤ n − 1

        is valid if and only if:

        s[1] + … + s[k] ≥ C(k, 2)

        for k = 1 .. n, with equality when k = n.

        (And I believe the theorem also holds providing 1 point is divided between the teams involved in each match, specifically it still holds in the case where each side in a drawn match is awarded ½ point. [J W Moon, 1963]).

        The following Python program runs in 138ms. (Internal runtime is 67ms).

        from enigma import (irange, primes, C, express, csum, compare, multiset, printf)
        
        # check the Landau condition for the score sequence of a tournament of <n> teams
        # ss = ordered score sequence (low to high) 0 <= ss[i] <= n - 1
        def landau(n, ss):
          if not (len(ss) == n and n > 1): raise ValueError("landau: invalid arguments")
          for (i, s) in enumerate(csum(ss), start=1):
            r = compare(s, C(i, 2))
            if r == -1: return False
          return (r == 0)
        
        # consider the number of players = n
        for n in irange(2, 26, step=2):
        
          # find primes less than n
          ps = list(primes.between(2, n - 1))
          if not ps: continue
        
          # count the number of matches in the tournament
          # this is the total number of points allocated
          t = C(n, 2)
        
          # express the points total using the prime numbers
          # each prime total must appear a prime number of times
          for qs in express(t, ps, qs=ps):
            # must be totals for n players
            if not (sum(qs) == n): continue
            # and the numbers of players with the lowest points must divide into 1000
            if not (1000 % qs[0] == 0): continue
        
            # generate the list of totals
            m = multiset.from_pairs(zip(ps, qs))
            ts = list(m.sorted())
            assert sum(ts) == t
            # check the Landau condition for this sequence
            if not landau(n, ts): continue
        
            # output solution
            ts.reverse()
            printf("[n={n}: ps={ps} t={t} -> scores = {ts}]")
            for (s, k) in m.multiplicity(max(m.values())):
              printf("num players = {n}; most common score = {s} (appears {k} times)")
            printf()
        

        Like

    • Ruud's avatar

      Ruud 3:23 pm on 7 June 2026 Permalink | Reply

      import peek
      import istr
      import functools
      
      @functools.cache
      def primes(n):
          return [*map(int, istr.primes(n))]
      
      def is_feasible_round_robin(wins): # Landau theorem (suggested byChatGPT)
          prefix = 0
          for k in range(1, len(wins) + 1):
              prefix += wins[k - 1]
              if prefix < k * (k - 1) // 2:
                  return False
      
          return True
      
      
      def assign(total, p, n, nteams):
          if p:
              for i in primes(total // p[0] + 1):
                  if total - p[0] * i == 0:
                      if len(p) == 1 and 10 % n[0] == 0 and sum(n) + i == nteams:
                          yield n + [i]
                      break
                  yield from assign(total - p[0] * i, p[1:], n + [i], nteams)
      
      
      for n in range(4, 28, 2):
          total = n * (n - 1) // 2
          for s in assign(total, primes(n), [], n):
              wins = []
              for i, j in zip(s, primes(n)):
                  wins.extend(i * [j])
              if is_feasible_round_robin(wins):
                  most_frequent = [j for i, j in zip(s, primes(n)) if i == max(s)]
                  peek(n, most_frequent, wins, s)
      

      Like

    • Frits's avatar

      Frits 8:01 pm on 8 June 2026 Permalink | Reply

      I totally forgot to work on yesterday’s teaser.

      # primes below 28
      P = [2, 3, 5, 7, 11, 13, 17, 19, 23]
      Pmin2 = {p - 2 for p in P}
              
      # decompose: choose numbers from <ns> so that sum(chosen numbers) equals <t>
      def decompose(t, k, ns, s=[]):
        if k == 0:
          if t == 0:
            # count last added number
            if s.count(s[-1]) in Pmin2: 
              yield s
        else:
          for n in ns:
            if s and n > s[-1]:
              # count last added number
              if s.count(s[-1]) not in Pmin2: break
            if n <= t and (not s or n >= s[-1]):
              yield from decompose(t - n, k - 1, ns, s + [n])        
      
      # Landau theorem (from Ruud's program)
      def is_feasible_round_robin(wins): 
        prefix = 0
        for k in range(1, len(wins) + 1):
          prefix += wins[k - 1]
          if 2 * prefix < k * (k - 1):
            return False
      
        return True     
      
      # number of players
      for n in range(2, 28, 2):
        pts = (n * (n - 1)) // 2
        # each prime number less than the number of players occurred as somebody's score
        if not(prms := [p for p in P if p < n]): continue
        # each such score was achieved by a prime number of players (so at least 2)
        if (todo := pts - 2 * sum(prms)) < 0: continue
        
        # determine the primes to make the missing <todo> points
        for s in decompose(todo, n - len(prms) * 2, prms):
          # the players who tied bottom shared equally the £10 booby prize
          if 1000 % (2 + s.count(2)): continue
          # is this set of wins viable (using Landau theorem)
          if not is_feasible_round_robin(wins := sorted(prms * 2 + s)): continue
          # frequency of scores
          freq = sorted(((wins.count(w), w) for w in set(wins)))
          # calculate most common scores
          mcs = [str(y) for (x, y) in freq if x == freq[-1][0]] 
          print(f"answer: {n} players,  most common score = {' or '.join(mcs)}")
      

      Like

    • Alex.T.Sutherland's avatar

      Alex.T.Sutherland 6:58 pm on 12 June 2026 Permalink | Reply

      The method used is as follows:-
      Iterate from N = 10:2:26 (does not get to the end).
      Calculate Cartesian Products to find all combinations (cp).
      Extract those vectors with a first digit 2 or 5 together with
      sum of cp*primes = games played.
      (primes are those relevant to N).
      Test each of these vectors against Landau’s Tournament Theorem
      until there is only one vector left that satisfies it.
      This gives the number of players who have played which primes.
      The vector is unique. Hence the answer to the question.
      Break when single vector found.
      My Landau’s function is similar to a python Landau function.
      Run time from N=10:? is ~ 4ms.
      My answer : Product of the Landau’s output vector = 240.

      Like

    • Tony Smith's avatar

      Tony Smith 10:59 am on 15 June 2026 Permalink | Reply

      An algorithm in which teams starting from the bottom beat the correct number of the lowest available teams can be used to construct a valid tournament from the single candidate score sequence.

      Team 16 beats 15 and 14.
      15 beats 14 and 13
      14 beats 13, 12,11
      13 beats 16, 12,11
      12 beats 16,15,11,10,9
      11 beats 16,15,,10,9,8
      10 beats16,15,14,13,9 etc

      Like

  • Unknown's avatar

    Jim Randell 11:08 am on 5 June 2026 Permalink | Reply
    Tags:   

    Teaser 2432: [Alphametic TIME] 

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

    I started with an addition sum and then consistently replaced digits by letters, with different letters used for different digits. This gave me the (very reasonable) result:

    TIMES = A + GOOD + READ

    Furthermore, I then glanced at my 24-hour digital clock and noticed that the same substitution for the four digits shown would make the display equal to TIME.

    What was the time?

    This puzzle was originally published with no title.

    [teaser2432]

     
    • Jim Randell's avatar

      Jim Randell 11:09 am on 5 June 2026 Permalink | Reply

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

      It runs in 116ms. (Internal runtime of the generated code is 2.5ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "A + GOOD + READ = TIMES"
      
      --extra="TI < 24"
      --extra="ME < 60"
      
      --answer="TIME"
      

      Solution: The time was: 15:30 (i.e. 3:30 pm).

      The sum is one of:

      TIMES = A + GOOD + READ
      15304 = 6 + 8229 + 7069
      15304 = 6 + 7229 + 8069

      Both of which give TIME = 1530.

      G and R are 7 and 8 in some order, and the values of the other letters are fixed.

      Like

    • Ruud's avatar

      Ruud 1:47 pm on 5 June 2026 Permalink | Reply

      import peek
      import istr
      
      for ti, m, e in istr.product(range(10, 24), range(6), range(10)):
          if (time := ti | m | e).all_distinct():
              rest = set(istr.digits()) - set(time)
              for s, a, g, o, d, r in istr.permutations(rest, 6):
                  if a and g and r and (times := time | s) == a + istr(":=good") + istr(":=read"):
                      peek(time, times, a, good, read)
      

      Like

    • Ruud's avatar

      Ruud 6:53 am on 6 June 2026 Permalink | Reply

      Extreme brute force:

      import peek
      import istr
      for t, i, m, e, s, a, g, o, d, r in istr.permutations(range(10), 10):
          if t and a and g and r and istr('=ti') < 24 and m < 6 and istr("=times") == a + istr("=good") + istr("=read"):
              peek(istr("=time"))
      

      Like

  • Unknown's avatar

    Jim Randell 8:20 am on 3 June 2026 Permalink | Reply
    Tags:   

    Teaser 2431: [Football league] 

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

    Teams A, B, C… (up to some letter) play in a football league. Last season, they finished in alphabetical order. This season, the league consisted of the same teams, but last season’s losers were the champions, and B finished below C, but not in the bottom three. One third of the teams finished higher this season than last; no two moved the same number of places. So, if one team went up four places, no other team will have gone up or down by four places. In particular, just one team had the same position in both seasons.

    Starting with the champions, list the order of the teams this season.

    This puzzle was originally published with no title.

    [teaser2431]

     
    • Jim Randell's avatar

      Jim Randell 8:21 am on 3 June 2026 Permalink | Reply

      If exactly one third of the teams moved up, then the number of teams must be divisible by 3, and if they are each allocated a letter of the alphabet there can be no more than 26.

      B finished below C, but not in the bottom three teams, so there must be at least 5 teams.

      So the possible number of teams is: 6, 9, 12, 15, 18, 21, 24.

      This Python program looks for the smallest possible number of teams.

      It labels the teams from 1 .. n, so last year’s position is the same as the label. It then tries all possible allocations for this year’s positions, and checks to see if all the conditions are met.

      It runs in 126ms. (Internal runtime is 55ms).

      from enigma import (
        Enumerator, irange, subsets, multiset, compare, update,
        diff, str_upper, item, join, map2str, printf
      )
      
      # check that pos: map team -> position satisfies the requirements
      def check(n, pos):
        # for each team collect the up/down and the gap
        (updown, gap) = (multiset(), multiset())
        for (k, v) in pos.items():
          d = k - v
          updown.add(compare(d, 0))
          gap.add(abs(d))
        # ups = n/3, non-movers = 1, no duplicate gaps
        return (updown.count(1) * 3 == n and updown.count(0) == 1 and not gap.is_duplicate())
      
      # solve for teams 1 .. n (= A .. ?)
      def solve(n):
        # choose values for teams B and C (1 < C < B < n - 2)
        for (C, B) in subsets(irange(2, n - 3), size=2):
          pos1 = { n: 1, 2: B, 3: C }
          # allocate the remaining positions
          ks = diff(irange(1, n), pos1.keys())
          for vs in subsets(diff(irange(1, n), pos1.values()), size=len, select='P'):
            pos = update(pos1, ks, vs)
            if check(n, pos):
              yield pos
      
      # consider multiples of 3
      for n in irange(6, 24, step=3):
        sols = Enumerator(solve(n))
        for pos in sols:
          # construct the order of the teams
          order = tuple(str_upper[k - 1] for (k, v) in sorted(pos.items(), key=item(1)))
          # output solution
          printf("{n} teams -> {order} [pos = {pos}]", order=join(order), pos=map2str(pos))
        # stop at first n with solutions
        if sols.count > 0: break
      

      Solution: The order of the teams (first to last) is: I, H, G, C, B, F, E, A, D.

      Fortunately the solution occurs with 9 teams, because this approach gets much slower as the number of teams increases. (I used a more sophisticated version of this approach to check teams up to 18, but it would take too long to check 21 and 24).

      Like

      • Jim Randell's avatar

        Jim Randell 4:37 pm on 5 June 2026 Permalink | Reply

        Here is a solution that uses the exact multiset cover solver [[ mcover() ]] from the enigma.py library to consider situations with 6 – 24 teams.

        For each team and possible position it constructs a set of tokens giving the team name, the position, the amount of movement, the direction of movement.

        So, for example in the situation with 9 teams, team I has only one possible position (it must end up first), so we add the following candidate:

        I1 → { I, p1, g8, + }

        The other teams have multiple candidates, so for Team D,we add candidates like this:

        D2 → { D, p2, g2, + }
        D3 → { D, p3, g1, + }
        D4 → { D, p4, g0, 0 }
        D5 → { D, p5, g1, − }

        D9 → { D, p9, g5, − }

        Once all the candidates are set up we use the [[ mcover() ]] solver to find a set of team/positions that give: 1 of each team, 1 of each position, 1 of each gap, n/3 teams moving up, 1 team staying in the same position, the rest of the teams moving down.

        The program is able to check all situations with 6 – 24 teams, without additional analysis, in 9.3s, and verifies that the result given above is the only solution.

        from enigma import (
          irange, multiset, mcover, compare, ediv,
          str_upper, sprintf as f, map2str, printf
        )
        
        # solve the puzzle for n teams
        def solve(n):
        
          # values are:
          #   A .. n  = team name
          #   p1 .. pn  = 1st - nth position this year)
          #   g0 .. g{n - 1}  = positions moved (abs)
          #   +|-|0  = up/down/same
          #
          # then we need to find values that give us:
          #   1 of each team
          #   1 of each position
          #   1 of each gap value
          #   n/3 up, 1 same, rest down
        
          # construct the map of <team>+<pos> -> <values>
          m = dict()
          def value(k, p, g, s): m[f("{k}{p}")] = multiset.from_seq([k, f("p{p}"), f("g{g}"), s])
          # last team (= team n) must move from last to pos 1
          k = str_upper[n - 1]
          value(k, 1, n - 1, '+')
        
          # fill out possible positions for teams A .. n-1
          for i in irange(1, n - 1):
            k = str_upper[i - 1]
            # team B cannot finish in the bottom 3 (nor team C in the bottom 4)
            M = (n - 4 if k == 'C' else n - 3 if k == 'B' else n)
            for g in irange(-(n - 2), +(n - 2)):
              p = i + g
              if not (p < 2 or p > M):
               value(k, p, abs(g), compare(g, 0, vs="+0-"))
        
          # set up the target multiset:
          tgt = multiset()
          # each team appears once
          tgt.update_from_seq(str_upper[k - 1] for k in irange(1, n))
          # each position is occupied once
          tgt.update_from_seq(f("p{i}") for i in irange(1, n))
          # each gap is occupied once
          tgt.update_from_seq(f("g{i}") for i in irange(0, n - 1))
          # n/3 ups; 1 non-mover; the rest are down
          k = ediv(n, 3)
          tgt.update_from_dict({'+': k, '0': 1, '-': n - (k + 1)})
        
          # reject situations where B is higher up the table than C
          def reject(ss):
            d = dict((x[0], int(x[1:])) for x in ss)
            try:
              return (d['B'] < d['C'])
            except KeyError:
              return False
        
          # find exact covers
          for ss in mcover(m, tgt, reject):
            # record teams by their position
            d = dict((int(x[1:]), x[0]) for x in ss)
            # output teams in order
            printf("{n} teams -> {d}", d=map2str(d, enc=""))
        
        # consider possible numbers of teams
        for n in irange(6, 24, step=3):
          solve(n)
        

        It we stop looking after the first solution is found (like my first program) the internal execution time is only 2.6ms.

        Liked by 1 person

      • Jim Randell's avatar

        Jim Randell 10:57 am on 6 June 2026 Permalink | Reply

        Here is some additional analysis that can be used to eliminate some of the cases under consideration, or provide a complete manual solution (also outlined below by John Crabtree).

        The positions of the teams in both years are 1 .. n, and the movement of each team is the difference in the positions.

        The sum of the n (signed) movements is therefore 0, so the upward movements must be balanced by the downward movements. (i.e. they have the same (absolute) sum).

        The largest possible movement is (n − 1) places, and no team moves the same (absolute) number of places. So with n teams all possible gaps from 0 to (n − 1) must be used, each exactly once.

        And these gaps can be collected into the “ups” and “downs”, each of which must have the same (absolute) sum. That value is then:

        t = tri(n − 1) / 2

        So, of the n values under consideration we can discard any where tri(n − 1) is not even.

        This leaves the following (n, t) values to consider:

        n = 9 → t = 18
        n = 12 → t = 33
        n = 21 → t = 105
        n = 24 → t = 138

        But we only have n / 3 “ups” we can use, one of which is the bottom team moving up (n − 1) places to the top to make the largest possible movement.

        We can try and see what the largest possible total for collection of n / 3 “ups” is.

        The second largest is achieved if the second to bottom team moves up to second place, this is a move of (n − 3) places.

        And the next largest is if the third to bottom moves up to third place, a move of (n − 5) places. And so on.

        So for each of our values the maximum possible sum of the “ups” (m) is:

        n = 9 → m = 8 + 6 + 4 = 18
        n = 12 → m = 11 + 9 + 7 + 5 = 32
        n = 21 → m = 20 + 18 + 16 + 14 + 12 + 10 + 8 = 98
        n = 24 → m = 23 + 21 + 19 + 17 + 15 + 13 + 11 + 9 = 128

        So, only n = 9 can achieve the required total, and then only by making the maximum possible arrangement of “up” moves.

        (More generally, the maximum possible sum is given by (2/9)n². And we need this to be at least as much as the required total tri(n − 1)/2. This is only possible when n ≤ 9).

        Hence any solution must involve 9 teams (A – I), where:

        I moves from 9th place to 1st place; a move of +8 positions
        H moves from 8th place to 2nd place; a move of +6 positions
        G moves from 7th place to 3rd place; a move of +4 positions

        This gives the required 18 “up” moves, which must be balanced by the remaining 6 teams with moves of: 0, −1, −2, −3, −5, −7 (which sum to −18).

        B must finish below C, but not in the bottom 3, which means B must drop 3 positions (to 5th), and C must drop 1 position (to 4th).

        A is the only remaining team that can drop 7 positions (to 8th).

        F is now the only team that can keep its position (remains 6th).

        And D is the only remaining team that drop 5 positions (to 9th), leaving E to drop 2 positions (to 7th).

        This provides a complete manual solution of the puzzle.

        Like

    • John Crabtree's avatar

      John Crabtree 1:49 pm on 4 June 2026 Permalink | Reply

      Let there be 3n teams.
      The maximum number of places gained is (3n – 1) + (3n – 3) + …(n + 1) with n terms.
      The minimum number of places lost is (3n – 2) + (3n – 4) + …(n) + sum[0 to (n -1)].
      And so n >= n(n-1)/2, ie n<=3
      If n = 1 or 2, the sum of the changes in places is odd. And so n = 3, with places gained = places lost = 18.
      The changes in the places are then forced: champions I up 8 to 1; H up 6 to 2; G up 4 to 3; A (not B) down 7 to 8; D (not B or C) down 5 to 9; B down 3 (max) to 5 and C down 1 to 4; E down 2 to 7; and F stays at 6.

      Like

  • Unknown's avatar

    Jim Randell 7:51 am on 31 May 2026 Permalink | Reply
    Tags:   

    Teaser 3323: Planet 9 from outer space 

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

    T’zer was the ninth planet of the Sol-Vit system. Over time, astronomers on T’zer observed each of the eight innermost planets when the lines from the sun to the planet and the planet to T’zer were perpendicular. Radar showed the eight T’zer-to-planet distances at these positions were different whole numbers of light-chrons.

    Simultaneously, measuring the angle between the sun’s centre and the planet, they then calculated each planet’s distance from the sun (they were all whole numbers of light-chrons).

    T’zer’s calculated distance from the sun was always the same whole number of light-chrons due to its circular orbit.

    Astrologers grumbled about this latter distance being under the 80 light-chrons used, historically, when producing horoscopes.

    What is T’zer’s distance from its sun (in light-chrons)?

    [teaser3323]

     
    • Jim Randell's avatar

      Jim Randell 7:52 am on 31 May 2026 Permalink | Reply

      When the distances are measured, T’zer (T), the planet (P), and the sun (S) form a right-angled triangle, where all distances are whole numbers of light-chrons, and the distance ST forms the hypotenuse of the triangle.

      Using the [[ pythagorean_triples() ]] function from the enigma.py library allows us to get a very fast solution.

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

      from enigma import (defaultdict, pythagorean_triples, printf)
      
      # collect possible SP distances by ST distance
      d = defaultdict(set)
      for (x, y, z) in pythagorean_triples(79):
        d[z].update({x, y})
      
      # look for ST distances with (at least) 8 values
      for (k, vs) in d.items():
        if len(vs) >= 8:
          printf("{k} -> {vs}", vs=sorted(vs))
      

      Solution: The distance from T’zer to the sun is 65 light-chrons.

      And the distances from the other 8 planets to the sun are: 16, 25, 33, 39, 52, 56, 60, 63 light-chrons.

      These correspond to the following 4 Pythagorean triangles with a shared hypotenuse of 65:

      (16, 63, 65)
      (25, 60, 65)
      (33, 56, 65)
      (39, 52, 65)


      The next set of triangles that would provide a solution has a hypotenuse of 85:

      (13, 84, 85)
      (36, 77, 85)
      (40, 75, 85)
      (51, 68, 85)

      Hence the limit of the hypotenuse to less than 80.

      Like

    • Ruud's avatar

      Ruud 3:23 pm on 31 May 2026 Permalink | Reply

      import math
      import itertools
      
      for c in range(10, 80):
          if len(sol := [b for b in range(1, c) if (a := math.sqrt(c * c - b * b)) == int(a)]) >= 8:
              print("T-zer:", c, "; others:", *sol)
      

      Like

  • Unknown's avatar

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

    Teaser 2435: [Swimming across the river] 

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

    Philip, the fastest swimmer, was on one bank of the River Times; Beth and Sam were directly opposite him on the other bank. Simultaneously, each of them started to swim at their own steady speed to the opposite bank, then, without pause, back to where they had started. On each stretch, Philip crossed each of the others once, and he noted the distances to the nearer bank on each occasion. These four distances were all different whole numbers of metres between 10 and 15, inclusive.

    How wide was the river?

    This puzzle was originally published with no title.

    [teaser2435]

     
    • Jim Randell's avatar

      Jim Randell 8:16 am on 29 May 2026 Permalink | Reply

      Consider Philip (starting from the far bank) and one of the other swimmers (starting from the near bank). They set off at the same time, and as Philip is the stronger swimmer, he encounters the other swimmer a distance a from the near bank. They have been swimming for the same amount of time, so the ratio of the swimming speeds is:

      P/Q = (x − a) / a

      As the stronger swimmer, Philip makes the turn first, and then passes the other swimmer for a second time at a distance b from the far bank (b < a). This time we get:

      P/Q = (2x − b) / (x + b)

      Equating these ratios gives:

      (x − a) / a = (2x − b) / (x + b)
      (x − a)(x + b) = a(2x − b)
      (x² + bx − ax − ab = 2ax − ab
      x² = (3a − b)x
      x = 3a − b

      So, we can choose a pair of values (a, b) (given 10 ≤ a < b ≤ 15) and calculate the corresponding value x (which is the width of the river).

      And we then need to find two disjoint pairs that give the same value for the width of the river.

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

      from enigma import (defaultdict, irange, subsets, union, printf)
      
      # record pairs by calculated width
      d = defaultdict(list)
      
      # consider possible pairs of distances
      for (b, a) in subsets(irange(10, 15), size=2):
      
        # calculate the width of the river = x
        # (x - a)(x + b) = (2x - b)a
        # => x = 3.a - b
        x = 3*a - b
        # check b < x / 2
        if x < 2 * b: continue
        d[x].append((a, b))
        printf("[({b}, {a}) -> x={x}]")
      
      # look for 2-different, disjoint pairs
      for (x, ps) in d.items():
        for (p1, p2) in subsets(ps, size=2):
          if len(set(p1 + p2)) != 4: continue
          # output solution
          printf("x={x} -> p1={p1} p2={p2}")
      

      Solution: The river is 32 m wide.

      The pairs of distances are (14, 10) and (15, 13).

      And the ratios of the speeds are 15/17 (≈ 0.882) and 7/9 (≈ 0.778).

      Other widths that give multiple pairs are:

      x=29 → (13, 10) and (14, 13)
      x=31 → (14, 11) and (15, 14)

      But these cannot give two disjoint pairs.

      Like

    • Frits's avatar

      Frits 12:11 pm on 2 June 2026 Permalink | Reply

      we are looking for 2 pairs (a, b) and (c, d) with a > b and c > d
      and we assume a < c.
      
      we have river width = 3a - b = 3c - d or 3.(c - a) = d - b
      as d - b <= 5 (c - a) must be 1 and (d - b) must be 3 which leads to
      pairs (a, b) and (a + 1, b + 3)
      also a + 1 > b + 3 or a > b + 2, as a may not be equal to b + 3 
      we have a >= b + 4, as a <= 14 and b >= 10 we see that a = 14, b = 10
      leading to pairs (14, 10) and (15, 13) with river width = 3.14 - 10 = 32
      

      Like

  • Unknown's avatar

    Jim Randell 10:35 am on 27 May 2026 Permalink | Reply
    Tags: ,   

    Teaser 2434: [Metal box] 

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

    In metalwork class, Pat was given a thin tin sheet, the shape of a regular polygon, each of whose sides was a two-digit number of centimetres. By sawing and discarding an identical piece from each corner, and doing some folding, he made an open-topped box. Its base was a similar-shaped polygon to the original piece, with vertical sides.

    Pat had chosen the size of the discarded pieces to maximise the volume of the box: a six-figure number of cubic centimetres. That number, written as dd/mm/yy, was his grandfather’s date of birth.

    What was the volume of the box?

    This puzzle was originally published with no title.

    [teaser2434]

     
    • Jim Randell's avatar

      Jim Randell 10:35 am on 27 May 2026 Permalink | Reply

      This Python program considers possible n-gons for n = 3 .. 10, and possible 2-digit side lengths. And looks for viable combinations where the maximum volume is a recognisable 6-digit date.

      It runs in 94ms. (Internal runtime is 25ms).

      from math import (tan, pi)
      from enigma import (irange, fdiv, polygon_area_regular, cproduct, find_max, snapf, nsplit, printf)
      
      # calculate volume of a box, starting with a regular n-gon
      # with side <x>, and then make a box by measuring <y> from
      # each corner and cutting off a quadrilateral from each corner
      def vol(n, x, y):
        # the area of the base = A
        A = polygon_area_regular(n, x - 2*y)
        # height of the sides = z
        t = pi * fdiv(n - 2, 2 * n)
        z = y * tan(t)
        # volume
        return A * z
      
      # consider possible n-gons and possible 2-digit sides (= x)
      for (n, x) in cproduct([irange(3, 10), irange(10, 99)]):
      
        # maximise the volume (= V)
        r = find_max((lambda y: vol(n, x, y)), 0, 0.5 * x)
        # volume is close to a 6-digit integer
        V = snapf(r.fv, t=1e-4, default=None)
        # V should be recognisable as a 6-digit date (dd/mm/yy)
        if V is None or V < 100000: continue
        (dd, mm, yy) = nsplit(V, k=3, base=100)
        if not (0 < dd < 32 and 0 < mm < 13): continue
        y = r.v
      
        # output solution
        printf("n={n} x={x} -> y={y:.4f} V={V} ({dd:02d}/{mm:02d}/{yy:02d})")
      

      Solution: The volume of the box is 140625 cm³ .

      So the grandfather’s DOB is 14/06/25 (14th June 1925).

      The original sheet of metal was a hexagon (6-gon) with a side length of 75 cm.

      Cuts perpendicular to the sides at a distance 12.5 cm from each corner are made to produce a hexagonal base with side length 50 cm (area = 3750 √3 cm²) and six rectangular sides with height 12.5 √3 cm.

      The volume of the box is then: (3750 √3) × (12.5 √3) = 3750 × 12.5 × 3 = 140625.

      Like

      • Jim Randell's avatar

        Jim Randell 3:30 pm on 28 May 2026 Permalink | Reply

        Using some analysis:

        The equation for the volume is:

        V(n, x, y) = (1/4) n y (x − 2y)² cot²(𝝅/n)

        And, for a fixed n and x this is at a maximum when:

        y = x/6

        hence:

        V = (1/54) n x³ cot²(𝝅 / n)

        And for an integer volume the cot²(𝝅/n) part (= c) must be rational, restricting the value of n to one of the following:

        n = 3 → c = 1/3
        n = 4 → c = 1
        n = 6 → c = 3

        And so we can simplify the program:

        from enigma import (irange, cproduct, div, cb, nsplit, printf)
        
        # rational (n, cot^2(pi/n)) pairs as (n, p/q)
        ncs = {
          3: (1, 3),
          4: (1, 1),
          6: (3, 1),
        }
        
        for ((n, (p, q)), x) in cproduct([ncs.items(), irange(10, 99)]):
        
          # calculate volume V
          V = div(n * cb(x) * p, 54 * q)
          # must be recognisable as a 6-digit date (dd/mm/yy)
          if V is None or V < 100000: continue
          (dd, mm, yy) = nsplit(V, k=3, base=100)
          if not (0 < dd < 32 and 0 < mm < 13): continue
        
          # output solution
          printf("n={n} x={x} -> V={V} ({dd:02d}/{mm:02d}/{yy:02d})")
        

        Like

  • Unknown's avatar

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

    Teaser 3322: Three houses in long lane 

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

    My two best friends and I all live along Long Lane, a row of 60 houses numbered consecutively from 1 to 60. I live at number 10, while my two friends have higher house numbers. One day, I was walking from one friend’s house to the other, and I kept my mind busy by adding up all the house numbers that I passed (not including their two house numbers). Thinking about this sum for a while, I realised that it is precisely the product of their two house numbers.

    In which two houses do my friends live?

    [teaser3322]

     
    • Jim Randell's avatar

      Jim Randell 7:02 am on 24 May 2026 Permalink | Reply

      (See also: Enigma 914, Teaser 2994, Enigma 1).

      If the friends houses are numbers A and B:

      10 < A < B ≤ 60

      Then the product A × B is the same as the sum of the house numbers in the interval [A + 1, B − 1].

      A simple search suffices to find the answer.

      This Python program runs in 70ms. (Internal runtime is 871µs).

      from enigma import (irange, subsets, printf)
      
      for (A, B) in subsets(irange(11, 60), size=2):
        if A * B == sum(irange(A + 1, B - 1)):
          printf("A={A} B={B}")
      

      But we can convert the O(n²) search into an O(n) one with a bit of analysis:

      We have:

      AB = (B − 1)B / 2 − A(A + 1) / 2

      2AB = (B − 1)B − A(A + 1)

      So we can choose a value for A, and then look for roots of the following quadratic equation in B:

      B² − B(2A + 1) − A(A + 1) = 0

      The following Python program runs in 72ms. (Internal runtime is 2.7ms).

      from enigma import (irange, quadratic, printf)
      
      # consider the lower friends number
      for A in irange(11, 59):
        # look for possible B values
        for B in quadratic(1, -(2*A + 1), -A*(A + 1), domain='Z'):
          if A < B <= 60:
            printf("A={A} B={B}")
      

      For the small search space of this puzzle, it is no faster.

      Solution: The house numbers of the friends are 14 and 35.

      The product is: 14 × 35 = 490.

      And the sum of the house numbers in the interval [15, 34] is also 490.

      Like

      • Jim Randell's avatar

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

        With a bit more analysis we can find a family of solutions where the numbers are not limited:

        If we suppose there are k houses between A and B (so B = A + k + 1).

        Then the sum of the numbers of these k houses is:

        S = kA + tri(k) = kA + k(k + 1)/2

        And the product of A and B is:

        P = A(A + k + 1)

        Equating these gives:

        k(k + 1)/2 = A(A + 1)

        tri(k) = 2 tri(A)

        And if we make the following substitution: X = 2k + 1, Y = 2A + 1 we get:

        X² − 2Y² = −1

        Which is a form of Pell’s equation [@wikipedia], and there is already code to solve such Diophantine equations in the pells.py library.

        The following program finds the first 10 non-negative candidate (k, A, B) values.

        from enigma import (div, arg, printf)
        import pells
        
        (n, i) = (arg(10, 0, int), 0)
        for (X, Y) in pells.diop_quad(1, -2, -1):
          k = div(X - 1, 2)
          A = div(Y - 1, 2)
          if k is None or A is None: continue
          B = A + k + 1
          # assert A * B == irange.sum(A + 1, B - 1)
          sol = (10 < A < B <= 60)
          printf("[X={X} Y={Y}] -> k={k} A={A} B={B}{s}", s=(" [*SOLUTION*]" if sol else ""))
          i += 1
          if i == n: break
        
        % python3 teaser3322pells.py 10
        [X=1 Y=1] -> k=0 A=0 B=1
        [X=7 Y=5] -> k=3 A=2 B=6
        [X=41 Y=29] -> k=20 A=14 B=35 [*SOLUTION*]
        [X=239 Y=169] -> k=119 A=84 B=204
        [X=1393 Y=985] -> k=696 A=492 B=1189
        [X=8119 Y=5741] -> k=4059 A=2870 B=6930
        [X=47321 Y=33461] -> k=23660 A=16730 B=40391
        [X=275807 Y=195025] -> k=137903 A=97512 B=235416
        [X=1607521 Y=1136689] -> k=803760 A=568344 B=1372105
        [X=9369319 Y=6625109] -> k=4684659 A=3312554 B=7997214
        

        Only one candidate is in the required range for the puzzle (and is found in 133µs).

        The (k, A) pairs form an increasing sequence such that tri(k) = 2 tri(A).

        Like

    • Ruud's avatar

      Ruud 7:22 am on 24 May 2026 Permalink | Reply

      for a in range(10, 60):
          for b in range(a + 1, 61):
              if (b - a - 1) * (a + b) / 2 == a * b:
                  print(a, b)
      

      Like

    • Frits's avatar

      Frits 9:12 am on 24 May 2026 Permalink | Reply

      from math import ceil
      
      # (f2 - f1)^2 = 2.f1^2 + f1 + f2
      for f1 in range(11, 60):
        # determine the minimum and maximum of (f2 - f1)
        mn = ceil((f1 * (2 * f1 + 1) + f1 + 1)**.5)
        mx = int((f1 * (2 * f1 + 1) + 60)**.5)
        for f2 in range(mn + f1, min(mx + f1 + 1, 61)):
          if (f2 - f1)**2 == f1 * (2 * f1 + 1) + f2:
            print(f"answer: {f1} and {f2}")
      

      Like

    • Ruud's avatar

      Ruud 10:43 am on 24 May 2026 Permalink | Reply

      As a one liner:

      import itertools
      
      print(*[f"{a} {b}" for a, b in itertools.combinations(range(10, 61), 2) if (b - a - 1) * (a + b) / 2 == a * b])
      

      Like

  • 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, 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 = 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 Rr, 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

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