Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:59 am on 14 November 2025 Permalink | Reply
    Tags:   

    Teaser 2487: [Alphametic sum] 

    From The Sunday Times, 23rd May 2010 [link] [link]

    I have taken an addition sum and consistently replaced digits with letters, using different letters for different digits. In this way, the sum has become:

    THE + TOP + PAPER = TIMES

    and this TEASER is odd.

    What is the number TEASER?

    This puzzle was originally published with no title.

    [teaser2487]

     
    • Jim Randell's avatar

      Jim Randell 9:00 am on 14 November 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "THE + TOP + PAPER = TIMES"
      --extra="R % 2 == 1"
      --answer="TEASER"
      

      Solution: TEASER = 809205.

      The alphametic expression corresponds to two possible sums:

      830 + 867 + 79705 = 81402
      860 + 837 + 79705 = 81402

      H and O are 3 and 6, but we don’t know which is which. The other values are fixed.

      Like

    • Ruud's avatar

      Ruud 8:41 am on 15 November 2025 Permalink | Reply

      Note that this extremely brute force solution requires istr 1.11.1 .

      import peek
      import istr
      
      with peek(show_enter=False):
          for t, h, e, o, p, a, r, i, m, s in istr.permutations(range(10)):
              if istr(":=teaser").is_odd() and istr(":=the") + istr(":=top") + istr(":=paper") == istr(":=times"):
                  peek(teaser, the, top, paper, times)
      

      Like

  • Unknown's avatar

    Jim Randell 10:02 am on 11 November 2025 Permalink | Reply
    Tags: ,   

    Teaser 2481: [Coach trip] 

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

    My wife and I took a coast-to-coast coach tour of the USA, starting at New York and travelling steadily westwards to San Francisco. The coach had rows of seats, each row consisting of a double seat on the left and a double seat on the right. In New Jersey we were in the left-hand seat of the middle row. For each new state we moved two seats clockwise. Just as we were about to move to the best seat (front right) Clive – the courier – told us thereafter to move three seats clockwise for each state. But we did get the best seat later in the holiday.

    In which state were we sitting in the best seat?

    This puzzle was originally published with no title.

    [teaser2481]

     
    • Jim Randell's avatar

      Jim Randell 10:03 am on 11 November 2025 Permalink | Reply

      I’m not sure how this puzzle is meant to work, as it surely depends on the route taken.

      I tried using Google and Apple Maps to give routes from New York to San Francisco, and the 4 suggested routes visited the following states:

      NY → NJ → PA → OH → IN → IL → IA → NE → WY → UT → NV → CA (I80)
      NY → NJ → PA → WV → OH → IN → IL → MO → NE → WY → UT → NV → CA (I70/I80)
      NY → NJ → PA → WV → OH → IN → IL → MO → OK → TX → NM → AZ → CA (I40/I5)
      NY → NJ → PA → MD → WV → VA → TN → AR → OK → TX → NM → AZ → CA (I40/I5)

      And these are just the “direct” routes. On a coach tour it is quite possible that it takes a less direct route, and visits more states.

      This Python program considers possible numbers of seats on the coach and looks for configurations where the front right seat is missed the first time (when the increment changes from +2 to +3) but hit subsequently. It then checks what that means for the four routes given above.

      from enigma import (irange, seq_get, seq2str, printf)
      
      # possible routes from New York to San Francisco
      routes = list(x.split() for x in [
        "NY NJ PA OH IN IL IA NE WY UT NV CA", # I80
        "NY NJ PA WV OH IN IL MO NE WY UT NV CA", # I70/I80
        "NY NJ PA WV OH IN IL MO OK TX NM AZ CA", # I40/I5
        "NY NJ PA MD WV VA TN AR OK TX NM AZ CA", # I40/I5
      ])
      
      # suppose the coach has n rows in front of the middle row and n rows
      # behind it, so (2n + 1) rows in total, and (4n + 2) seats, which we
      # will number 0 .. 4n + 1, going clockwise starting from the left hand
      # seat, middle row
      
      def solve(n):
        # layout of the coach
        rows = 2 * n + 1
        seats = 2 * rows
        best = n + 1
        # we start in seat 0, and move 2 each state
        k = 0
        i = 2
        # consider visiting state j (starting at 1 = NJ)
        for j in irange(2, 50):
          # move to the next seat
          k = (k + i) % seats
          # are we going to get the best seat?
          if k == best:
            if i == 2:
              # we start to move 3 seats instead
              i += 1
              k += 1
            else:
              # we got the best seats in state j
              printf("{rows} rows; {seats} seats; best @ {j}")
              # check against the routes
              for route in routes:
                r = seq_get(route, j)
                if r is not None:
                  printf("-> {route} @ {r}", route=seq2str(route))
      
      # consider possible coach configurations
      for n in irange(1, 20):
        solve(n)
      

      There appear to be two possible scenarios:

      (1) In a coach with 7 rows of seats (= 28 individual seats).

      The seats are laid out as follows (viewed from above):

      [01] - [02] = front
      [03] - [04]
      [05] - [06]
      [07] - [08]
      [09] - [10]
      [11] - [12]
      [13] - [14] = back

      In NJ (state 1) the setter is in seat 07 and proceeds as follows:

      07 (+2) 03 (+3) 04 (+3) 10 (+3) 13 (+3) 07 (+3) 01 (+3) 06 (+3) 12 (+3) 11 (+3) 05 (+3) 02 [best]

      Which means being in the best seat in state 12.

      The shortest route given above only has 10 states after NJ, so is not possible, and for the remaining three routes this is the final state, California.

      (2) In a coach with 11 rows of seats (= 44 individual seats).

      The seats are laid out as:

      [01] - [02] = front
      [03] - [04]
      [05] - [06]
      [07] - [08]
      [09] - [10]
      [11] - [12]
      [13] - [14]
      [15] - [16]
      [17] - [18]
      [19] - [20]
      [21] - [22] = back

      In NJ (state 1) the setter is in seat 11 and proceeds as follows:

      11 (+2) 07 (+2) 03 (+3) 04 (+3) 10 (+3) 16 (+3) 22 (+3) 17 (+3) 11 (+3) 05 (+3) 02 [best]

      Which means being in the best seat in state 11.

      For the routes given this could be California, Nevada, or Arizona.

      And if the route were to visit more states there are further solutions.

      The next is in a coach with 23 rows of seats (= 92 individual seats), and happens in state 22 (which would require a less direct route, possibly revisiting states).


      If the list of states visited had been given as:

      NY → NJ → PA → OH → IN → IL → IA → NE → WY → UT → NV → CA (I80)

      (i.e. the most direct of the suggested routes).

      Then the only possible solution is a coach with 11 rows of seats, and the setter gets the best seat in state 11 = California.

      And the published solution is “California”, so perhaps this is what the setter had in mind.

      Like

  • Unknown's avatar

    Jim Randell 6:50 am on 9 November 2025 Permalink | Reply
    Tags:   

    Teaser 3294: Four by four 

    From The Sunday Times, 9th November 2025 [link] [link]

    With the school inspector scheduled to visit her school, Tina is taking extra care in preparing her lesson plan. The lesson deals with areas, shapes and symmetries. She has produced soft cards on which have been printed a four by four grid, and will ask the pupils to cut the grid into two shapes of equal area, but by only cutting along the lines. She wanted them to create as many different possible shapes in this way, explaining that two shapes are different only if you can’t make one the same as the other by rotating it, turning it over, or both. It turned out that the maximum number of different shapes was the same as the number of pupils in the class.

    How many pupils are there in the class?

    [teaser3294]

     
    • Jim Randell's avatar

      Jim Randell 7:30 am on 9 November 2025 Permalink | Reply

      (See also: BrainTwister #29, Enigma 54).

      This Python program considers possible dissections of the grid into two connected shapes, and then records a canonical form of the shapes to find the number of different shapes.

      It runs in 233ms. (Internal runtime is 154ms).

      from enigma import (Accumulator, irange, subsets, grid_adjacency, filter2, repeat, printf)
      
      # adjacency matrix for a 4x4 grid
      adj = grid_adjacency(4, 4)
      
      # the pieces (individual cells in the grid)
      pieces = set(irange(16))
      
      # rotate the grid 90 degrees clockwise
      rot = [3, 7, 11, 15, 2, 6, 10, 14, 1, 5, 9, 13, 0, 4, 8, 12]
      def rotate(ps): return set(rot[p] for p in ps)
      
      # mirror the grid vertically
      mir = [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12]
      def mirror(ps): return set(mir[p] for p in ps)
      
      # slide a shape to the bottom LH corner
      (Bs, Ls) = ({0, 1, 2, 3}, {0, 4, 8, 12})
      def slide(ps):
        while Bs.isdisjoint(ps):
          ps = set(p - 4 for p in ps)
        while Ls.isdisjoint(ps):
          ps = set(p - 1 for p in ps)
        return ps
      
      # find the canonical form for a shape
      def canonical(ps):
        # find the minimal representation
        r = Accumulator(fn=min)
        # consider the possible rotations of the shape
        for ss in repeat(rotate, ps, 3):
          # also consider reflections
          for xs in repeat(mirror, ss, 1):
            # slide to minimal position
            xs = slide(xs)
            r.accumulate(tuple(sorted(xs)))
        # return the minimum value
        return r.value
      
      # are the pieces in <ps> connected?
      def is_connected(ps, cs=None):
        if ps and not cs:
          # move a piece from ps to cs
          ps = set(ps)
          cs = {ps.pop()}
        # move pieces in ps that are connected to cs into cs
        while ps:
          # partition ps into those connected to cs and those that aren't
          (ps, pcs) = filter2((lambda p: cs.isdisjoint(adj[p])), ps)
          if not pcs: return False
          cs.update(pcs)
        return True
      
      # collect canonical shapes
      ss = set()
      
      # choose an 8-subset of the pieces
      for ps in subsets(pieces, size=8, fn=set):
        # and the remaining pieces
        rs = pieces.difference(ps)
        # check both sets are connected
        if not (is_connected(ps) and is_connected(rs)): continue
        # collect canonical shapes
        ss.add(canonical(ps))
      
      # output canonical shapes
      for (i, ps) in enumerate(sorted(ss), start=1): printf("{i}: {ps}")
      printf("{n} canonical shapes", n=len(ss))
      

      (The internal runtime can easily be reduced to 41ms by noting that each shape must include a corner cell, and a cell adjacent to it, so we only need to look for 6 other cells to go with them).

      Solution: There are 19 pupils in the class.

      These are the shapes (in grey) that can be generated:

      Although these are not necessarily the only ways to cut the shapes out of the grid. (Note that cutting out shape 9 and shape 15 both leave a complementary shape (in orange) that corresponds to shape 18).

      Like

    • Frits's avatar

      Frits 3:58 pm on 9 November 2025 Permalink | Reply

      Version 1.

      I had a version without itertools.combinations but that was less efficient.

      from collections import defaultdict
      from itertools import combinations
      
      N = 4
      N2 = N * N
      H = N2 // 2
      
      def I(i, j, n=N):
        return i * n + j
      
      def rotate(s, n):
        r = [None] * n * n
        for i in range(n):
          for j in range(n):
            r[I(j, n - i - 1, n)] = s[I(i, j, n)]
        return tuple(r)
      
      def mirror(s, n):
        r = [None] * n * n
        for i in range(n):
          for j in range(n):
            r[I(j, i, n)] = s[I(i, j, n)]
        return tuple(r)
      
      # is <s> the smallest after rotations/mirrors?
      def is_canonical(s):
        z4 = (0, ) * N
        # go from 4x4 matrix to a 3x3 matrix if all data in SE corner
        if s[:N] == z4:
          if tuple(s[i * N] for i in range(N)) == z4:
            s = tuple(s[i] for i in range(N, N2) if i % N)
          
        # dimension of <s>
        n = int(len(s)**.5) 
        
        # rotations of the square
        if (rot1 := rotate(s, n)) < s: return False
        if (rot2 := rotate(rot1, n)) < s: return False
        if (rot3 := rotate(rot2, n)) < s: return False
        # mirrors
        if (mirror(s, n)) < s: return False
        if (mirror(rot1, n)) < s: return False
        if (mirror(rot2, n)) < s: return False
        if (mirror(rot3, n)) < s: return False
        
        return True
      
      # adjacent squares  
      d_a = defaultdict(list)
      for a in range(N):
        for b in range(N):
          for i, c in enumerate([a - 1, a, a + 1]):
            for j, d in enumerate([b - 1, b, b + 1]):
              if 0 <= c < N and 0 <= d < N and i + j in {1, 3}:
                d_a[I(a, b)] += [I(c, d)]
      
      # are all positive elements in <s> connected with each other
      def _connected(k, s, adj, curr):
        global conn_seen
        if len(conn_seen) == H: 
          yield conn_seen
        elif k:
          # add positive adjacent squares
          conn_seen |= set(x for x in adj[curr] if s[x])    
          for nxt in adj[curr]:
            if s[nxt]:
              yield from _connected(k - 1, s, adj, nxt)
      
      def connected(k, s, adj, curr):
        global conn_seen
        conn_seen = {curr}
        # check if all selected squares are connected
        for conn in _connected(k, s, adj, curr): 
          return True
        
        return False
      
      sols = []         
      # check all shapes of <H> squares
      for cmb in combinations(range(N2), H):
        s = tuple((1 if i in cmb else 0)for i in range(N2))
        if not connected(H - 1, s, d_a, cmb[0]): continue
        # check if <s> is smallest of all rotations/mirrors
        if is_canonical(s):
          oth = tuple(1 - x for x in s)
          # determine first square not selected
          f = next(i for i, x in enumerate(oth) if x)
          if not connected(H - 1, oth, d_a, f): continue
          sols.append(s)
          
      print("answer:", len(sols))
      

      Like

      • Frits's avatar

        Frits 7:18 am on 11 November 2025 Permalink | Reply

        When comparing my “connected()” to Brian Gladman’s “is_connected()” I noticed that my version was slow. This one is more efficient. The overall program now also has a decent internal runtime.

        # are all positive elements in <s> connected with each other
        def _connected(s, adj, curr, ss=set()):
          global conn_seen
          # add positive adjacent squares
          conn_seen |= set(x for x in adj[curr] if s[x])    
          if len(conn_seen) == H: 
            yield conn_seen
          else:
            for nxt in adj[curr]:
              if s[nxt] and nxt not in ss:
                yield from _connected(s, adj, nxt, ss | {nxt})
        
        def connected(s, adj, curr):
          global conn_seen
          conn_seen = {curr}
          # check if all selected squares are connected
          for _ in _connected(s, adj, curr, {curr}): 
            return True
          
          return False
        

        Like

        • Brian Gladman's avatar

          Brian Gladman 2:42 pm on 11 November 2025 Permalink | Reply

          @Frits
          The version on which you comment above was:

          # given a set <p> of (x, y) positions for unit squares arranged 
          # on a rectangular grid, return true if the set of squares form
          # a connected region
          def is_connected(p):
            # if two sets of squares in a list of such sets have 
            # any squares that are connected, combine the two sets 
            def combine(s):
              for s1, s2 in combinations(s, 2):
                if any(y in adj[x] for x in s1 for y in s2):
                  s[s.index(s1)].update(s2)
                  s.remove(s2)
                  return True
              return False
            sets = [set([n]) for n in p]
            # combine any sets that have connected members
            while combine(sets):
              pass
            # are all squares connected?
            return len(sets) == 1
          

          After thinking about it, I realised it was pretty inefficient so I updated it.

          Like

    • Frits's avatar

      Frits 7:26 am on 10 November 2025 Permalink | Reply

      Version 2.

      More code (not my priority) but more efficient (my priority).
      When cutting from side to side all squares in both shapes have to be connected.
      Choosing 4 starting positions for curring is enough to generate all possible shapes.

      from collections import defaultdict
      
      N = 4
      N2 = N * N
      H = N2 // 2
      
      # direction moving from x to y: Up/Right/Down/Left: 0/1/2/3
      dir = lambda x, y: (0 if x[0] > y[0] else 1 if y[1] > x[1] else
                          2 if x[0] < y[0] else 3)
      
      def I(i, j, n=N):
        return i * n + j
      
      def rotate(s, n):
        r = [None] * n * n
        for i in range(n):
          for j in range(n):
            r[I(j, n - i - 1, n)] = s[I(i, j, n)]
        return tuple(r)
      
      def mirror(s, n):
        r = [None] * n * n
        for i in range(n):
          for j in range(n):
            r[I(j, i, n)] = s[I(i, j, n)]
        return tuple(r)
      
      # is squaee list <s> the highest after rotations/mirrors?
      def is_canonical(s):
        z4 = (0, ) * N
        # go from N x N matrix to a N-1 x N-1 matrix if all data in NW corner
        if s[N2 - N:] == z4:
          if tuple(s[i * N + N - 1] for i in range(N)) == z4:
            s = tuple(s[i] for i in range(N2 - N) if (i + 1) % N)
          
        # dimension of <s>
        n = int(len(s)**.5) 
         
        # rotations of the square
        if (rot1 := rotate(s, n)) > s: return False
        if (rot2 := rotate(rot1, n)) > s: return False
        if (rot3 := rotate(rot2, n)) > s: return False
        # mirrors
        if (mirror(s, n)) > s: return False
        if (mirror(rot1, n)) > s: return False
        if (mirror(rot2, n)) > s: return False
        if (mirror(rot3, n)) > s: return False
         
        return True
      
      # check cutting sequence <s> and return list of squares
      def gen_squares(s):
        # containers for shape 1 and 2
        shape1, shape2 = set(), set()
        # process the directions of the cuts
        for x, y in zip(s, s[1:]):
          match(dir(x, y)):
            case 0: # U: add square east of it
              shape1.add(I(y[0], y[1]))
              shape2.add(I(y[0], y[1] - 1))
            case 1: # R: add square south of it
              shape1.add(I(x[0], y[1] - 1))  
              shape2.add(I(x[0] - 1, y[1] - 1))  
            case 2: # D: add square west of it
              shape1.add(I(x[0], y[1] - 1))
              shape2.add(I(x[0], y[1]))
            case 3: # L: add square north of it
              shape1.add(I(x[0] - 1, y[1]))   
              shape2.add(I(x[0], y[1]))    
        
        ns = True
        # keep coloring adjacent fields of shape 1 with same color
        while ns:
          ns = set()
          for s in shape1:
            # if adjacent square not in shape2 then add to shape1
            ns |= set(a for a in d_a[s] if a not in shape1 and 
                                           a not in shape2)
          shape1 |= ns 
        
        # both shapes must be of the same size
        if len(shape1) != H:
          return None
        return tuple(1 if i in shape1 else 0 for i in range(N2))
        
      # generate a cutting sequence from side to side and return squares
      def cut(curr, ss=[]):
        # stop when reaching a side 
        if len(ss) > 1 and not {0, N}.isdisjoint(ss[-1]):
          # do we have <H> squares per shape?
          if (sqs := gen_squares(ss)) is not None:
            yield sqs
        else:
          # move the pair of scissors in 4 directions
          for mv in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
            new = (curr[0] + mv[0], curr[1] + mv[1])
            # if not out of bounds
            if {-1, N + 1}.isdisjoint(new) and new not in ss:
              # we must move inside the NxN square after first cut
              if ss or {0, N}.isdisjoint(new): 
                yield from cut(new, ss + [new])
      
      # adjacent squares  
      d_a = defaultdict(list)
      for a in range(N):
        for b in range(N):
          for i, c in enumerate([a - 1, a, a + 1]):
            for j, d in enumerate([b - 1, b, b + 1]):
              if 0 <= c < N and 0 <= d < N and i + j in {1, 3}:
                d_a[I(a, b)] += [I(c, d)]
                 
      seen = set()
      # start cutting near last column
      for st in [(0, 3), (1, 4), (2, 4), (3, 4)]:
        # generate squares by performing cuts
        for s in cut(st, [st]):
          # check if <s> is highest of all rotations/mirrors
          if is_canonical(s):
            seen.add(s)
      
      print("answer:", len(seen)) 
      

      Like

    • Alex.T.Sutherland's avatar

      Alex.T.Sutherland 2:19 pm on 15 November 2025 Permalink | Reply

      I have a pattern that I don’t think is included in the above results.Should the answer be 20?
      Am I wrong?

         1   1   0   0
         1   1   1   0
         1   1   1   0
         0   0   0   0

      Let me know what you think.

      Like

      • Jim Randell's avatar

        Jim Randell 2:29 pm on 15 November 2025 Permalink | Reply

        @Alex: The 0’s are the same as shape 15 in my diagram. And the 1’s are the same as shape 18.

        Like

        • Alex.Sutherland's avatar

          Alex.Sutherland 4:24 pm on 20 November 2025 Permalink | Reply

               Thank you for your reply.
               My error .I have cleared up the problem.
               The following may be of interest.

               It is concerned with Teaser 3295 (Always as the Crow flies.)

               Quad = abcd; Vertical diagonal = S; Horz diagonal = F;

               My answer is based on the equation a^2 + c^2 = b^2 + d^2 simce it is
               an orthodiagonal quad .
               This is the equation for Pythagorean Trebles (x^2 * y^2 = R^2);
               I have a list (from a previous puzzle) of such trebles.
               From the list of (x y R) trebles I get a set of possible values
          for ‘abcd’.
               Choosing at least 2  with equal largest R  and with  both x and y
          <100.

                x     y     R
               13    84    85;        % Obtained from the list
               36    77    85;
               40    75    85;
               51    68    85;

               Choose 2 from the set of 4….(6). eg [a c] = [40 75] ;[b d] = [13 84]
               Using each choice iterate S (99->50) to find if F is integer.In
          this case
               I get 3 answers for ‘abcdSF’.
               Choose that whose S is maximum.
               The periphery = a+b+c+d.
               In my  program the answer is a 3 digit number with digits
          increasing consectutively.
               Diagonal difference = 7 miles.
               Run time = 17ms . PC using Pentium processor .Non Python.

               As a sideline the locations were found to form a cyclic quad as
          well as being
               ortho diagonal. ie ac + bd = S*F;
               Am I in the right ball park or do I start again?

                               Regards
                                      A.T.Sutherland.

          Like

          • Jim Randell's avatar

            Jim Randell 2:41 pm on 21 November 2025 Permalink | Reply

            @Alex:

            I also used the condition a^2 + c^2 = b^2 + d^2 to find candidate orthodiagonal quadrilaterals (my program is [ here ]).

            Although I did not assume that the actual value of the sums must be a perfect square (and so form a Pythagorean triple), as this is not true in general (e.g. 1^2 + 8^2 = 4^2 + 7^2 = 65, which is not a perfect square), and I wanted to make sure I found all candidate arrangements.

            But, if you do assume this, you can still find all 11 possible candidate orthodiagonal quadrilaterals with different 2-digit sides (or 12 if you count a solution that has the same sides as one of the other candidates, but a smaller AS diagonal) that do not have any three of the vertices collinear. And one of these is the solution to the puzzle.

            Four of the 11 candidate quadrilaterals are convex and cyclic as well as orthodiagonal, and for these the intersection of the diagonals cuts each of the diagonals into 2 integer lengths, so they can be composed from 4 Pythagorean triangles.

            However, there are candidate solutions where three of the vertices are collinear, that are not found if you only use Pythagorean triples to construct opposite sides).

            I will post some additional notes on my solution on Sunday (on the page for Teaser 3295).

            Like

  • Unknown's avatar

    Jim Randell 11:00 am on 7 November 2025 Permalink | Reply
    Tags: ,   

    Teaser 2480: [I saw three ships] 

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

    At the start of the computer war game Midway, the three Japanese aircraft carriers Akagi, Kaga and Soryu are in positions 120 kilometres from each other. At a signal, all three ships set sail at the same speed. Akagi always sails directly towards Kaga until they meet. Similarly, Kaga always sails towards Soryu, whilst Soryu always sails towards Akagi.

    How far does Akagi sail before she meets Kaga?

    This puzzle was originally published with no title.

    [teaser2480]

     
    • Jim Randell's avatar

      Jim Randell 11:01 am on 7 November 2025 Permalink | Reply

      See my comments on: Puzzle #13.

      If the objects start at the vertices of a regular n-gon with sides L, then the distance and they each travel before they meet is given by:

      d = L / (1 – cos(2𝛑/n))

      In this puzzle we have n = 3 and L = 120 km.

      d = 120 / (1 − (−0.5))
      d = 120 / 1.5
      d = 80 km

      Solution: The ships meet after travelling 80 km.

      Like

  • Unknown's avatar

    Jim Randell 10:00 am on 4 November 2025 Permalink | Reply
    Tags:   

    Teaser 2512: [A square, a cube, a prime] 

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

    Using each of the digits 0 to 9 exactly once, I have written down two three-digit numbers and a four-digit number.

    In no particular order, one of them is a perfect square, one is a perfect cube, and the other is a prime.

    If I told you which of the square, the cube and the prime was the smallest then it would be possible for you to work out what those three numbers are.

    What are they?

    This puzzle was originally published with no title.

    [teaser2512]

     
    • Jim Randell's avatar

      Jim Randell 10:01 am on 4 November 2025 Permalink | Reply

      I wondered if we are meant to exclude numbers that fall into more than one category (i.e. 729 and 4096 are both squares and cubes). It turns out you get the same answer if you exclude them or not.

      I recently extended the [[ choose() ]] function in enigma.py, so that you can specify different collections for each choice, and we can use that functionality to solve this puzzle.

      The following Python program runs in 88ms. (Internal runtime is 11ms).

      from enigma import (powers, primes, is_duplicate, choose, filter_unique, printf)
      
      # find 3 and 4 digit, squares, cubes, primes with no repeated digits
      def numbers(seq):
        (n3s, n4s) = (list(), list())
        for n in seq:
          if n < 100: continue
          if n > 9999: break
          if is_duplicate(n): continue
          (n3s if n < 1000 else n4s).append(n)
        return (n3s, n4s)
      
      (sq3, sq4) = numbers(powers(10, 99, 2))  # squares
      (cb3, cb4) = numbers(powers(5, 21, 3))  # cubes
      (pr3, pr4) = numbers(primes.between(100, 9999))  # primes
      
      # find combinations of (square, cube, prime) that use 10 different digits
      check = lambda *ns: not is_duplicate(*ns)
      ss = list()
      for nss in [(sq3, cb3, pr4), (sq3, cb4, pr3), (sq4, cb3, pr3)]:
        ss.extend(choose(nss, check, multi_vs=1, multi_fns=0))
      
      # find candidates unique by the category of the smallest number
      rs = filter_unique(ss, (lambda ss: ss.index(min(ss)))).unique
      
      # output solution
      for (sq, cb, pr) in rs:
        printf("square = {sq}, cube = {cb}, prime = {pr}")
      

      Solution: The numbers are: 324 (square), 6859 (cube), 701 (prime).

      There are 19 candidate sets of numbers (or 16 if you remove those containing numbers that are both squares and cubes), but only one of them is uniquely identified if we are told that the square is the smallest number.

      Like

    • ruudvanderham's avatar

      ruudvanderham 12:09 pm on 4 November 2025 Permalink | Reply

      import istr
      import collections
      
      squares = [i for i in istr.range(100, 10000) if i.all_distinct() and i.is_square()]
      cubes = [i for i in istr.range(100, 10000) if i.all_distinct() and i.is_cube()]
      primes = [i for i in istr.range(100, 10000) if i.all_distinct() and i.is_prime()]
      
      solutions = collections.defaultdict(list)
      for square in squares:
          for cube in cubes:
              for prime in primes:
                  if len(square | cube | prime) == 10 and (square | cube | prime).all_distinct():
                      solutions[(square == min(square, cube, prime), cube == min(square, cube, prime), square == min(square, cube, prime))].append(
                          (square, cube, prime)
                      )
      
      for solution in solutions.values():
          if len(solution) == 1:
              print(solution[0])
      

      Like

  • Unknown's avatar

    Jim Randell 7:02 am on 2 November 2025 Permalink | Reply
    Tags:   

    Teaser 3293: Three sisters 

    From The Sunday Times, 2nd November 2025 [link] [link]

    I have three granddaughters Jay, Kay and Elle. I set Jay and Kay a test and asked each of them to produce a list of positive numbers that used just nine digits [in total] with no digit occurring more than once. I wanted the majority of the numbers in each list to be odd and the majority of the numbers in each list to be perfect squares.

    Jay’s list (which contained more numbers than Kay’s) added up to give the year of Elle’s birth, whereas Kay’s list added up to give the year in which Elle will be 25.

    In one of the lists the highest number was a perfect square.

    What (in increasing order) were the numbers in that list?

    [teaser3293]

     
    • Jim Randell's avatar

      Jim Randell 8:36 am on 2 November 2025 Permalink | Reply

      Assuming the puzzle is set at the current time (i.e. during 2025). Then the latest Elle can be born is 2025, and if she has not reached her 25th birthday yet, the earliest she can be born is 2000. So Jay’s numbers must sum to between 2000 and 2025. (And Kay’s numbers sum to between 2025 and 2050).

      I also assumed that Jay and Kay completed their test successfully, and the lists of numbers they produced satisfied the given requirements.

      My first program was a bit slow. This one is a bit more involved, but both find the same answer.

      The following Python program runs in 225ms. (Internal runtime is 155ms).

      from enigma import (
        defaultdict, irange, powers, nsplit, seq_is_distinct, nconcat,
        divc, subsets, disjoint_union, intersect, cproduct, printf
      )
      
      # map squares to digit contents
      sq = dict()
      for s in powers(1, 45, 2):
        ds = nsplit(s)
        if seq_is_distinct(ds):
          sq[s] = ds
      
      # available digits
      digits = set(irange(0, 9))
      
      # allocate remaining digits <rs> for units digits <us>
      def allocate(us, rs, ns=list()):
        k = len(rs)
        if k == 1:
          # there should be 1 digit remaining unused
          yield us + ns
        elif k > 1 and us:
          # choose some digits to add to the next unit digit
          u = us[0]
          for ds in subsets(rs, max_size=3, select='P', fn=list):
            if ds and ds[0] == 0: continue
            n = nconcat(ds + [u])
            if 0 < n <= 2050:
              yield from allocate(us[1:], rs.difference(ds), ns + [n])
      
      # record candidate sequences for J and K
      (Js, Ks) = (defaultdict(set), defaultdict(set))
      
      # consider the length of the list
      for k in irange(3, 6):
        # majority size
        m = divc(k + 1, 2)
      
        # choose some squares
        for sqs in subsets(sq.keys(), size=m, fn=list):
          # digits used
          ds = disjoint_union(sq[s] for s in sqs)
          if not ds: continue
      
          # count odd numbers so far, and remaining digits
          odd = sum(s % 2 for s in sqs)
          rs = digits.difference(ds)
      
          # choose units digits for the remaining numbers
          for us in subsets(rs, size=k - m, fn=list):
            if odd + sum(u % 2 for u in us) < m: continue
            # allocate the remaining digits
            for ns in allocate(us, rs.difference(us)):
              ss = tuple(sorted(sqs + ns))
              t = sum(ss)
              # record candidates by birth year
              if 2000 <= t <= 2025: Js[t].add(ss)
              if 2025 <= t <= 2050: Ks[t - 25].add(ss)
      
      # find solutions
      for j in intersect([Js.keys(), Ks.keys()]):
        for (J, K) in cproduct([Js[j], Ks[j]]):
          if len(J) > len(K) and (max(J) in sq or max(K) in sq):
            printf("J={J} [{j}]; K={K} [{k}]", k=j + 25)
      

      Solution: The numbers are: 305, 784, 961.

      Jay’s list is:

      [4, 9, 625, 1387]; sum = 2025
      (4 numbers; 3 squares; 3 odd)

      So Elle was born this year (2025), and will be 25 in the year 2050.

      Kay’s list is:

      [305, 784, 961]; sum = 2050
      (3 numbers; 2 squares; 2 odd)

      In Kay’s list the highest number (961) is a square (= sq(31)), and so this list is the required answer.

      Like

    • Jim Olson's avatar

      Jim Olson 4:11 pm on 10 November 2025 Permalink | Reply

      I am perhaps missing something but I can’t see how the solution meets the requirements of the puzzle. Each list is supposed to be a list of positive numbers. Zero is not a positive number.

      Like

      • Jim Randell's avatar

        Jim Randell 4:33 pm on 10 November 2025 Permalink | Reply

        @Jim: The numbers in the lists are all positive ((4, 9, 625, 1387) and (305, 784, 961)), but that doesn’t mean the individual digits have to be non-zero. Many positive numbers are written using a 0 digit.

        Like

  • Unknown's avatar

    Jim Randell 7:21 am on 31 October 2025 Permalink | Reply
    Tags:   

    Teaser 2470: [Peter’s birthday] 

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

    Peter told me that one of his teenage birthdays had occurred on the same day of the week as the day that he was born.

    Furthermore, on that birthday the 10 digits that made up his age, the date (dd/mm/yy) and the year of his birth (yy) were all different.

    Knowing these facts, and that Peter was born after the summer solstice, I was able to work out his date of birth.

    What is Peter’s date of birth?

    This puzzle was originally published with no title.

    [teaser2470]

     
    • Jim Randell's avatar

      Jim Randell 7:21 am on 31 October 2025 Permalink | Reply

      The birthday must be 13 – 19 (teenage), so the 1 is already used. This means the month must be chosen from 02 – 09, so the 0 is already used. And this means that the date must be 23 – 29, so the 2 is already used. So we know where the digits 0, 1, 2 are used.

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --invalid=""  # allow leading zeros
      
      # date of birth is: AB/CD/EF
      # date of birthday is: AB/CD/GH
      # age is: IJ (13..17)
      "EF + IJ = GH"
      
      # valid dates
      "0 < AB < 32"  # day
      "5 < CD < 13"  # month (in the second half of the year)
      "12 < IJ < 20" # age
      "(6, 20) < (CD, AB) < (12, 21)" # between solstices
      
      # birthdate and birthday are same day of week
      --code="from datetime import date"
      --code="dow = lambda d, m, y: date(1900 + y, m, d).isoweekday()"
      "dow(AB, CD, EF) == dow(AB, CD, GH)"
      
      # [optional] assign known values
      --assign="C,0"
      --assign="I,1"
      --assign="A,2"
      
      # [optional] neaten up output
      --template="AB/CD/EF -> AB/CD/GH; age = IJ"
      --solution=""
      --verbose="1-H"
      

      Solution: Peter’s date of birth is 23rd September 1948.

      Which was a Thursday.

      Peter’s 17th birthday was on Thursday, 23rd September 1965.

      An alternative that would work is Monday, 29th March 1948, but this is ruled out by the condition Peter’s birthday is after the summer solstice (in the northern hemisphere).

      Like

    • Ruud's avatar

      Ruud 6:56 am on 1 November 2025 Permalink | Reply

      import datetime
      
      
      day0 = datetime.datetime(1900, 1, 1)
      
      while day0.year < 2020:
          day0 += datetime.timedelta(1)
          for age in range(10, 20):
              try:
                  day1 = datetime.datetime(day0.year + age, day0.month, day0.day)
                  if day0.weekday() == day1.weekday():
                      s = f"{day1.year}"[2:] + f"{day1.month:02d}" + f"{day1.day:02d}" + f"{day0.year}"[2:] + f"{age:02d}"
                      if len(set(s)) == 10:
                          print(f"Born: {day0:%Y-%m-%d} Age:{age}")
              except ValueError:
                  ...
      

      Like

  • Unknown's avatar

    Jim Randell 9:22 am on 28 October 2025 Permalink | Reply
    Tags:   

    Teaser 2497: [Measuring jugs] 

    From The Sunday Times, 1st August 2010 [link] [link]

    I have two jugs that hold whole numbers of pints, and together they hold two gallons [= 16 pints]. Using only the two jugs and a tap, discarding water as necessary, I can end up with a total of any whole number of pints from 1 to 16 in the two jugs combined. If I want to end up with exactly one pint in the most economical way possible, I have to draw more water than if I want to end up with exactly four pints.

    What are the capacities of the two jugs?

    This puzzle was originally published with no title.

    [teaser2497]

     
    • Jim Randell's avatar

      Jim Randell 9:22 am on 28 October 2025 Permalink | Reply

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

      from enigma import (Enumerator, irange, printf)
      
      # solve for capacities A and B
      # n = max number of steps to consider
      def solve(A, B, n=50):
        k = 0
        r = dict()  # r maps (vA + vB) positions to minimal measure (volume drawn)
        p = dict()  # p maps (vA, vB) to minimal measure
        vs = {(0, 0, 0)}  # (vol in A, vol in B, vol drawn)
        while vs and k < n:
          ss = Enumerator(vs)
          vs = set()
          for (vA, vB, vD) in ss:
            # skip positions where we have already check a lower measure
            if (vA, vB) in p and p[(vA, vB)] <= vD: continue
            p[(vA, vB)] = vD
      
            # check current amounts
            vT = vA + vB
            # record vT -> vD in r, if vD is minimal
            if vT not in r or (vD < r[vT]): r[vT] = vD
      
            # move some water around:
            # fill A from the tap
            if vA < A:
              vs.add((A, vB, vD + A - vA))
            # fill B from the tap
            if vB < B:
              vs.add((vA, B, vD + B - vB))
            # discard A
            if vA:
              vs.add((0, vB, vD))
            # discard B
            if vB:
              vs.add((vA, 0, vD))
            # transfer water from A to B
            if vA and vB < B:
              if vT > B:
                vs.add((vT - B, B, vD))
              else:
                vs.add((0, vT, vD))
            # transfer water from B to A
            if vB and vA < A:
              if vT > A:
                vs.add((A, vT - A, vD))
              else:
                vs.add((vT, 0, vD))
          # move to the next step
          k += 1
        # return result
        return r
      
      # make all values from 1 to 16 pints
      ts = set(irange(1, 16))
      
      # consider the capacity of the smaller jug
      for A in irange(1, 8):
        # capacity of the larger jug
        B = 16 - A
        # find minimal configurations
        r = solve(A, B)
        # check we can make all target values
        if not ts.issubset(r.keys()): continue
        # and that making 1 pint requires drawing more water than 4 pints
        if not (r[1] > r[4]): continue
      
        # output configuration
        printf("capacities = [{A}, {B}]")
        for k in irange(1, 16):
          printf("  {k} pints -> {d} pints drawn", d=r[k])
        printf()
      

      Solution: The jugs have capacities of 7 pints and 9 pints.

      We can make 1 pint by drawing 28 pints (and discarding 27 pints) as follows (13 steps):

      fill 7 → (7/7, 0/9; 7 total; 7 drawn)
      pour 7 into 9 → (0/7, 7/9; 7 total; 7 drawn)
      fill 7 → (7/7, 7/9; 14 total; 14 drawn)
      pour 7 into 9 → (5/7, 9/9; 14 total; 14 drawn)
      discard 9 → (5/7, 0/9; 5 total; 14 drawn)
      pour 7 into 9 → (0/7, 5/9; 5 total; 14 drawn)
      fill 7 → (7/7, 5/9; 12 total; 21 drawn)
      pour 7 into 9 → (3/7, 9/9; 12 total; 21 drawn)
      discard 9 → (3/7, 0/9; 3 total; 21 drawn)
      pour 7 into 9 → (0/7, 3/9; 3 total; 21 drawn)
      fill 7 → (7/7, 3/9; 10 total; 28 drawn)
      pour 7 into 9 → (1/7, 9/9; 10 total; 28 drawn)
      discard 9 → (1/7, 0/9; 1 total; 28 drawn)

      And we can make 4 pints by drawing 18 pints (and discarding 14 pints) as follows (7 steps):

      fill 9 → (0/7, 9/9; 9 total; 9 drawn)
      pour 9 into 7 → (7/7, 2/9; 9 total; 9 drawn)
      discard 7 → (0/7, 2/9; 2 total; 9 drawn)
      pour 9 into 7 → (2/7, 0/9; 2 total; 9 drawn)
      fill 9 → (2/7, 9/9; 11 total; 18 drawn)
      pour 9 into 7 → (7/7, 4/9; 11 total; 18 drawn)
      discard 7 → (0/7, 4/9; 4 total; 18 drawn)

      There are a couple of shortcuts we could use:

      If the capacities have a GCD > 1, then it will be impossible to make an amount that is not a multiple of that GCD, so we could just consider co-prime capacities.

      If one of the capacities is 1 (and the other is 15), and we we can make all amounts from 1 – 16 without any wastage, so we could skip checking this arrangement.

      Like

    • Frits's avatar

      Frits 5:05 am on 1 November 2025 Permalink | Reply

      I did this recursion in a different way than my other recursion code. Normally I would do all checks before the next recursion call. Now the first thing I do in the recursion is to check the new values v1, v2 and w.

      The code to check other targets has been set up so that solve() doesn’t have to be called anymore.

      from math import gcd
      
      # try go get to target by filling, discarding or transfering water
      # parameters: volumes, target and water drawn
      def solve(v1, v2, tgt, w=0, ss=set()):
        global mw, c1, c2
        # skip if we have already drawn more water than another solution
        if w >= mw or (v1, v2) in ss:
          return
        t = v1 + v2 
        ss_ = ss | {(v1, v2)}
        # end up with the target of whole number of pints in the two jugs combined
        if t == tgt:
          mw = min(w, mw)
          yield w
        else:  
          # fill 1st jug from tap, don't allow (c1, 0) at a late stage
          if v1 < c1 and (not w or v2): 
            yield from solve(c1, v2, tgt, w + c1 - v1, ss_)
          # fill 2nd jug from tap, don't allow (0, c2) at a late stage
          if v2 < c2  and (not w or v1): 
            yield from solve(v1, c2, tgt, w + c2 - v2, ss_)
          
          # empty 1st jug 
          if v1 and v2 < c2: yield from solve(0, v2, tgt, w, ss_)
          # empty 2nd jug 
          if v2 and v1 < c1: yield from solve(v1, 0, tgt, w, ss_)
          
          # fill 1st jug as much as possible from 2nd jug
          if v1 < c1 and v2:
            m = min(v2, c1 - v1) 
            yield from solve(v1 + m, v2 - m, tgt, w, ss_)
          # fill 2nd jug as much as possible from 1st jug
          if v2 < c2 and v1:
            m = min(v1, c2 - v2) 
            yield from solve(v1 - m, v2 + m, tgt, w, ss_)  
      
      M = 10**9      # maximum of number of pints of water available
      # assume capacity of jug 1 is not more than capacity of jug 2
      for c1 in range(1, 9):  
        c2 = 16 - c1
        # all possible solutions are a mutiple of the gcd
        if gcd(c1, c2) > 1: continue
       
        m, mw = M, M     # minimum water drawn   
        # check for one pint 
        for m in solve(0, 0, 1, 0, set()):
          pass
        if (m1 := m) == M: continue
      
        # check for four pints
        m, mw = M, M     # minimum water drawn
        for m in solve(0, 0, 4, 0, set()):
          pass
        if m >= m1 or m == M: continue
        
        # check other targets within 1-16
        doable = {1, 4, c1, c2, 16,
                  c1 + 1,      # 1 in J2, fill J1 
                  c1 + 4,      # 4 in J2, fill J1 
                  c2 - c1,     # fill J2, transfer to J1, empty J1
                  c2 - c1 + 1, # 1 in J1, fill J2, transfer to J1, empty J1
                  c2 - c1 + 4, # 4 in J1, fill J2, transfer to J1, empty J1
                  2 * c1 - c2, # c1 in both jugs, transfer to J2, empty J2
                  2 * c1}
        
        # check feasability of other targets 
        for t in [i for i in range(1, 16 - c1) if i not in doable]:
          mw = M     # minimum water drawn    
          if any(solve(0, 0, t, 0, set())): continue
          break
        else: # no break  
          print(f"answer: {c1} and {c2} pints")
      

      Like

  • Unknown's avatar

    Jim Randell 6:44 am on 26 October 2025 Permalink | Reply
    Tags:   

    Teaser 3292: Doctor Hoo’s TARDSI 

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

    My blood pressure was 160 systolic over 100 diastolic. I knew 120 over 80 is “normal”, so Doctor Hoo was concerned. She loaned me a TARDSI (Test And Record Diastolic Systolic Indicator) for a few days. I logged 12 blood pressure readings, comprising 24 different values (12 systolic between 120 and 160, and 12 diastolic between 80 and 100). I noticed some curious things about these values. Each systolic:diastolic pair had no repeated digits nor common prime factors. The systolic and diastolic sets each had exactly six odd values, but the lowest and highest values in each set were the only primes. No value was a digit rearrangement of any other and the systolic set had no consecutive values.

    Give the systolic:diastolic pair you can be sure I measured (as SSS:DD e.g. 123:74).

    [teaser3292]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 26 October 2025 Permalink | Reply

      I recently added some functionality to the [[ choose() ]] function in the enigma.py library. And we can use that functionality in solving this puzzle.

      There are a fairly limited number of possible sets of values for both the systolic and diastolic readings.

      This Python program finds possible values for each and tries to match up each of the pairs of sets of individual readings to give candidate sets of pairs of readings, and then looks for values that are common to all possible candidate sets.

      It runs in 271ms. (Internal runtime is 196ms).

      from enigma import (
        Accumulator, primes, irange, seq_all_different, flatten, choose, nsplit, cproduct,
        disjoint_cproduct, union, ordered, call, gcd, cache, join, sprintf as f, printf
      )
      
      primes.expand(160)  # primes up to 160
      
      # digit content of numbers
      digits = cache(lambda n: call(ordered, nsplit(n)))
      
      # are there duplicated digits in <ns>?
      def is_duplicate(*ns):
        return not seq_all_different(flatten((digits(n) for n in ns), fn=iter))
      
      # find values with no repeated digits
      def values(a, b, step=1):
        ns = list()
        for n in irange(a, b, step=step):
          if is_duplicate(n): continue
          ns.append(n)
        # lowest and highest values must be prime
        while ns and ns[0] not in primes: ns.pop(0)
        while ns and ns[-1] not in primes: ns.pop(-1)
        return ns
      
      # possible systolic and diastolic values
      sys = values(120, 160)
      dia = values(80, 100)
      
      # check sets of values <vs>
      def check(*vs):
        k = len(vs)
        # only the lowest and highest values are primes
        if (k == 1 or k == 12) != (vs[-1] in primes): return
        # there are [exactly] 6 odd numbers
        n = sum(v % 2 for v in vs)
        if (k == 12 and n != 6) or n > 6: return
        # no value is an anagram of any other
        if digits(vs[-1]) in (digits(vs[i]) for i in irange(0, k - 2)): return
        # looks OK
        return 1
      
      # find possible systolic 12-sets (no consecutive values)
      sss = list(choose(sys, check, 12, increasing=1, gap=2, multi_fns=0))
      printf("[{n} systolic sets]", n=len(sss))
      
      # find possible diastolic 12-sets
      dss = list(choose(dia, check, 12, increasing=1, gap=1, multi_fns=0))
      printf("[{n} diastolic sets]", n=len(dss))
      
      # pair up <ss> values with <ds> values (in some order)
      def pairs(ss, ds):
        # for each value in <ss> find acceptable values in <ds>
        vss = list()
        for s in ss:
          # find values with no repeated digits; no common divisors
          vss.append(set(d for d in ds if not (is_duplicate(s, d) or gcd(s, d) > 1)))
      
        # check all <ds> values are possible
        if not union(vss).issuperset(ds): return
      
        # find viable sequences
        for vs in disjoint_cproduct(vss):
          # return (s, d) pairs
          yield list(zip(ss, vs))
      
      # format a collection of readings
      fmt = lambda vs: join((f("{s}:{d}") for (s, d) in vs), sep=", ", enc="()")
      
      # find common values in solutions
      r = Accumulator(fn=set.intersection)
      
      # match a systolic set with a diastolic set
      for (ss, ds) in cproduct([sss, dss]):
        for ps in pairs(ss, ds):
          printf("pairs = {ps}", ps=fmt(ps))
          r.accumulate(set(ps))
      
      # output solution
      printf("[{n} sequences found]", n=r.count)
      if r.value:
        printf("SOLUTION = {vs}", vs=fmt(r.value))
      

      Solution: The value that must have been recorded was 132:85.

      There 171 possible sets of readings that satisfy the conditions. These are derived from the following sets:

      systolic = (127, 130, 132, 135, 138, 140, 143, 145, 147, 150, 152, 157)
      systolic = (127, 130, 132, 136, 138, 140, 143, 145, 147, 150, 153, 157)
      diastolic = (83, 84, 85, 86, 87, 90, 92, 93, 94, 95, 96, 97)

      Odd numbers are underlined (6 in each set). Primes are in bold (smallest and largest of each set). The systolic values have no consecutive pairs.

      An example combination of readings is as follows (ordered by systolic value):

      (127:84, 130:87, 132:85, 135:86, 138:95, 140:83, 143:90, 145:96, 147:92, 150:97, 152:93, 157:94)

      Each pair has no shared digits, and also no common divisor (> 1).

      The value 132:85 appears in all possible reading sets. The next most common reading is 138:95 (which appears in 152 of the 171 candidate sets).

      Like

      • Jim Randell's avatar

        Jim Randell 3:50 pm on 28 October 2025 Permalink | Reply

        It occurred to me that the systolic and diastolic values form the independent sets of a bipartite graph [ @wikipedia ], where two values are connected iff they have no duplicated digits and are co-prime.

        The candidate sets of readings are then subsets of this graph with 12 systolic and 12 diastolic values, that have a perfect matching between them.

        I added code to graph.py [ @github ] to find perfect matchings of a bipartite graph:

        # bipartite graphs
        
        # subgraph of (x, y) edges that connect X and Y
        def bipartite_subgraph(edges, X, Y):
          (X, Y) = (set(X), set(Y))
          return set((x, y) for (x, y) in edges if x in X and y in Y)
        
        # matchings
        
        # (k, vs) -> len(vs)
        _key_fn = (lambda k_v: len(k_v[1]))
        
        # remove s -> t from adj
        _adj_remove = lambda adj, s, t: dict((k, vs.difference({t})) for (k, vs) in adj.items() if k != s)
        
        def _matching(adj_xy, adj_yx, d):
          # are we done?
          if adj_xy and adj_yx:
            # find minimal x->y and y->x sets
            ((x, ys), (y, xs)) = (min(adj.items(), key=_key_fn) for adj in (adj_xy, adj_yx))
            if not (xs and ys): return
            # process the smallest choice
            if len(xs) < len(ys):
              ys = {y}
            else:
              xs = {x}
            for (x, y) in cproduct([xs, ys]):
              adj_xy_ = _adj_remove(adj_xy, x, y)
              adj_yx_ = _adj_remove(adj_yx, y, x)
              yield from _matching(adj_xy_, adj_yx_, update(d, [(x, y)]))
          elif not (adj_xy or adj_yx):
            yield d
        
        # find (perfect) matchings in the bipartite graph specified by (x, y) edges
        def find_bipartite_matching(edges, X=None, Y=None):
          # construct x -> y, y -> x adjacency matrices
          adj_xy = (dict((k, set()) for k in X) if X else defaultdict(set))
          adj_yx = (dict((k, set()) for k in Y) if Y else defaultdict(set))
          for (x, y) in edges:
            adj_xy[x].add(y)
            adj_yx[y].add(x)
          fail(not is_disjoint([adj_xy.keys(), adj_yx.keys()]), "matching: graph is not bipartite")
          return _matching(adj_xy, adj_yx, dict())
        

        And then I updated my solution for this puzzle to use graph.py to find candidate pairs of values (and also to use a faster method of finding candidate systolic and diastolic sets).

        The following code runs in 124ms. (Internal runtime is 56ms).

        from enigma import (
          Accumulator, primes, irange, seq_all_different, flatten, subsets, choose,
          nsplit, union, cproduct, ordered, call, gcd, cache, join, sprintf as f, printf
        )
        import graph
        
        primes.expand(160)  # primes up to 160
        
        # digit content of numbers
        digits = cache(lambda n: call(ordered, nsplit(n)))
        
        # are there duplicated digits in <ns>?
        def is_duplicate(*ns):
          return not seq_all_different(flatten((digits(n) for n in ns), fn=iter))
        
        # find values with no repeated digits
        def values(a, b, step=1):
          ns = list()
          for n in irange(a, b, step=step):
            if is_duplicate(n): continue
            ns.append(n)
          # lowest and highest values must be prime
          while ns and ns[0] not in primes: ns.pop(0)
          while ns and ns[-1] not in primes: ns.pop(-1)
          return ns
        
        # possible systolic and diastolic values
        sys = values(120, 160)
        dia = values(80, 100)
        
        # check sets of values (4 odd; 6 even)
        def check(*ns):
          k = len(ns)
          # there are exactly 4 odd and 6 even numbers
          odd = sum(n % 2 for n in ns)
          if (k == 10 and odd != 4) or odd > 4 or k - odd > 6: return
          # looks OK
          return 1
        
        # find 12-sets of values
        def find_sets(vs, gap=1):
          # find primes in vs
          prs = list(v for v in vs if v in primes)
          # choose two primes
          for (p, q) in subsets(prs, size=2):
            # that we can fit another 10 numbers between
            if q - p < 11 * gap: continue
            # remaining numbers to choose from
            rs = list(v for v in vs if p + gap <= v <= q - gap and v not in prs)
        
            # choose 10 more numbers (4 odd (non-primes), 6 even)
            for ns in choose(rs, check, 10, increasing=1, gap=gap, multi_fns=0):
              # construct the complete list
              ns.insert(0, p)
              ns.insert(-1, q)
              # no value is an anagram of any other
              if not seq_all_different(digits(n) for n in ns): continue
              yield ns
        
        # find possible systolic 12-sets (no consecutive values)
        sss = list(find_sets(sys, gap=2))
        printf("[{n} systolic sets]", n=len(sss))
        
        # find possible diastolic 12-sets
        dss = list(find_sets(dia))
        printf("[{n} diastolic sets]", n=len(dss))
        
        # find viable value pairs (no repeated digits; no common factors)
        viable = lambda s, d: not (is_duplicate(s, d) or gcd(s, d) > 1)
        edges = set((s, d) for (s, d) in cproduct(union(xs) for xs in (sss, dss)) if viable(s, d))
        
        # pair up <ss> values with <ds> values (in some order)
        def pairs(ss, ds):
          # find perfect matchings in the bipartite graph
          subg = graph.bipartite_subgraph(edges, ss, ds)
          for m in graph.find_bipartite_matching(subg, ss, ds):
            # return (s, d) pairs
            yield list((s, m.get(s)) for s in ss)
        
        # format a collection of readings
        fmt = lambda vs: join((f("{s}:{d}") for (s, d) in vs), sep=", ", enc="()")
        
        # find common values in solutions
        r = Accumulator(fn=set.intersection)
        
        # match a systolic set with a diastolic set
        for (ss, ds) in cproduct([sss, dss]):
          for ps in pairs(ss, ds):
            printf("pairs = {ps}", ps=fmt(ps))
            r.accumulate(set(ps))
        
        # output solution
        printf("[{n} sequences found]", n=r.count)
        if r.value:
          printf("SOLUTION = {vs}", vs=fmt(r.value))
        

        The parent bipartite graph linking readings is as shown:

        There are a lot of potential links, but we can simplify the graph as follows:

        We see 91 cannot be linked with any systolic value (as it contains a 1 digit, and all systolic values start with 1). So any candidate set of diastolic values containing 91 can be removed, as it cannot participate in any matching. And this leaves us with a single candidate set of 12 diastolic readings.

        We also see in any two sets of 12 readings that consist of 6 even and 6 odd values, the even values of one set must be linked with the odd values of the other set. So we can also remove any odd-odd links in the parent graph.

        And we can then remove any further candidate sets that include a disconnected vertex (as we did with 91). This brings us down to the two candidate sets of 12 systolic values given above.

        This leaves us with the following simplified graph:

        We see there are only 12 candidate diastolic values, so each of these must occur in any solution set. And the only systolic value that 85 can be linked with is 132 (shown in red), so this pair must appear in any viable set of readings. (Although this does not show that there are viable sets of readings).

        Like

    • Frits's avatar

      Frits 2:27 am on 27 October 2025 Permalink | Reply

      from itertools import combinations as comb, product
      from math import gcd
      from collections import defaultdict
      
      # set of prime numbers between 80 and 160
      P = [3, 5, 7, 11]
      P = [x for x in range(81, 161, 2) if all(x % p for p in P)]
      
      sysF, sysT = (120, 160)
      diaF, diaT = (80, 99) # not including 100 (no repeated digits)
      # prime numbers
      sysP = [p for p in P if sysF <= p <= sysT] 
      diaP = [p for p in P if diaF <= p <= diaT] 
      
      # possible values for sets
      sys = [n for n in range(sysF, sysT + 1) if len(set(str(n))) == len(str(n))]
      dia = [n for n in range(diaF, diaT + 1) if "1" not in (s := str(n)) and 
                                                    len(set(s)) == len(s)]
      
      # combine each systolic value with a diastolic value
      def solve(s, d, ss=[]):
        if not s:
          yield ss
        else:
          for y in d:
            # had no repeated digits nor common prime factors
            if set(str(s[0])).isdisjoint(str(y)) and gcd(s[0], y) < 2:
              yield from solve(s[1:], d.difference([y]), ss + [(s[0], y)])
              
      # generate systolic or a diastolic sets
      def gen_sets(s, p, check=0):
        # choose the 2 primes
        for p1, p2 in comb(p, 2):
          if p2 - p1 < 10: continue   # allow for 4 odd numbers in between
          evens = [x for x in s if x % 2 == 0 and p1 + check < x < p2 + check] 
          # choose remaining odd numbers
          for odds in comb([x for x in s if x not in p and x % 2 and p1 < x < p2], 4):
            o = set(odds) | {p1, p2}
            # a specific set had no consecutive values
            if check:
              evens_ = [x for x in evens if all(x + i not in o for i in [-1, 1])]
            # no value was a digit rearrangement of any other
            if len({tuple(sorted(str(x))) for x in o}) != 6: continue   
            o = tuple(o)  
            # choose 6 even numbers  
            for e in comb(evens if not check else evens_, 6):
              vs12 = e + o
              # no value was a digit rearrangement of any other
              if len({tuple(sorted(str(x))) for x in vs12}) != 12: continue 
              yield vs12              
      
      # generate all sets
      systo = list(gen_sets(sys, sysP, 1))
      diasto = list(gen_sets(dia, diaP))
      
      cnt = 0
      dct = defaultdict(int)
      for s, d in product(systo, diasto):
        # combine each systolic value with a diastolic value
        for ps in solve(s, set(d)):
          cnt += 1
          for tup in ps:
            dct[tup] += 1
      
      print("answer:", ' or '.join([str(x) + ":" + str(y) 
                                   for (x, y), v in dct.items() if v == cnt]))
      

      Like

      • Frits's avatar

        Frits 2:54 am on 27 October 2025 Permalink | Reply

        @Jim, please change line 47 to “vs = e + o”.

        I wondered why I had used sorted() in line 47. Interestingly it turns out that putting the even numbers up front is efficient (only 8 ms and 1718 recursions). Putting the odd numbers up front is a lot less efficient (697 ms and 450557 recursions).

        Like

  • Unknown's avatar

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

    Teaser 2495: [Alphametic] 

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

    I have taken some numbers and consistently replaced the digits by letters, with different letters for different digits, in this way:

    FOUR is a perfect square;
    EIGHT is a perfect cube;
    ONE + TEN is a prime.

    What is the value of THREE?

    This puzzle was originally published with no title.

    [teaser2495]

     
    • Jim Randell's avatar

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

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library, that is a direct interpretation of the puzzle statement.

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "is_square(FOUR)"
      "is_cube(EIGHT)"
      "is_prime(ONE + TEN)"
      
      --answer="THREE"
      

      Solution: THREE = 42911.

      We have:

      FOUR = 7569 = sq(87)
      EIGHT = 13824 = cb(24)
      ONE + TEN = 501 + 410 = 911 (prime)

      Like

      • Ruud's avatar

        Ruud 7:48 am on 25 October 2025 Permalink | Reply

        import peek
        import istr
        
        for x2 in istr.range(32, 100):
            for x3 in istr.range(22, 47):
                for n in istr.range(10):
                    f, o, u, r = [*x2**2]
                    e, i, g, h, t = [*x3**3]
                    if ((o | n | e) + (t | e | n)).is_prime() and ((f | o | u | r | e | i | g | h | t | n).all_distinct()):
                        peek(t | h | r | e | e)
        

        Like

    • GeoffR's avatar

      GeoffR 8:58 pm on 25 October 2025 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 0..9: U; var 0..9: I; var 0..9: R;
      var 0..9: G; var 0..9: H; var 0..9: N;
      var 1..9: O; var 1..9: F; var 1..9: E; var 1..9: T;
      
      constraint all_different([U,I,R,G,H,N,O,F,E,T]);
      var 100..999:ONE == 100*O +10*N + E;
      var 100..999:TEN == 100*T +10*E + N;
      var 1000..9999: FOUR == 1000*F + 100*O + 10*U + R;
      var 10000..99999: EIGHT == 10000*E + 1000*I + 100*G + 10*H + T;
      var 10000..99999: THREE == 10000*T + 1000*H + 100*R + 11*E;
      
      set of int: sq4 = {n*n | n in 32..99}; 
      set of int: cb5 = {n*n*n | n in 22..46}; 
      
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) 
      ((i < x) -> (x mod i > 0));
      
      constraint FOUR in sq4 /\ EIGHT in cb5 /\ is_prime(ONE + TEN);
      
      solve satisfy;
      
      output ["THREE = " ++ show(THREE)
      ++ "\n" ++ "FOUR = " ++ show(FOUR)
      ++ "\n" ++ "EIGHT = " ++ show(EIGHT)
      ++ "\n" ++ "ONE = " ++ show(ONE) 
      ++ "\n" ++ "TEN = " ++ show(TEN)];
      
      % THREE = 42911
      % FOUR = 7569
      % EIGHT = 13824
      % ONE = 501
      % TEN = 410
      % ----------
      % ==========
      
      
      

      Like

    • ruudvanderham's avatar

      ruudvanderham 3:04 pm on 26 October 2025 Permalink | Reply

      With the latest version of istr, we can decompose and compose to/from one letter variables, resulting in cleaner code:

      import istr
      import peek
      
      for x2 in istr.range(32, 100):
          for x3 in istr.range(22, 47):
              for n in istr.range(10):
                  (x2**2).decompose("four")
                  (x3**3).decompose("eight")
                  if (istr.compose("one") + istr.compose("ten")).is_prime() and istr.compose("foureightn").all_distinct():
                      peek(istr.compose("three"))
      

      Like

  • Unknown's avatar

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

    Teaser 2494: [Council election] 

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

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

    How many seats did each party win?

    This puzzle was originally published with no title.

    [teaser2494]

     
    • Jim Randell's avatar

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

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

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

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

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

      Before the election the seats were allocated thus:

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

      So the changes are:

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

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

      Like

    • Ruud's avatar

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

      As brute force as possible:

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

      Like

    • Frits's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 3291: Top of the Pops 

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

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

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

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

    [teaser3291]

     
    • Jim Randell's avatar

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

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

      p1 > p2 > p3

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

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

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

      We have:

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

      Like

    • Frits's avatar

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

      Two solutions.

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

      Like

      • Jim Randell's avatar

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

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

        Like

        • Frits's avatar

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

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

          Like

          • Jim Randell's avatar

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

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

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

            Like

    • Frits's avatar

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

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

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

      Like

      • Brian Gladman's avatar

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

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

        Like

        • Jim Randell's avatar

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

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

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

          Liked by 1 person

    • Brian Gladman's avatar

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

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

      Like

  • Unknown's avatar

    Jim Randell 8:10 am on 17 October 2025 Permalink | Reply
    Tags: ,   

    Teaser 2493: [Driving competition] 

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

    Pat won the driving competition. The course was a whole number of miles long and he completed the course in a whole number of minutes (less than two hours). His speed was a two-digit number of miles per hour. The time taken to complete the course was 17 minutes less than the time he would have taken to drive 17 miles less than the distance he would have gone if he had driven for 17 minutes less than the time he would have taken to complete the course at two-thirds of his speed.

    How long was the course?

    This puzzle was originally published with no title.

    [teaser2493]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 17 October 2025 Permalink | Reply

      It is straightforward to consider the possible velocities (2-digits) and course times (< 2 hours).

      This Python program runs in 85ms. (Internal runtime is 11.4ms).

      from enigma import (Rational, irange, cproduct, printf)
      
      Q = Rational()
      
      # speed was a 2-digit mph, time was a whole number of minutes < 120
      for (v, t) in cproduct([irange(10, 99), irange(1, 119)]):
      
        # the course was a whole number of miles long
        d = Q(v * t, 60)
        if d % 1 != 0: continue
      
        # t1 = time taken at 2/3 of the speed
        t1 = Q(3, 2) * t
      
        # d1 = distance travelled in time (t1 - 17)
        if not (t1 > 17): continue
        d1 = Q(v * (t1 - 17), 60)
      
        # t2 = time to drive (d1 - 17)
        if not (d1 > 17): continue
        t2 = Q(d1 - 17, v) * 60
      
        # time take to complete the course was (t2 - 17)
        if not (t2 - 17 == t): continue
      
        # output solution
        printf("v={v} t={t} d={d} [t1={t1} d1={d1} t2={t2}]")
      

      Solution: The course was 102 miles long.

      Pat’s speed was 60 mph (i.e. 1 mile per minute), so he took 102 minutes to complete the course.

      If he had driven at 2/3 of his actual speed (i.e. 40 mph), then it would have taken 153 minutes to complete the course.

      And in 153 − 17 = 136 minutes, driving at 60 mph, he would have been able to complete 136 miles.

      The time taken to drive 136 − 17 = 119 miles at 60 mph is 119 minutes.

      And 119 − 17 = 102 minutes is the time actually taken to complete the course.


      With more analysis we can derive the following equations for t and d:

      t = 68 + 2040/v
      d = 34 + 17v/15

      from which we see v must be a multiple of 15.

      This allows a manual solution by considering just 6 cases:

      v = 15 → t = 204
      v = 30 → t = 136
      v = 45 → t = 113.333…
      v = 60 → t = 102
      v = 75 → t = 95.2
      v = 90 → t = 90.666…

      And only one of these gives an integer in the required range (< 120).

      Hence:

      d = 34 + 17×60/15 = 102

      Or, programatically:

      from enigma import (irange, div, printf)
      
      # v is a 2-digit multiple of 15
      for v in irange(15, 99, step=15):
      
        # calculate time taken (< 120)
        t = div(2040, v)
        if t is None: continue
        t += 68
        if not (t < 120): continue
      
        # calculate course distance
        d = div(17 * v, 15) + 34
      
        # output solution
        printf("v={v} t={t} d={d}")
      

      Like

  • Unknown's avatar

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

    Teaser 2490: [Cans of paint] 

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

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

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

    How much green paint was ordered?

    This puzzle was originally published with no title.

    [teaser2490]

     
    • Jim Randell's avatar

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

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

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

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

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

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

      Which is mixed as follows:

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

      or:

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

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

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

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

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

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

      Like

    • Frits's avatar

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

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

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

      Like

  • Unknown's avatar

    Jim Randell 6:29 am on 12 October 2025 Permalink | Reply
    Tags:   

    Teaser 3290: Omnibus additions 

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

    In our town centre the scheduled times taken for stages between bus stops are 5, 8 or 9 minutes. The only possible stages (in either direction) are between the following stops:

    Market and Castle;
    Market and Park;
    Market and Hospital;
    Station and Castle;
    Station and University;
    Castle and Park;
    University and Park;
    Park and Hospital.

    There are the following routes (with total time given):

    Route 1: from Market to Castle; 5 stages; 29 minutes.
    Route 2: from University to Market; 19 minutes.
    Route 3: from University to Hospital; 4 stages; 31 minutes.
    Route 4: from University to Station; 27 minutes.
    Route 5: from Market to University; 4 stages; 24 minutes.

    No route visits a stop more than once.

    What are the stage times (in order) for Route 1, and Route 4?

    [teaser3290]

     
    • Jim Randell's avatar

      Jim Randell 7:49 am on 12 October 2025 Permalink | Reply

      The lengths of routes 2 and 4 are not given, but their times are, and there are only certain combinations of 5, 8, 9 minutes that allow a given time to be achieved.

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

      from enigma import (express, cproduct, update, unpack, printf)
      
      # adjacent stops
      adj = dict(C="MPS", H="MP", M="CHP", P="CHMU", S="CU", U="PS")
      
      # possible stage times
      stages = [5, 8, 9]
      
      # find a n-length path from src to tgt
      def path(n, src, tgt, vs=list()):
        # are we done
        if n == 1:
          if tgt in adj[src]:
            yield vs + [src + tgt]
        elif n > 1:
          # move from src (but not directly to tgt)
          for v in adj[src]:
            if v != tgt and not any(v in p for p in vs):
              yield from path(n - 1, v, tgt, vs + [src + v])
      
      key = unpack(lambda x, y: (x + y if x < y else y + x))
      
      # allocate times for each stage to sum to t
      def times(t, vs, d):
        # are we done?
        if not vs:
          if t == 0:
            yield d
        else:
          # next stage
          k = key(vs[0])
          x = d.get(k)
          # is the time already chosen?
          if x is not None:
            if not (x > t):
              yield from times(t - x, vs[1:], d)
          else:
            # choose a time for this stage
            for x in stages:
              if not (x > t):
                yield from times(t - x, vs[1:], update(d, [(k, x)]))
      
      # solve a set of routes
      def solve(ks, rs, ns, ts, d=dict(), ps=dict()):
        # are we done?
        if not ks:
          yield (d, ps)
        else:
          # choose a remaining route
          k = ks[0]
          # find a path
          ((src, tgt), n) = (rs[k], ns[k])
          for vs in path(n, src, tgt):
            # allocate times
            for d_ in times(ts[k], vs, d):
              yield from solve(ks[1:], rs, ns, ts, d_, update(ps, [(k, vs)]))
      
      # keys; routes; lengths; times
      ks = [1, 2, 3, 4, 5]
      rs = { 1: "MC", 2: "UM", 3: "UH", 4: "US", 5: "MU" }
      ns = { 1: 5, 3: 4, 5: 4 }  # lengths for routes 2 and 4 are not given
      ts = { 1: 29, 2: 19, 3: 31, 4: 27, 5: 24 }
      
      # possible missing lengths (routes 2 and 4)
      (n2s, n4s) = (set(sum(qs) for qs in express(ts[k], stages)) for k in [2, 4])
      
      # choose lengths for routes 2 and 4
      for (n2, n4) in cproduct([n2s, n4s]):
        ns_ = update(ns, [(2, n2), (4, n4)])
        for (d, ps) in solve(ks, rs, ns_, ts):
          # output routes
          for k in ks:
            printf("Route {k}: {n} stages; {t} min", n=ns_[k], t=ts[k])
            for (src, tgt) in ps[k]:
              printf("  {src} -> {tgt} [{x} min]", x=d[key((src, tgt))])
          printf()
      

      Solution: Stage times (in minutes) are: Route 1 = (5, 5, 9, 5, 5); Route 4 = (9, 5, 8, 5).

      The complete solution is:

      Route 1: 5 stages; 29 min
      M → H [5 min]
      H → P [5 min]
      P → U [9 min]
      U → S [5 min]
      S → C [5 min]

      Route 2: 3 stages; 19 min
      U → P [9 min]
      P → H [5 min]
      H → M [5 min]

      Route 3: 4 stages; 31 min
      U → P [9 min]
      P → C [9 min]
      C → M [8 min]
      M → H [5 min]

      Route 4: 4 stages; 27 min
      U → P [9 min]
      P → M [5 min]
      M → C [8 min]
      C → S [5 min]

      Route 5: 4 stages; 24 min
      M → P [5 min]
      P → C [9 min]
      C → S [5 min]
      S → U [5 min]

      Like

    • Frits's avatar

      Frits 1:45 pm on 12 October 2025 Permalink | Reply

      from math import ceil
      from itertools import product, permutations
      
      # sorted tuple
      stup = lambda tup: tup if tup[0] < tup[1] else tup[::-1]
      
      # make total <t> with <k> elements taken from <m> numbers in list <ns> 
      def decompose_(rest, m, t, k, ns, ss=[]):
        if m == 2:
          # example:
          # 5.A + 8.B + 9.C = t and A + B + C = k with t=29 and k=5
          # (8 - 5).B + (9 - 5).C = t - 5.k
          ssrev = ss[::-1]
          # calculate 2nd quantity
          q2, r = divmod(t - ns[0] * k - sum([(x - ns[0]) * y  
                         for x, y in zip(ns[2:], ssrev)]), ns[1] - ns[0]) 
          if not r and q2 >= 0:
            if (q1 := k - q2 - sum(ss)) >= 0:
              yield [a for g in [y * [x] for x, y in zip(ns, [q1, q2] + ssrev)] 
                     for a in g] 
        else:
          n = ns[m - 1]
          for amt in range(min(n, k - sum(ss), rest // n) + 1):
            yield from decompose_(rest - amt * n, m - 1, t, k, ns, ss + [amt])  
       
      # make total <t> with <k> numbers from list <ns>      
      def decompose(t, k, ns):
        yield from decompose_(t, len(ns), t, k, ns) 
         
      # get path from <fr> to <to> in <k> steps visiting different towns
      def path(fr, to, k, ss=[]):
        if k == 0:
          if ss[-1] == to:
            yield ss
        else:
          for town in d[fr]:
            if town not in ss: 
              yield from path(town, to, k - 1, ss + [town])
      
      # solve route <n>, not contradicting settings in dictionary <d>
      def solve(n, d=dict(), ss=[]):
        if n < 0: 
          yield ss[::-1]
        else:  
          (fr, to), dur = routes[n], duration[n]
          rng = ([n_stops[n]] if n_stops[n] else 
                  range(ceil(dur / times[-1]), dur // times[0] + 1)) 
          # make the route in <k> stages
          for k in rng:
            # combine possible times with possible paths
            for ts, ps in product(decompose(duration[n], k, times), 
                                  path(fr, to, k, [fr])):
              # permutations of times  
              for ts in set(permutations(ts)):
                # assign times <ts> to paths in <ps>
                d_ = dict(s := tuple(zip([stup(x) for x in zip(ps, ps[1:])], ts))) 
                # is path, time already in dictionary?
                if all((k_ not in d or d[k_] == v_) for k_, v_ in d_.items()): 
                  yield from solve(n - 1, d | d_, ss + [s])  
                
      times    = [5, 8, 9]
      stages   = "MC MP MH SC SU CP UP PH".split()
      towns    = {y for x in stages for y in x}
      routes   = "MC UM UH US MU".split()
      n_stops  = [5, 0, 4, 0, 4]
      duration = [29, 19, 31, 27, 24]
      
      # dictionary of visitable towns
      d = {t: [s[1 - s.index(t)] for s in stages if t in s] for t in towns}
      
      # solve all 5 routes
      for s in solve(len(routes) - 1):
        print("answer:", [y for x, y in s[0]], "and", [y for x, y in s[3]])
      

      Like

    • Frits's avatar

      Frits 4:25 am on 13 October 2025 Permalink | Reply

      We probably should assume that the scheduled time from A to B is the same as the scheduled time from B to A. This is not always true for towns in the mountains/hilly terrain.

      Like

      • Jim Randell's avatar

        Jim Randell 8:39 am on 13 October 2025 Permalink | Reply

        @Frits: Yes. I think we need to assume the times are the same in both directions to get a single solution.

        I think changing the [[ key ]] function in my code to just return [[ x + y ]] shows this.

        Like

    • Ruud's avatar

      Ruud 3:12 pm on 13 October 2025 Permalink | Reply

      Brute force, but still instantaneous:

      import peek
      import collections
      import itertools
      
      
      def route(stop0, stop1, nstops, called=None):
          if called is None:
              called = [stop0]
          if nstops in (0, -1):
              if called and called[-1] == stop1:
                  yield called
              else:
                  if nstops == 0:
                      return
          for stop in next_stop[stop0]:
              if stop not in called:
                  yield from route(stop, stop1, (-1 if nstops == -1 else nstops - 1), called + [stop])
      
      
      stages = "mc mp mh sc su cp up ph".split()
      
      next_stop = collections.defaultdict(list)
      for stage in stages:
          stop0, stop1 = tuple(stage)
          next_stop[stop0].append(stop1)
          next_stop[stop1].append(stop0)
      
      
      routes = {1: list(route("m", "c", 5)), 2: list(route("u", "m", -1)), 3: list(route("u", "h", 4)), 4: list(route("u", "s", -1)), 5: list(route("m", "u", 4))}
      route_durations = {1: 29, 2: 19, 3: 31, 4: 27, 5: 24}
      
      for x in itertools.product((5,8,9), repeat=len(stages)):
          stage_duration = dict(zip(stages, x))
          found_route = {}
          for i in range(1, 6):
              for route in routes[i]:
                  if sum(stage_duration.get(stop0 + stop1, 0) + stage_duration.get(stop1 + stop0, 0) for stop0, stop1 in zip(route, route[1:])) == route_durations[i]:
                      found_route[i] = route
      
          if len(found_route) == 5:
              peek(found_route)
      
      

      Like

      • Frits's avatar

        Frits 1:31 pm on 15 October 2025 Permalink | Reply

        Hi Ruud,

        You don’t answer the question but that is an easy fix.
        The program is even more instantaneous with the following line after line 38:

        if i not in found_route: break 
        

        Like

    • Frits's avatar

      Frits 5:22 am on 16 October 2025 Permalink | Reply

      Only permuting times for unknown stages.

      from math import ceil
      from itertools import product, permutations
      
      # sorted key of a stage
      key = lambda tup: tup[0] + tup[1] if tup[0] < tup[1] else tup[1] + tup[0]
      
      # t_ and k_ are being decremented, t and k remain the same
      def decompose_(t_, k_, t, k, ns, ns_0, ss=[]):
        if k_ == 2:
          # example:
          # 5.A + 8.B + 9.C = t and A + B + C = k with t=29 and k=5
          # (8 - 5).B = t - 5.k - (9 - 5).C
          # calculate 2nd quantity
          q2, r = divmod(t - ns[0] * k - sum([x * y  for x, y in zip(ns_0, ss)]), 
                         ns[1] - ns[0]) 
          if not r and q2 >= 0:
            # calculate 1st quantity
            if (q1 := k - q2 - sum(ss)) >= 0:
              yield tuple(a for g in [y * [x] for x, y in zip(ns, [q1, q2] + ss)] 
                          for a in g) 
        else:
          n = ns[k_ - 1]
          for x in range(min(n, k - sum(ss), t_ // n) + 1):
            yield from decompose_(t_ - x * n, k_ - 1, t, k, ns, ns_0, [x] + ss)  
       
      # make total <t> with <k> numbers from list <ns>      
      def decompose(t, k, ns):
        ns_0 = [x - ns[0] for x in ns[2:]]
        yield from decompose_(t, len(ns), t, k, ns, ns_0) 
             
      # get paths from <fr> to <to> in <k> steps visiting different stops
      def paths(fr, to, k, ss):
        if k == 1:
          if to in d[fr]:
            ss = ss[1:] + [to]
            yield tuple(key(x) for x in zip(ss, ss[1:]))
        else:
          for stop in d[fr]:
            if stop not in ss: 
              yield from paths(stop, to, k - 1, ss + [stop])
      
      # remove elements of 2nd list from 1st list
      def list_diff(li1, li2):
        for x in li2:
          if x in li1:
            li1.remove(x)
      
      # solve route <n>, not contradicting settings in dictionary <d>
      def solve(n, d=dict(), ss=[]):
        if n < 0: 
          yield ss[::-1]
        else:  
          (fr, to), dur = routes[n], duration[n]
          rng = ([n_stops[n]] if n_stops[n] else 
                  range(ceil(dur / times[-1]), dur // times[0] + 1)) 
          # make the route in <k> stages
          for k in rng:
            # combine possible times with possible paths
            for ts, pth in product(decompose(duration[n], k, times), 
                                  paths(fr, to, k, [to, fr])):
              # determine times not yet used for this path (new_ts is updated)                     
              list_diff(new_ts := list(ts), pth_ := [d[x] for x in pth if x in d]) 
              if new_ts == []:
                yield from solve(n - 1, d, ss + 
                                 [tuple((k, v) for k, v in d.items() if k in pth)])  
              else:         
                # number of new times must equal number of new stages
                if len(new_ts) != k - len(pth_): continue
                # permutations of new times  
                for new_ts in set(permutations(new_ts)):
                  d_, ts_ = dict(), []
                  for st in pth:
                    if st in d: 
                      ts_.append(d[st])
                    else:  
                      # assign new time to stage <st>
                      d_[st] = new_ts[0]
                      ts_.append(new_ts[0])
                      new_ts = new_ts[1:]
                  yield from solve(n - 1, d | d_, ss + [tuple(zip(pth, ts_))])
                
      times    = [5, 8, 9]
      stages   = "MC MP MH SC SU CP UP PH".split()
      stops    = {y for x in stages for y in x}
      routes   = "MC UM UH US MU".split()
      n_stops  = [5, 0, 4, 0, 4]
      duration = [29, 19, 31, 27, 24]
      
      # dictionary of visitable stops
      d = {stp: {s[1 - s.index(stp)] for s in stages if stp in s} for stp in stops}
      
      # solve all 5 routes
      for s in solve(len(routes) - 1):
        print("answer:", [y for x, y in s[0]], "and", [y for x, y in s[3]])    
      

      Like

  • Unknown's avatar

    Jim Randell 11:09 am on 10 October 2025 Permalink | Reply
    Tags:   

    Teaser 2514: [The Hardy Annular] 

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

    The Hardy Annular is an ornamental garden consisting of the area between two circles with the same centre, with the radius of each a whole number of metres. A line joining two points on the circumference of the bigger circle just touches the smaller circle and measures exactly 20 metres.

    What is the radius of each of the circles?

    This puzzle was originally published with no title.

    There are now 1250 Teaser puzzles available on the site (around 38% of all Teaser puzzles published).

    [teaser2514]

     
    • Jim Randell's avatar

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

      See also: Puzzle #64.

      We are looking for solutions to the equation:

      R² = r² + 10²

      Here is a solution using the pells.py library (a bit of overkill perhaps for a simple problem, but the code is already written, so here goes):

      from enigma import printf
      import pells
      
      # solve: R^2 - r^2 = 10^2
      for (R, r) in pells.diop_quad(1, -1, 100):
        if R > r > 0:
          printf("R={R} r={r}")
      

      Solution: The circles have radii of 24 m and 26 m.


      Manually, we solve:

      R² − r² = 100
      (R − r)(R + r) = 100

      for positive integer values of R, r.

      So we can consider divisor pairs of 100:

      100 = 1×100 ⇒ R = 50.5, r = 49.5
      100 = 2×50 ⇒ R = 26, r = 24 [SOLUTION]
      100 = 4×25 ⇒ R = 14.5, r = 10.5
      100 = 5×20 ⇒ R = 12.5, r = 7.5
      100 = 10×10 ⇒ R = 10; r = 0

      Only one of the pairs gives positive integer solutions.

      And this is the same process followed by the program.

      Like

    • Ruud's avatar

      Ruud 8:33 am on 11 October 2025 Permalink | Reply

      Strictly speaking the circles could also have a radius of 0 and 10.

      Like

    • Ruud's avatar

      Ruud 8:37 am on 11 October 2025 Permalink | Reply

      for r in range(50):
          for R in range(r, 50):
              if R * R == 100 + r * r:
                  print(r, R)
      

      Like

  • Unknown's avatar

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

    Teaser 2510: [Extension number] 

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

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

    “No problem”, said the operator.

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

    What is George’s new extension number?

    This puzzle was originally published with no title.

    [teaser2510]

     
    • Jim Randell's avatar

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

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

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

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

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

      Solution: The number is: 8147.

      The sum of the digits is: 20.

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

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

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

      Like

      • Jim Randell's avatar

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

        Or even faster with some analysis:

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

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

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

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

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

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

        Like

    • Ruud's avatar

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

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

      Like

    • Frits's avatar

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

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

      Like

  • Unknown's avatar

    Jim Randell 6:08 am on 5 October 2025 Permalink | Reply
    Tags:   

    Teaser 3289: Spot check 

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

    Jack has a set of 28 standard dominoes: each domino has a spot pattern containing 0 to 6 spots at either end and every combination ([0-0], [0-1] and so on up to [6-6]) is represented in the set. He discarded a non-blank domino and arranged the remaining dominoes into six “groups”, each of which contained a positive cube number of spots; a group might comprise a single domino. He then discarded another non-blank domino and rearranged the remaining dominoes into six groups each of which again contained a positive cube number of spots. He managed to do this the maximum possible number of times.

    How many dominoes did Jack discard? And how many spots in total were there on the dominoes that remained at the end?

    [teaser3289]

     
    • Jim Randell's avatar

      Jim Randell 9:26 am on 5 October 2025 Permalink | Reply

      I ignored the [0-0] domino as it doesn’t affect the results (it can’t be one of the discarded dominoes, and it can be added to any of the piles without affecting the total number of pips).

      I assumed the groups are just collections of 1 or more dominoes. (And not that they had to fit together as in a domino game). So we can just keep track of the number of pips on each domino.

      And that a “non-blank” domino is a domino that does not have either half blank.

      My code gets multiple answers for the second part of the puzzle.

      from enigma import (
        Accumulator, irange, inf, multiset, subsets,
        peek, powers, first, lt, group, printf
      )
      
      # collect pips on dominoes; all; non-0; total
      # (we discard 0-0 as it does not affect the results)
      (ds_all, ds_non0, T) = (multiset(), multiset(), 0)
      # the complete set of dominoes
      for (x, y) in subsets(irange(0, 6), size=2, select='R'):
        if x == y == 0: continue  # skip [0-0]
        t = x + y
        ds_all.add(t)
        if x > 0 and y > 0: ds_non0.add(t)
        T += t
      
      # possible cubes
      cubes = list(first(powers(1, inf, 3), count=lt(T)))
      
      # decompositions of numbers that can be expressed as the sum of 6 cubes
      g = group(subsets(cubes, size=6, select='R'), by=sum, st=(lambda ns: sum(ns) < T))
      
      # make piles with total in <ts> from dominoes <ds>
      def piles(ts, ds, ps=list()):
        # are we done?
        if not ts:
          yield ps
        else:
          t = ts[0]
          for ss in ds.express(t):
            yield from piles(ts[1:], ds.difference(ss), ps + [ss])
      
      # starting with total <t>
      # discard a domino from <ds_non0> and form the remaining dominoes in piles
      # return [(t1, ps1), ..., (tk, psk)]
      def solve(t, ds_all, ds_non0, ss=list()):
        if ss: yield ss
        # look for a non0 domino to discard
        for x in ds_non0.keys():
          # remaining total pips
          t_ = t - x
          if t_ not in g: continue
          ds_all_ = ds_all.difference([x])
          # consider possible decompositions into 6 piles
          for ts in g[t_]:
            # can we make an appropriate set of piles?
            ps = peek(piles(ts, ds_all_), default=None)
            if ps:
              yield from solve(t_, ds_all_, ds_non0.difference([x]), ss + [(t_, ps)])
              break  # we only need one viable decomposition
      
      # find maximal length sequences
      r = Accumulator(fn=max, collect=1)
      for ss in solve(T, ds_all, ds_non0):
        r.accumulate_data(len(ss), ss)
      printf("max discards = {r.value}")
      
      # output example maximal sets
      rs = set()
      for ss in r.data:
        for (t, ps) in ss:
          printf("{t} -> {ps}", ps=list(list(p.sorted()) for p in ps))
        printf()
        rs.add(t)
      printf("remaining dominoes = {rs} pips", rs=sorted(rs))
      

      Solution: [See below]

      Like

      • Jim Randell's avatar

        Jim Randell 2:45 pm on 5 October 2025 Permalink | Reply

        Taking a “non-blank” domino to mean any domino other than [0-0] (which I excluded anyway) allows me to simplify my code (although we could just change the [[ and ]] on line 14 to an [[ or ]]), and get a single answer to both of the questions in the puzzle. And so may well be what the setter intended.

        If this is the case, I think it would probably have been better to say the [0-0] domino was removed from a standard 28-domino set, and then the puzzle is solved with the remaining 27 dominoes. That way we don’t need mention the “non-blank” condition (we can just discard any of the remaining dominoes).

        from enigma import (
          Accumulator, irange, inf, multiset, subsets,
          peek, powers, first, lt, group, printf
        )
        
        # collect pips on dominoes; and total pips
        ds = multiset.from_seq(x + y for (x, y) in subsets(irange(0, 6), size=2, select='R'))
        ds.remove(0)  # remove [0-0] as it does not affect the results
        T = ds.sum()
        
        # possible cubes
        cubes = list(first(powers(1, inf, 3), count=lt(T)))
        
        # decompositions of numbers that can be expressed as the sum of 6 cubes
        g = group(subsets(cubes, size=6, select='R'), by=sum, st=(lambda ns: sum(ns) < T))
        
        # make piles with total in <ts> from dominoes <ds>
        def piles(ts, ds, ps=list()):
          # are we done?
          if not ts:
            yield ps
          else:
            t = ts[0]
            for ss in ds.express(t):
              yield from piles(ts[1:], ds.difference(ss), ps + [ss])
        
        # starting with total <t>
        # discard a domino from <ds> and form the remaining dominoes in piles
        # return [(t1, ps1), ..., (tk, psk)]
        def solve(t, ds, ss=list()):
          if ss: yield ss
          # look for a domino to discard
          for x in ds.keys():
            # remaining total pips
            t_ = t - x
            if t_ not in g: continue
            ds_ = ds.difference([x])
            # consider possible decompositions into 6 piles
            for ts in g[t_]:
              # can we make an appropriate set of piles?
              ps = peek(piles(ts, ds_), default=None)
              if ps:
                yield from solve(t_, ds_, ss + [(t_, ps)])
                break  # we only need one viable decomposition
        
        # find maximal length sequences
        r = Accumulator(fn=max, collect=1)
        for ss in solve(T, ds):
          r.accumulate_data(len(ss), ss)
        printf("max discards = {r.value}")
        
        # output example maximal sets
        rs = set()
        for ss in r.data:
          for (t, ps) in ss:
            printf("{t} -> {ps}", ps=list(list(p.sorted()) for p in ps))
          printf()
          rs.add(t)
        printf("remaining dominoes = {rs} pips", rs=sorted(rs))
        

        Solution: Jack discarded 11 dominoes. At the end, the total number of spots on the remaining dominoes was 98.

        There are 3 sequences of totals that can be made:

        [165, 162, 160, 158, 153, 143, 136, 124, 116, 105, 98 ]
        [165, 162, 160, 158, 153, 143, 135, 124, 117, 105, 98 ]
        [165, 162, 160, 158, 153, 143, 135, 123, 116, 105, 98 ]

        Like

        • Frits's avatar

          Frits 3:42 am on 10 October 2025 Permalink | Reply

          @Jim, couldn’t you use a break on line 44 after the yield?

          In my opinion as soon as you have found a valid “ps” you don’t need to process another “ts”. It may depend on what you want to display as output.

          Like

          • Jim Randell's avatar

            Jim Randell 8:54 am on 10 October 2025 Permalink | Reply

            @Frits: Yes, we could. There only needs to be one viable decomposition into cubes. (Although for most totals there is only one possible decomposition anyway). I’ll add it in.

            Like

  • Unknown's avatar

    Jim Randell 9:38 am on 3 October 2025 Permalink | Reply
    Tags:   

    Teaser 2515: [Interleaved powers] 

    From The Sunday Times, 5th December 2010 [link] [link]

    Using each digit just once I wrote down two five-figure numbers. One was a cube and the other was a square. Then I combined these two five-figure numbers to give me a ten-figure number by using one of them to occupy the odd positions (from left to right) and the other to occupy the even positions (again from left to right). Then the four-figure number made up in order from the digits in the prime positions turned out to be prime.

    What was the ten-figure number?

    This puzzle was originally published with no title.

    [teaser2515]

     
    • Jim Randell's avatar

      Jim Randell 9:39 am on 3 October 2025 Permalink | Reply

      If we suppose the square number is ABCDE, and the cube is FGHIJ then the combined number is one of:

      AFBGCHDIEJ
      FAGBHCIDJE

      And the corresponding prime positions (2nd, 3rd, 5th, 7th) form a prime.

      So the prime is:

      AFBGCHDIEJFBCD
      FAGBHCIDJEAGHI

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

      It runs in 114ms. (Internal runtime is 43ms).

      from enigma import (SubstitutedExpression, is_prime, printf)
      
      # use the SubstitutedExpression solver to find the square and the cube
      p = SubstitutedExpression(
        ["is_square(ABCDE)", "is_cube(FGHIJ)"],
        answer="(ABCDE, FGHIJ, AFBGCHDIEJ, FBCD, FAGBHCIDJE, AGHI)",
        # [optional] squares cannot end in 2, 3, 7, 8
        d2i={ 0: 'AF', 2: 'E', 3: 'E', 7: 'E', 8: 'E' }
      )
      
      # look for viable solutions
      for (sq, cb, n1, p1, n2, p2) in p.answers(verbose=0):
        (f1, f2) = (is_prime(p1), is_prime(p2))
        if f1 or f2:
          printf("square = {sq}, cube = {cb}")
          if f1: printf("-> n = {n1}, prime = {p1}")
          if f2: printf("-> n = {n2}, prime = {p2}")
          printf()
      

      Solution: The 10-digit number is: 5204137869.

      So the 4-digit prime is 2017, the odd digits form 50176 (= 224^2), and the even digits form 24389 (= 29^3).


      For a faster runtime we can collect the squares and cubes up front.

      The following Python program has an internal runtime of 479µs.

      from enigma import (
        defaultdict, irange, sq, cb, diff, cproduct,
        interleave, restrict, is_prime, join, printf
      )
      
      # index numbers (with distinct digits) by digit content
      def numbers(seq):
        d = defaultdict(list)
        for n in seq:
          s = str(n)
          # check digits are distinct
          if len(s) != len(set(s)): continue
          d[join(sorted(s))].append(s)
        return d
      
      # find candidate squares and cubes
      sqs = numbers(sq(x) for x in irange(100, 316))
      cbs = numbers(cb(x) for x in irange(22, 46))
      
      # consider possible cubes
      for (kc, vs) in cbs.items():
        # and look for corresponding squares
        ks = join(diff("0123456789", kc))
        for (square, cube) in cproduct([sqs[ks], vs]):
          # look for possible interleavings
          for (a, b) in [(square, cube), (cube, square)]:
            n = join(interleave(a, b))
            # extract the digits in (1-indexed) prime positions
            p = int(restrict(n, [1, 2, 4, 6]))
            if is_prime(p):
              printf("square = {square}, cube = {cube} -> n = {n}, p = {p}")
      

      Like

  • Unknown's avatar

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

    Teaser 2482: [Running up that hill] 

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

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

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

    This puzzle was originally published with no title.

    [teaser2482]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

      And the total time is 72 min.

      Like

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