Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:04 am on 12 August 2025 Permalink | Reply
    Tags:   

    Teaser 2330: Tetrahedra 

    From The Sunday Times, 20th May 2007 [link]

    Fabule’s latest jewellery creation consists of a set of equal-sized solid-gold tetrahedra, each of whose four faces is an equilateral triangle. Each face of each tetrahedron is divided into two triangles of equal area by a thin line of identical diamonds.

    No two tetrahedra are the same, but had Fabule made any more, it would have been necessary to repeat one of the designs.

    How many tetrahedra are there in the set?

    [teaser2330]

     
    • Jim Randell's avatar

      Jim Randell 8:05 am on 12 August 2025 Permalink | Reply

      See also Teaser 2835, which is a similar problem on a cube, rather than a regular tetrahedron.

      The only option for a single line to divide an equilateral triangle into two triangles of equal area is a line that runs from one vertex to the centre of the opposite side.

      This program follows the same strategy as my program for Teaser 2835 — we first define a class that deals with the rotations of the regular tetrahedron, and then use that to determine the number of different constructions.

      Here is the Tetra class:

      # labels for the 4 faces
      face_label = "DLRB"
      (D, L, R, B) = (0, 1, 2, 3)
      
      # the 12 rotational positions of a regular tetrahedron
      # we record the positions of the faces
      # and their orientations (in clockwise 1/3 turns)
      _rotations = (
        # the first 4 match with a rotation of the face D, L, R, B
        # faces         orientations
        # D  L  R  B    D  L  R  B      D B
        ((D, B, L, R), (1, 0, 0, 0)), # D R = rotate D
        ((R, L, B, D), (0, 1, 2, 1)), # R D = rotate L
        ((B, D, R, L), (1, 0, 1, 2)), # B L = rotate R
        ((L, R, D, B), (2, 2, 2, 1)), # L B = rotate B
        # the remaining rotations can be derived from those above
        ((D, L, R, B), (0, 0, 0, 0)), # D B = rotate []
        ((D, R, B, L), (2, 0, 0, 0)), # D L = rotate DD / LR / RB / BL
        ((R, D, L, B), (1, 1, 1, 2)), # R B = rotate BB / RL / LD / DR
        ((R, B, D, L), (2, 2, 1, 1)), # R L = rotate LDD / RBB
        ((B, R, L, D), (0, 1, 2, 0)), # B D = rotate LBB / RDD
        ((B, L, D, R), (2, 2, 0, 1)), # B R = rotate LL / RD / BR / DB
        ((L, D, B, R), (1, 2, 1, 2)), # L R = rotate BDD / DBB
        ((L, B, R, D), (0, 1, 2, 2)), # L D = rotate RR / LB / BD / DL
      )
      
      # map names to rotations D, L, R, B
      _names = dict(zip(face_label, (D, L, R, B)))
      
      # a class representing the rotations of the regular tetrahedron
      class Tetra(object):
      
        def __init__(self, faces=(D, L, R, B), orientations=(0, 0, 0, 0)):
          self.faces = tuple(faces)
          self.orientations = tuple(orientations)
      
        # a new tetra derived from the old one by applying the specified transformation
        def transform(self, faces, orientations):
          return Tetra(
            faces=tuple(self.faces[i] for i in faces),
            orientations=tuple((self.orientations[i] + r) % 3 for (i, r) in zip(faces, orientations))
          )
      
        # generate all rotations of the tetra
        def rotations(self):
          for (faces, orientations) in _rotations:
            yield self.transform(faces, orientations)
      
        # apply specific rotations
        def rotate(self, ts):
          tetra = self
          for t in ts:
            tetra = tetra.transform(*(_rotations[_names.get(t, t)]))
          return tetra
      

      And the following Python program runs in 67ms. (Internal runtime is 2.04ms).

      from enigma import (subsets, printf)
      from tetra import Tetra
      
      # canonical representation of the lines on the faces
      # - faces cannot be distinguished
      # - lines can be in one of three positions (0, 1, 2)
      def canonical(s):
        tetra = Tetra(orientations=s)
        return min(r.orientations for r in tetra.rotations())
      
      # consider possible orientations of the faces D, L, R, B
      patterns = set(canonical(s) for s in subsets((0, 1, 2), size=4, select='M'))
      
      # output solution
      printf("{n} patterns", n=len(patterns))
      

      Solution: There are 9 different tetrahedra in the set.

      Like

  • Unknown's avatar

    Jim Randell 6:57 am on 10 August 2025 Permalink | Reply
    Tags:   

    Teaser 3281: Square dates 

    From The Sunday Times, 10th August 2025 [link] [link]

    Each year when I get my new diary, I like to look at the dates to see if they have any interesting properties. When I looked at this year’s diary, I was pleased to find two dates that are “square” in the following way. If the date is expressed with one or two digits for the day (i.e., no leading zero is allowed), followed by two digits for the month and then the year in full, then 1.09.2025 is a square date, since 1092025 is the square of 1045.

    The only other square date this year is 27.09.2025, since 27092025 is the square of 5205.

    Using the same method of expressing the date, what is the first square date after 2025?

    [teaser3281]

     
    • Jim Randell's avatar

      Jim Randell 7:06 am on 10 August 2025 Permalink | Reply

      A straightforward search finds the answer in a reasonable time.

      The following Python program runs in 68ms. (Internal runtime is 2.8ms).

      from datetime import (date, timedelta)
      from enigma import (repeat, inc, is_square, printf)
      
      # consider increasing dates
      for d in repeat(inc(timedelta(days=1)), date(2025, 1, 1)):
        # form the date as a number
        n = d.year + d.month * 10000 + d.day * 1000000
        r = is_square(n)
        if r:
          printf("{d:%d.%m.%Y} -> {n} = sq({r})")
          if d.year > 2025: break
      

      Solution: The first square date after 2025 is: 01.01.2036 (1st January 2036).

      01.01.2036 → 1012036 = sq(1006)

      There are 15 dates in 20xx years that are square numbers:

      09.01.2004 → 9012004 = sq(3002)
      04.01.2009 → 4012009 = sq(2003)
      03.05.2009 → 3052009 = sq(1747)
      16.03.2016 → 16032016 = sq(4004)
      01.09.2025 → 1092025 = sq(1045)
      27.09.2025 → 27092025 = sq(5205)
      01.01.2036 → 1012036 = sq(1006)
      02.04.2041 → 2042041 = sq(1429)
      09.04.2049 → 9042049 = sq(3007)
      18.12.2049 → 18122049 = sq(4257)
      04.03.2064 → 4032064 = sq(2008)
      05.02.2081 → 5022081 = sq(2241)
      16.07.2081 → 16072081 = sq(4009)
      02.02.2084 → 2022084 = sq(1422)
      02.06.2096 → 2062096 = sq(1436)


      The program can be easily modified to answer a similar puzzle with triangular dates:

      There are only 5 dates in 20xx years that are triangular numbers:

      05.11.2003 → 5112003 = tri(3197)
      08.02.2015 → 8022015 = tri(4005)
      02.09.2035 → 2092035 = tri(2045)
      11.05.2051 → 11052051 = tri(4701)
      08.04.2055 → 8042055 = tri(4010)

      And the next triangular date after that is New Year’s Eve 2105:

      31.12.2105 → 31122105 = tri(7889)

      Like

      • Ruud's avatar

        Ruud 4:25 pm on 10 August 2025 Permalink | Reply

        Why not start at 1-1-2026? Then you don’t need to test for >2025 .

        Like

        • Jim Randell's avatar

          Jim Randell 4:38 pm on 10 August 2025 Permalink | Reply

          @Ruud: Because I also wanted to verify the dates for 2025 that are given in the puzzle text.

          Like

        • Tony Smith's avatar

          Tony Smith 5:11 am on 11 August 2025 Permalink | Reply

          In fact since no square ends 026, 027, …… 035 you could just start with the date which is the answer to the teaser.

          I assume that Jim’s program could easily be adapted to find all square dates in a given range.

          Like

          • Jim Randell's avatar

            Jim Randell 7:34 am on 11 August 2025 Permalink | Reply

            Although my original program was quick to write and fast to run, I did consider doing some rejection of years that couldn’t possibly form squares.

            This code rejects years based on their final 2 digits. So it only checks 2025, 2029, 2036, etc.

            from datetime import (date, timedelta)
            from enigma import (irange, repeat, inc, is_square, sq, printf)
            
            # reject years that cannot give a square
            mods = set(sq(n) % 100 for n in irange(0, 99))
            
            # generate solutions in interval [y1 .. y2]
            def generate(y1, y2):
              for year in irange(y1, y2):
                if year % 100 not in mods: continue
                # consider dates in the year
                for d in repeat(inc(timedelta(days=1)), date(year, 1, 1)):
                  if d.year > year: break
                  n = d.year + d.month * 10000 + d.day * 1000000
                  r = is_square(n)
                  if r:
                    yield (d, n, r)
            
            # consider dates from 2025 onwards
            for (d, n, r) in generate(2025, 2099):
              printf("{d:%d.%m.%Y} -> {n} = sq({r})")
              if d.year > 2025: break
            

            In the end though I went with my first program, as it was quick to write, fast to run, short, easily understandable, and can be easily modified to check for other scenarios (e.g. if we want to find dates that are triangular numbers).

            Like

      • Jim Randell's avatar

        Jim Randell 12:57 pm on 16 August 2025 Permalink | Reply

        I recently added code to enigma.py to implement the method of finding modular roots of an integer valued polynomial described at Teaser 3117. And we can use it to solve this puzzle by looking for modular square roots.

        This code can find square dates in any particular year, so we can start looking for the next year with square dates after 2025.

        The following code runs in less than 1ms (and is faster than using the simple implementation of [[ enigma.sqrtmod() ]]).

        from datetime import date
        from enigma import (enigma, Enumerator, irange, sq, mdivmod, catch, printf)
        
        sqrtmod = enigma.poly_roots_mod.sqrtmod
        
        # find square dates in year <y>
        def solve(y):
          for r in sqrtmod(y, 10, 4):
            n = sq(r)
            (dd, mm, yy) = mdivmod(n, 100, 10000)
            d = catch(date, yy, mm, dd)
            if d is not None:
              yield (d, n, r)
        
        # look for first year after 2025 with square dates
        for y in irange(2025, 2099):
          ss = Enumerator(solve(y))
          for (d, n, r) in ss:
            printf("{d:%d.%m.%Y} -> {n} = sq({r})")
          if ss.count > 0 and y > 2025: break
        

        Liked by 1 person

    • Frits's avatar

      Frits 9:56 am on 10 August 2025 Permalink | Reply

      Assuming that a solution exists before the year 10000.

      from datetime import date
      from enigma import catch
      
      def is_valid_date(n):
        s = str(n)
        if (mm := int(s[-6:-4])) > 12: return None
        if (dd := int(s[:-6])) > 31: return None
        yy = n % 10000
        return catch(date, yy, mm, dd)
      
      # dictionary of square endings
      d = {i: [j for j in range(100) if (j * j) % 100 == i] for i in range(100)}
      
      y = 2026 # start with first year after 2025
      while True:  
        sols = []
        # process numbers that ends on <y> if squared
        for n in d[y % 100]:
          # 31130000**.5 = 5579.426493825329
          for dmy in [sq for k in range(10, 56) 
                      if (sq := (100 * k + n)**2) % 10000 == y]:
            if (yyyymmdd := is_valid_date(dmy)) is not None:
              sols.append(yyyymmdd)
        
        if not sols and y < 10000:
          y += 1
        else:
          print("answer:", min(sols)) 
          break  
      

      Like

      • Frits's avatar

        Frits 3:53 pm on 11 August 2025 Permalink | Reply

        Fewer iterations.

        from datetime import date
        from enigma import catch
         
        def is_valid_date(n):
          ddmm, yy = divmod(n, 10000)
          dd, mm = divmod(ddmm, 100)
          if not (0 < mm < 13): return None
          if not (0 < dd < 32): return None
          return catch(date, yy, mm, dd)
         
        # numbers where last 2 digits of it's square are equal to <n>
        # if x is part of sq_end(n) then 50 - x and 100 - x must also be part of it
        sq_ends = lambda n: [j for j in range(26) if (j * j) % 100 == n]
         
        y = 2026 # start with first year after 2025
        while True:  
          sols = []
          # select numbers that if squared end on last 2 digits of <y> 
          for e in sq_ends(y % 100):
            for n in {e, 50 - e, e + 50, 100 - e if e else e}:
              # 31130000**.5 = 5579.426493825329
              for dmy in [sq for k in range(10, 56) 
                          if (sq := (100 * k + n)**2) % 10000 == y]:
                if (yyyymmdd := is_valid_date(dmy)) is not None:
                  sols.append(yyyymmdd)
        
          if not sols and y < 10000:
            y += 1
          else:
            if y < 10000:
              print("answer:", min(sols), "(YYYY-MM-DD)") 
            break 
        

        Like

    • Ruud's avatar

      Ruud 4:23 pm on 10 August 2025 Permalink | Reply

      import datetime
      
      try_date = datetime.datetime(2026, 1, 1)
      while not int((n := int(f"{try_date:%d%m%Y}")) ** 0.5) ** 2 == n:
          try_date += datetime.timedelta(days=1)
      print(try_date)
      

      Like

  • Unknown's avatar

    Jim Randell 10:16 am on 8 August 2025 Permalink | Reply
    Tags:   

    Teaser 2334: Tetrahedral cubes 

    From The Sunday Times, 17th June 2007 [link]

    I have constructed a tetrahedron (a solid with four triangular faces and six edges). On measuring those six edges, I find that their lengths, in centimetres, form six consecutive perfect cubes. Furthermore, if you managed to construct a tetrahedron with the same properties, the longest side of yours could not be shorter than any side of mine.

    How long is the longest side of my tetrahedron?

    [teaser2334]

     
    • Jim Randell's avatar

      Jim Randell 10:17 am on 8 August 2025 Permalink | Reply

      So we want to find the smallest set of consecutive cubes that allows us to construct a tetrahedron.

      This is a problem that we have encountered before, see Enigma 1152 (also set by Adrian Somerfield, 6 years before this puzzle).

      from enigma import (Enumerator, powers, inf, tuples, ipartitions, sq, is_triangle, printf)
      
      # compute the Cayley-Menger determinant (= 288 vol^2)
      def CMdet(x, y, z, X, Y, Z):
        (x2, X2, y2, Y2, z2, Z2) = map(sq, (x, X, y, Y, z, Z))
        # calculate: Matrix([[0, 1, 1, 1, 1], [1, 0, x2, y2, Z2], [1, x2, 0, z2, Y2], [1, y2, z2, 0, X2], [1, Z2, Y2, X2, 0]]).det()
        return (
          2 * x2 * X2 * (y2 + Y2 + z2 + Z2 - x2 - X2) +
          2 * y2 * Y2 * (x2 + X2 + z2 + Z2 - y2 - Y2) +
          2 * z2 * Z2 * (x2 + X2 + y2 + Y2 - z2 - Z2) -
          (x2 - X2) * (y2 - Y2) * (z2 - Z2) -
          (x2 + X2) * (y2 + Y2) * (z2 + Z2)
        )
      
      # check length form a tetrahedron: base = (x, y, z), opposite edges = (X, Y, Z)
      # return 288 V^2 if the tetrahedron is viable, else None
      def is_tetra(x, y, z, X, Y, Z):
        # check each face is a triangle
        if not all(is_triangle(*s) for s in [(x, y, z), (x, Y, Z), (X, y, Z), (X, Y, z)]): return
        # does it have a positive volume? d = 288 V^2
        d = CMdet(x, y, z, X, Y, Z)
        if d < 0: return
        return d
      
      # generate tetrahedra for sides in cs
      def tetras(cs):
        # group the sides into pairs
        for ((x, X), (y, Y), (z, Z)) in ipartitions(cs, 2):
          # do they form a tetrahedron?
          if is_tetra(x, y, z, X, Y, Z):
            yield ((x, X), (y, Y), (z, Z))
      
      # generate consecutive cubes
      for cs in tuples(powers(1, inf, 3), 6):
        ss = Enumerator(tetras(cs))
        for ((x, X), (y, Y), (z, Z)) in ss:
          # output solution
          printf("({x}, {X}) ({y}, {Y}) ({z}, {Z}) -> max = {m}", m=cs[-1])
        if ss.count > 0: break  # stop at smallest solution
      

      Solution: The longest side is 2197 cm.

      So it is quite a large tetrahedron.

      Like

  • Unknown's avatar

    Jim Randell 9:06 am on 5 August 2025 Permalink | Reply
    Tags:   

    Brainteaser 1153: Teasing square 

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

    The diagram shows part of a magic square (a 3×3 array of positive whole numbers each of whose rows, columns and long diagonals adds up to the same total). But in this case only five of the numbers are shown. And each digit has been replaced by a letter, different letters representing different digits.

    To add to the squareness of this magic I can tell you also that the sum of each row is a perfect square.

    What is the full (numerical) version of the magic square?

    [teaser1153]

     
    • Jim Randell's avatar

      Jim Randell 9:07 am on 5 August 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # [ @a | @b | @c ]
      # [ BR | AI |  N ]
      # [ TE |  A | @d ]
      
      # magic sum is a square number
      --macro="@sum = (3 * AI)"
      "is_square(@sum)"
      
      # missing values are positive integers
      --macro="@a = (@sum - BR - TE)"  # @a + BR + TE == @sum  [col 1]
      --macro="@b = (2 * AI - A)"      # @b + AI + A  == @sum  [col 2]
      --macro="@c = (2 * AI - TE)"     # @c + AI + TE == @sum  [diag 2]
      --macro="@d = (@sum - TE - A)"   # TE +  A + @d == @sum  [row 3]
      "@a > 0" "@b > 0" "@c > 0" "@d > 0"
      
      # remaining rows / cols / diags give the magic sum
      "@a + @b + @c == @sum"  # [row 1]
      "BR + AI + N == @sum"   # [row 2]
      "@c + N + @d == @sum"   # [col 3]
      "@a + AI + @d == @sum"  # [diag 1]
      
      --answer="((@a, @b, @c), (BR, AI, N), (TE, A, @d))"
      --output="lambda p, s, ans: print(ans)"
      

      Solution: The completed square is:

      And so the magic constant is 81.

      Which would correspond to: [X, XA, AB], [BR, AI, N], [TE, A, BY] for some unused symbols X and Y.

      Like

    • Frits's avatar

      Frits 2:07 pm on 5 August 2025 Permalink | Reply

      Minimising the number of iterations.

      # magic constant M = 3 * AI or AI = 3 * k^2
      for AI in [3 * k * k for k in range(2, 6)]:
        A, I = divmod(AI, 10)
        # A can never be equal to I as AI is not a multiple of 11
        M = 3 * AI
        sum2 = M - AI
        # BR + N = sum2 
        if sum2 > 105: continue
        # BR = A + 2.(c33 - AI) so R and A have the same parity
        # (sum2 - N) has same parity as A so N must have the same parity as A
        for N in set(range(A % 2, 10, 2)) - {A, I}: 
          B, R = divmod(sum2 - N, 10)
          if not (0 < B < 10) or not {A, I, N}.isdisjoint({B, R}): continue
          c33 = ((BR := 10 * B + R) - A + sum2) // 2
          T, E = divmod(M - c33 - A, 10)
          if not (0 < T < 10) or not {A, I, N, B, R}.isdisjoint({T, E}): continue
          c11 = sum2 - c33
          c12 = sum2 - A
          c13 = sum2 - (TE := 10 * T + E)
          
          mat = [(c11, c12, c13), (BR, AI, N), (TE, A, c33)]
          # double check matrix
          if not all(sum(r) == M for r in mat): continue
          if len({sum2, c11 + c33, TE + c13}) > 1: continue
          if not all(sum(r) == M for r in zip(*mat)): continue
          for c1, c2, c3 in mat:
            print(f"|{c1:>2} | {c2:>2} | {c3:>2} |")
      

      Like

    • GeoffR's avatar

      GeoffR 12:22 pm on 6 August 2025 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      %   w   x   y
      %   BR  AI  N
      %   TE  A   z
      
      predicate is_sq(var int: y) =
      exists(z in 1..ceil(sqrt(int2float(ub(y))))) (z*z = y );
      
      var 1..9:A; var 1..9:B; var 1..9:T; var 1..9:N; 
      var 0..9:R; var 0..9:I; var 0..9:E;
      var 10..99:BR; var 10..99:AI; var 10..99:TE;
      var 1..99:w; var 1..99:x; var 1..99:y; var 1..99:z;
      var 20..210:m_const;
      
      constraint all_different ([B, R, A, I, N, T, E]);
      constraint BR = 10*B + R /\ AI = 10*A + I /\ TE = 10*T + E;
      
      % All rows are squares
      constraint is_sq(BR + AI + N) /\ is_sq(TE + A + z) /\ is_sq(w + x + y);
      
      constraint m_const  == BR + AI + N;
      
      % Check conditions for a magic square
      constraint m_const == w + x + y /\ m_const == TE + A + z
      /\ m_const == w + BR + TE /\ m_const == x + AI + A /\ m_const == y + N + z
      /\ m_const == w + AI + z /\ m_const == y + AI + TE;
      
      solve satisfy;
      
      output[ show([w, x, y]) ++ "\n" ++ show([BR, AI, N]) ++ "\n" ++ show([TE, A, z]) ];
      
      % [5, 52, 24]
      % [46, 27, 8]
      % [30, 2, 49]
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 6:17 am on 3 August 2025 Permalink | Reply
    Tags:   

    Teaser 3280: No square rolls 

    From The Sunday Times, 3rd August 2025 [link] [link]

    Clark had a set of more than one identical six-sided dice, and each die had a different positive whole number on each face. He would roll the dice and work out the total of all the numbers showing on the top faces. Clark knew that the largest possible total was 69, but it took him much longer to realise that it was impossible to roll a total that was a perfect square. The latter was true no matter how many of the dice were rolled.

    In ascending order, what were the six numbers on each die?

    [teaser3280]

     
    • Jim Randell's avatar

      Jim Randell 6:18 am on 3 August 2025 Permalink | Reply

      If the largest possible total is 69, then there must be 3 dice and the largest number in the set is 23.

      The other numbers must be from 1 to 22, from which we can remove any number that is itself a square, or if a double or treble would give a square. Or where that number with 23 or double 23 would give a square.

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

      from enigma import (irange, sq, subsets, printf)
      
      # squares up to 69
      sqs = set(sq(i) for i in irange(1, 8))
      
      # there must be 3 dice, with a largest number of 23
      # choose the remaining 5 numbers
      ns = list(x for x in irange(1, 22) if sqs.isdisjoint({x, 2*x, 3*x, x + 23, x + 46, 2*x + 23}))
      for ds in subsets(ns, size=5, fn=list):
        ds.append(23)
        # check possible throws with 2 and 3 dice
        if any(sum(ss) in sqs for ss in subsets(ds, min_size=2, max_size=3, select='R')): continue
        # output solution
        printf("dice = 3x {ds}")
      

      Solution: The numbers on each die are: 7, 10, 14, 17, 20, 23.

      And so the possible throws are:

      7, 10, 14, 17, 20, 21, 23, 24, 27, 28, 30, 31, 33, 34, 35, 37, 38, 40,
      41, 42, 43, 44, 45, 46, 47, 48, 50, 51, 53, 54, 56, 57, 60, 63, 66, 69

      Avoiding the square numbers in the range [7 .. 69] = 9, 16, 25, 36, 49, 64 (as well as some non-square numbers in this range).

      Like

      • Jim Randell's avatar

        Jim Randell 11:03 am on 3 August 2025 Permalink | Reply

        Or, with more generic code.

        This Python program has an internal runtime of 3.4ms.

        from enigma import (irange, powers, inf, first, le, subsets, decompose, divisors_pairs, arg, printf)
        
        # consider up to k dice combinations of x with m
        combine = lambda x, m, k: (x + i * x + j * m for (i, j) in decompose(irange(0, k - 1), 2, increasing=0, sep=0, min_v=0))
        
        # solve for k dice, with n faces and a max value of m
        def solve(k, n, m):
          # squares up to k.m
          sqs = set(first(powers(1, inf, 2), count=le(k * m)))
          if any(i * m in sqs for i in irange(1, k)): return
        
          # the candidate numbers (other than m)
          ns = list(x for x in irange(1, m - 1) if not any(t in sqs for t in combine(x, m, k)))
        
          # choose numbers for the remaining faces
          for ds in subsets(ns, size=n - 1, fn=list):
            ds.append(m)
            # check up to k throws
            if not any(sum(ss) in sqs for ss in subsets(ds, min_size=2, max_size=k, select='R')):
              yield ds
        
        # solve the puzzle for a max throw of 69
        M = arg(69, 0, int)
        for (k, m) in divisors_pairs(M, every=1):
          if not (k > 1 and m > 8): continue
          for ds in solve(k, 6, m):
            printf("dice = {k}x {ds}")
        

        This program allows us to explore similar problems with other largest totals.

        For a largest total of 87 there are 3 sets of 3 dice. And for a largest total of 99 there are 35 sets of 3 dice.

        The first set of 4 dice appears at a largest total of 120:

        dice = 4× [11, 13, 15, 26, 28, 30]

        Like

    • Frits's avatar

      Frits 11:31 am on 3 August 2025 Permalink | Reply

      from itertools import combinations, product
      
      # possible square totals
      sqs = {i * i for i in range(1, 9)}
      
      # dice that can cause a square total
      invalid = {n for n in range(1, 23) for i in range(4) for j in range(4 - i)
                if i * n + j * 23 in sqs}
      
      rng = set(range(1, 23)) - invalid
      
      # dice pairs that can cause a square total
      invalid_pairs = {(x, y) for x, y in combinations(rng, 2)
                       if x + y in sqs or x + y + 23 in sqs}
      
      # choose 5 dice (besides 23)
      for d5 in combinations(rng, 5):
        # check possible dice pairs from these dice
        if any(d2 in invalid_pairs for d2 in combinations(d5, 2)): continue
        # check rolling of 3 dice (we have already checked combinations with 23)
        if any(sum(d3) in sqs for d3 in product(d5, repeat=3)): continue     
        print("answer:", d5 + (23, ))
      

      Like

      • Frits's avatar

        Frits 12:14 pm on 3 August 2025 Permalink | Reply

        A little bit more efficient.

        from itertools import combinations
        
        # possible square totals
        sqs = {i * i for i in range(1, 9)}
        
        # dice that can cause a square total
        invalid = {n for n in range(1, 23) for i in range(4) for j in range(4 - i)
                  if i * n + j * 23 in sqs}
        
        rng = set(range(1, 23)) - invalid
        
        # dice pairs that can cause a square total
        invalid_pairs = {(x, y) for x, y in combinations(rng, 2)
                         if x + y in sqs or 2 * x + y in sqs or 
                            x + 2 * y in sqs or x + y + 23 in sqs}
                         
        # choose 5 dice (besides 23)
        for d5 in combinations(rng, 5):
          # check possible dice pairs from these dice
          if any(d2 in invalid_pairs for d2 in combinations(d5, 2)): continue
          # check total of 3 different top faces
          if any(sum(d3) in sqs for d3 in combinations(d5, 3)): continue     
          print("answer:", d5 + (23, ))
        

        Like

      • Frits's avatar

        Frits 2:02 pm on 3 August 2025 Permalink | Reply

        In my first program itertools.combinations_with_replacement() is more suitable than product() (after reviewing Jim’s code).

        This version is again a bit more efficient (especially with CPython).

        from itertools import combinations
        
        # possible square totals
        sqs = {i * i for i in range(1, 9)}
        
        # dice that can cause a square total
        invalid = {n for n in range(1, 23) for i in range(4) for j in range(4 - i)
                  if i * n + j * 23 in sqs}
        
        rng = set(range(1, 23)) - invalid
        
        # dice pairs that can cause a square total
        invalid_pairs = {(x, y) for x, y in combinations(rng, 2)
                         if x + y in sqs or 2 * x + y in sqs or 
                            x + 2 * y in sqs or x + y + 23 in sqs}
        
        # add <k> top face numbers                    
        def solve(k, ns, ss=tuple()):
          if k == 0:
            yield ss
          else:
            for i, n in enumerate(ns):
              # check for possible square totals
              if ss and any((s, n) in invalid_pairs for s in ss): continue
              yield from solve(k - 1, ns[i + 1:], ss + (n, ))
        
        # choose 5 other top face numbers (besides 23)
        for s in solve(5, sorted(rng)):
          print("answer:", s + (23, ))
        

        Like

    • Frits's avatar

      Frits 3:44 pm on 4 August 2025 Permalink | Reply

      Another.

      from itertools import combinations
       
      # possible square totals
      sqs = {i * i for i in range(1, 9)}
      
      # all combinations with x and y
      c2 = lambda x, y: [2*x, 3*x, x + y, x + 2*y, 2*x + y]     
      c3 = lambda x, y, z: [x + y, x + 2*y, 2*x + y, x + y + z]
      
      rng = [x for x in range(1, 23) if x not in sqs and sqs.isdisjoint(c2(x, 23))]
      # dictionary of numbers that may be paired with a number (including itself)                      
      d = {x: {y for y in rng if sqs.isdisjoint(c3(x, y, 23))} for x in rng}
      
      # choose 5 other top face numbers (besides 23)
      for d5 in combinations(d.keys(), 5):
        # each number must pair with other selected numbers
        if any(not d[n].issuperset(d5) for n in d5): continue
        print("answer:", d5 + (23, ))
      

      Like

  • Unknown's avatar

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

    Teaser 2472: [Running from a train] 

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

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

    How far ahead of the train did he fall?

    This puzzle was originally published with no title.

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

    [teaser2472]

     
    • Jim Randell's avatar

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

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

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

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

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

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

      a = −2 [m/s^2]

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

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

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

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

      This happens at:

      v = 2

      t = 9

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

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

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

      So there is 1 m of platform left.

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

      The time taken for the boy to do this is:

      t = (99 − 2x) / 2

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

      t = x + 9

      If disaster is just averted, these times are equal:

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

      x = 81/4 = 20.25

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

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

      Hence:

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

      Here is a time/distance graph of the situation:

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

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

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

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

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

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


      However, the published answer was 440 m.

      This gives us:

      20x + 2x = 440

      x = 20

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

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

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

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

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

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

      Like

  • Unknown's avatar

    Jim Randell 8:10 am on 29 July 2025 Permalink | Reply
    Tags: ,   

    Teaser 2502: [Swimming lengths] 

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

    John and Pat were swimming at the pool. John started at the deep end at the same time as Pat started at the shallow end, each swimming lengths at their own steady speed. They passed each other on their first lengths and then passed each other again on their second lengths. It turned out that the length of the pool was a multiple of the distance between those first two passing points. John got out of the water after completing 48 lengths. At that moment Pat had also completed a whole number of lengths.

    How many?

    This puzzle was originally published with no title.

    [teaser2502]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 29 July 2025 Permalink | Reply

      (See also: Enigma 799).

      Suppose J takes 1 unit of time to swim a length, and P takes p units of time to swim a length.

      And after J has completed 48 lengths, P has completed n = 48/p lengths, which is a whole number.

      For them to pass on their first and then second lengths we have:

      p > 1/2
      p < 2

      Hence:

      24 < n < 96

      For a given n value, we can calculate the crossing positions using a time/distance graph:

      And then look for values where the distance between the positions divides exactly into the pool length.

      This Python program runs in 74ms. (Internal runtime is 4.6ms).

      from enigma import (irange, rdiv, line_intersect, catch, printf)
      
      # positions for J after lengths 0, 1, 2; P after length 0
      (J0, J1, J2, P0) = ((0, 1), (1, 0), (2, 1), (0, 0))
      
      # consider number of lengths completed by P at t=48
      for n in irange(25, 95):
        p = rdiv(48, n)
        # positions for P after lengths 1, 2
        (P1, P2) = ((p, 1), (2*p, 0))
      
        # calculate crossing position during first two lengths
        i1 = catch(line_intersect, J0, J1, P0, P1, internal=1, div=rdiv)
        i2 = catch(line_intersect, J1, J2, P1, P2, internal=1, div=rdiv)
        if i1 is None or i2 is None: continue
        ((t1, d1), (t2, d2)) = (i1.pt, i2.pt)
      
        # pool length is an exact multiple of the distance between the positions
        if d1 == d2: continue
        k = rdiv(1, abs(d1 - d2))
        if k % 1 != 0: continue
      
        # output solution
        printf("n={n} [p={p} k={k}]")
      

      Solution: Pat had completed 80 lengths.

      So Pat completes a length in 48/80 = 3/5 the time it takes John. And the distance between the crossing positions is exactly 1/2 the length of the pool.

      We have:

      (t1, d1) = (3/8, 5/8)
      (t2, d2) = (9/8, 1/8)

      Like

    • Frits's avatar

      Frits 2:27 pm on 30 July 2025 Permalink | Reply

      # L = length of the pool
      
      # 1st passing
      # 0     j1*L                  p1*L            L   j1 + p1 = 1
      # |---------------+---------------------------|
      
      # speed ratio r1 = p1 / j1
      
      # 2nd passing
      # 0                                           L  
      # |-------------------------------------------|
      #                  j2*L                  p2*L   j2 + p2 = 1
      # |------------------------------------+------|
      
      # speed ratio Pat / John
      # r = n / 48
      # j1 = 1 / (r + 1)
      # 2nd passing: distance that P swam = r * (distance that J swam)
      # L + j2.L = r.(L + p2.L) or 1 + j2 = r.(2 - j2) or j2 = (2.r - 1) / (r + 1)
      # j2 = (2 * r - 1) / (r + 1)
      
      #  24 < n < 96
      # n must be a multiple of 4 as denominator in formula below is a multiple of 4
      for n in range(28, 96, 4):
        # distance between those first two passing points > 0
        if n == 48: continue
        # L = multiple of the distance passing points = m * (abs(j1 - j2) * L)
        # if (r + 1) % (2 * r - 2): continue  
        if (n + 48) % (2 * n - 96): continue  
        print("answer:", n)
      

      Like

      • Frits's avatar

        Frits 3:05 pm on 30 July 2025 Permalink | Reply

        “n” can even be proven to be a multiple of 16.

        Like

  • Unknown's avatar

    Jim Randell 1:56 am on 27 July 2025 Permalink | Reply
    Tags:   

    Teaser 3279: It’s a doddle! 

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

    The Holmes’s new maths tutor, Dr Moriarty, wrote several two-digit by three-digit whole number multiplications on the board (with no answer repeated). Young Sherlock whispered to his brother, “That first one’s a doddle and the three-digit number is prime.”

    Mycroft replied, “Yes, it is a doddle, but the answer is also a DODDLE”. Sherlock looked puzzled. Mycroft explained that a a DODDLE is a number with “Descending-Order Digits left to right — all different — DivisibLe Exactly by each digit”.

    Mycroft continued, “All of the above applies to the second multiplication as well”. Sherlock noticed that just one other answer, to the final problem, was a DODDLE, with the three-digit value being odd, but not prime.

    What is the answer to the final problem?

    [teaser3279]

     
    • Jim Randell's avatar

      Jim Randell 1:57 am on 27 July 2025 Permalink | Reply

      This Python program collects numbers that are DODDLEs resulting from a multiplication with a 3-digit prime (in n1s) and those that result from a multiplication with a 3-digit odd non-prime (in n2s).

      We then look for a pair of results from n1s, and a different result from n2s to give a viable solution.

      It runs in 138ms. (Internal runtime is 59ms).

      from enigma import (irange, primes, cproduct, nsplit, is_decreasing, subsets, printf)
      
      # check if n is DODDLE
      def is_doddle(n):
        ds = nsplit(n)
        if 0 in ds: return False
        return all(n % d == 0 for d in ds) and is_decreasing(ds, strict=1)
      
      # collect results where the 3-digit number is prime or non-prime
      (n1s, n2s) = (set(), set())
      for (a, b) in cproduct([irange(10, 99), irange(101, 999, step=2)]):
        n = a * b
        if not is_doddle(n): continue
        if b in primes:
          printf("[1] {a} * {b} = {n}")
          n1s.add(n)
        else:
          printf("[2] {a} * {b} = {n}")
          n2s.add(n)
      
      # consider possible results for the first 2 problems
      for r12 in subsets(n1s, size=2):
        # find a final problem with a different result
        for rf in n2s.difference(r12):
          printf("r12={r12} -> rf={rf}", r12=sorted(r12))
      

      Solution: The answer to the final problem is: 6432.

      There are only 2 possible multiplications where the 3-digit number is prime that give a DODDLE.

      72 × 131 = 9432
      72 × 137 = 9864

      And these give the first two problems (in some order).

      There are only 3 possible multiplications where the 3-digit number is odd but not prime that give a DODDLE:

      24 × 393 = 9432
      24 × 411 = 9864
      32 × 201 = 6432

      But as no results are repeated the final multiplication on this list gives the required answer to the puzzle.


      In fact the only multi-digit DODDLEs are:

      3-digit: 432, 864
      4-digit: 6432, 9432, 9864

      Like

      • Jim Randell's avatar

        Jim Randell 3:44 pm on 27 July 2025 Permalink | Reply

        A faster approach is to generate possible DODDLEs (as there are not very many), and then factorise them into 2- and 3-digit numbers.

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

        from enigma import (irange, subsets, nconcat, chain, divisors_pairs, is_prime, printf)
        
        # generate k-digit doddles
        def doddle(k):
          for ds in subsets(irange(9, 1, step=-1), size=k):
            n = nconcat(ds)
            if all(n % d == 0 for d in ds):
              yield n
        
        # record results using 3-digit numbers that are prime and non-prime
        (n1s, n2s) = (set(), set())
        # consider 4- and 5-digit doddles
        for n in chain(doddle(4), doddle(5)):
          # look for 2-digit, 3-digit divisor pairs
          for (a, b) in divisors_pairs(n):
            # a is a 2-digit number
            # b is a 3-digit odd number
            if a > 99 or b < 100: break
            if a < 10 or b > 999 or b % 2 == 0: continue
            if is_prime(b):
              printf("[1] {a} * {b} = {n}")
              n1s.add(n)
            else:
              printf("[2] {a} * {b} = {n}")
              n2s.add(n)
        
        # consider possible results for the first 2 problems
        for r12 in subsets(n1s, size=2):
          # find a final problem with a different result
          for rf in n2s.difference(r12):
            printf("r12={r12} -> rf={rf}", r12=sorted(r12))
        

        Like

    • GeoffR's avatar

      GeoffR 12:31 pm on 28 July 2025 Permalink | Reply

      The teaser description states that there are three Doddle type numbers, which my posting is based upon.

      from enigma import is_prime, nsplit
      
      cnt = 0
      DL = []
      
      def is_doddle(num):
        # 4-digit Doddle numbers
        if 1000 < num < 10000:
          a, b, c, d = nsplit(num)
          # cannot divide by zero
          if 0 not in (b, c, d):
            if len(str(num)) == len(set(str(num))):
              if a > b > c > d and all(num % n == 0 \
              for n in (a, b, c, d)):
                return True
              
          # 5-digit Doddle numbers
          if 10000 < num < 99999:
            a, b, c, d, e = nsplit(num)
            if 0 not in (b, c, d, e):
              if len(str(num)) == len(set(str(num))) :
                if a > b > c > d > e and all(num % n == 0 \
                for n in (a, b, c, d, e)):
                  return True
                                       
          return False
      
      # find the first two DODDLE numbers
      # ... where the three digit number(b) is prime
      for a in range(10, 99):
        for b in range(101, 999):
          if not is_prime(b): continue
          if not is_doddle(a * b): continue
          cnt += 1
          if a * b not in DL:
            DL.append(a * b)
          if cnt == 2: 
            # we are specifically told there are
            # ... only 2 Doddle numbers with b as prime
            print(f"First two Doddle numbers are {DL[0]} and {DL[1]}")
                 
      # find the unique third DODDLE number
      # this three digit number (b) is not prime, but is of odd parity
      for a in range(10, 99):
        for b in range(101, 999):
          if not is_prime(b) and b % 2 == 1:
            if is_doddle(a * b) and a * b not in DL:
              print("The answer to the final problem is", a * b)
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:43 am on 24 July 2025 Permalink | Reply
    Tags: ,   

    Teaser 2477: [Shamrock in a box] 

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

    At the St Patrick’s Day Feis, Pat exhibits his painting “Shamrock in a Box”. This shows a square containing two touching circles of the same diameter (less than a metre). One circle also touches two adjacent sides of the square; the other touches the remaining two sides. For the middle leaf of the shamrock, Pat drew a third circle in the square: it touches each of the other circles, is the largest that will fit in the available space, and its radius is almost exactly a whole number of centimetres, that number being the same as the radius of the larger circles, but with the two digits in reverse order.

    What is the radius of the larger circles?

    This puzzle was originally published with no title.

    [teaser2477]

     
    • Jim Randell's avatar

      Jim Randell 9:43 am on 24 July 2025 Permalink | Reply

      The layout looks like this:

      Suppose the large circles have a radius 1.

      The semi-diagonal of the square is: 1 + √2.

      And if the smaller circle has a radius f (where 0 < f < 1), then we can also write the length of the semi-diagonal as: f√2 + h.

      Where: f + 1 = hypot(h, 1).

      And so we have two expressions for h:

      h = 1 + √2 − f√2
      h^2 = (f + 1)^2 − 1

      We can solve these equations to find the value of f, and then consider the actual radius of the large circle, R (a 2-digit integer, less than 50). And then calculate the radius of the smaller circle r = f . R.

      And we are look for cases where r is very close to the reverse of R.

      This Python program runs in 61ms. (Internal runtime is 193µs).

      from enigma import (Polynomial, sqrt, sq, irange, nrev, printf)
      
      r2 = sqrt(2)  # "root 2"
      
      # we have 2 expressions for h^2, which we can express as polynomials
      p1 = sq(Polynomial([1 + r2, -r2]))  # h^2 = (1 + sqrt(2) - f.sqrt(2))^2
      p2 = sq(Polynomial([1, 1])) - 1  # h^2 = (f + 1)^2 - 1
      
      # find real roots of p(f) = p1 - p2
      for f in (p1 - p2).roots(domain='F'):
        if not (0 < f < 1): continue
        printf("[f = {f}]")
      
        # consider 2-digit numbers for R
        for R in irange(10, 49):
          # the reverse should be a smaller 2-digit number
          rR = nrev(R)
          if rR < 10 or rR >= R: continue
      
          # check size of smaller circle is close to rev(R)
          r = f * R
          if abs(r - rR) < 0.05:
            printf("R={R} -> r={r}")
      

      Solution: The larger circles have a radius of 32 cm.

      And the smaller circles have a radius of 22.998 cm, which is very close to 23 cm.

      f is a root of the following quadratic equation:

      f^2 − (6 + 2√2)f + (3 + 2√2) = 0

      And this allows us to give a formula for f:

      f = 3 + √2 − 2√(2 + √2)
      f = 0.718695432327948…

      Hence:

      R = 32

      r = f . R = 22.99825383449434…

      Like

    • Frits's avatar

      Frits 12:49 pm on 24 July 2025 Permalink | Reply

      Using Jim’s formulas.

      # h = 1 + sqrt(2) - f.sqrt(2)
      # h^2 = (f + 1)^2 - 1 
      h1 = lambda f: 1 + (1 - f) * 2**.5 
      h2 = lambda f: ((f + 1)**2 - 1)**.5
      
      sols = set()
      for a in range(1, 5):
        for b in range(1, a):
          R, r = 10 * a + b, 10 * b + a
          f = r / R
          # store difference between 2 calculations for "h"
          sols.add((abs(h1(f) - h2(f)), R))
      
      if (mn := min(sols))[0] < 0.01:
        print("answer:", mn[-1])
      

      Like

  • Unknown's avatar

    Jim Randell 8:50 am on 22 July 2025 Permalink | Reply
    Tags:   

    Brainteaser 1263: I do apologise 

    From The Sunday Times, 16th November 1986 [link]

    The artist Pussicato has a new painting which is a 6×6 array of squares and each square is red or blue or green.

    (A) Label the horizontal rows of the painting U, V, W, X, Y, Z from top to bottom and the vertical columns 1, 2, 3, 4, 5, 6 from left to right. Then U1, U3, V5 are red; W2, Z2 are blue; and U2, V4, X2, X3, Y3, Y4 are green. Also V1 and W1 are the same colour, as are Y1 and Z1, and also X6 and Y6. Each row and each column contains two red squares, two blue and two green.

    (B) I do apologise. The information in (A) gives you a painting which is wrong in every square.

    (C) Use the information in (B), together with the following facts. Any two squares which are next to each other, horizontally or vertically, have different colours. There are more green squares than blue squares.

    (D) Look, I am so sorry. The information in (C) gives you a painting which is wrong in every square.

    (E) To be serious, the information in (B) and (D) is correct.

    Give (in order) the colours of the squares in the top row of Pussicato’s painting.

    [teaser1263]

     
    • Jim Randell's avatar

      Jim Randell 8:52 am on 22 July 2025 Permalink | Reply

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

      The program first uses the information in (A) to produce a grid (= “layout 1”). It then uses layout 1, and the information in (B) and (C) to produce another layout (= “layout 2”). It then uses layout 1, layout 2 and the information in (B) and (D) to produce a final layout, that provides the answer to the puzzle.

      Unfortunately, the program below fails with the standard CPython implementation (due to limitations in the CPython interpreter), but works fine with PyPy.

      (The code on codepad includes a workaround for CPython [*]).

      The following Python program runs in 132ms (under PyPy). (Internal runtime is 37.9ms).

      Run: [ @codepad ]

      from enigma import (enigma, SubstitutedExpression, first, sprintf as f, join, printf)
      
      # symbols for the 36 cells
      syms = first(enigma.str_upper + enigma.str_lower, 36)
      
      # map cells to symbols
      (rs, cs) = ("UVWXYZ", "123456")
      (v, vs) = (dict(), iter(syms))
      for r in rs:
        for c in cs:
          v[r + c] = next(vs)
      
      # rows and cols
      rows = list(list(r + c for c in cs) for r in rs)
      cols = list(list(r + c for r in rs) for c in cs)
      
      # colours
      (red, green, blue) = (0, 1, 2)
      
      
      # find layout 1
      def solve1():
      
        # construct expressions
        exprs = list()
      
        # U1, U3, V5 are red
        exprs.extend(f("{{{x}}} = {red}", x=v[x]) for x in ["U1", "U3", "V5"])
        # W2, Z2 are blue
        exprs.extend(f("{{{x}}} = {blue}", x=v[x]) for x in ["W2", "Z2"])
        # U2, V4, X2, X3, Y3, Y4 are green
        exprs.extend(f("{{{x}}} = {green}", x=v[x]) for x in ["U2", "V4", "X2", "X3", "Y3", "Y4"])
        # V1 = W1; Y1 = Z1; X6 = Y6
        exprs.extend(f("{{{x}}} = {{{y}}}", x=v[x], y=v[y]) for (x, y) in [("V1", "W1"), ("Y1", "Z1"), ("X6", "Y6")])
      
        # each row/column contains 2 red, 2 green, 2 blue
        check = lambda *vs: all(vs.count(x) == 2 for x in (red, green, blue))
        for r in rows:
          exprs.append(join((f("{{{x}}}", x=v[x]) for x in r), sep=", ", enc=["check(", ")"]))
        for c in cols:
          exprs.append(join((f("{{{x}}}", x=v[x]) for x in c), sep=", ", enc=["check(", ")"]))
      
        # make an alphametic puzzle to solve the grid
        p = SubstitutedExpression(exprs, base=3, distinct=0, env=dict(check=check))
        for s in p.solve(verbose=0):
          yield list(s[k] for k in syms)
      
      
      # find layout 2
      def solve2(g1):
      
        # no colour from layout 1 is correct
        d2i={ red: [], green: [], blue: [] }
        for (k, v) in zip(syms, g1):
          d2i[v].append(k)
      
        # no adjacent squares are the same colour
        distinct=list()
        for (i, k) in enumerate(syms):
          (r, c) = divmod(i, 6)
          if c < 5: distinct.append(k + syms[i + 1])
          if r < 5: distinct.append(k + syms[i + 6])
      
        # there are more green than blue squares
        check = lambda *vs: vs.count(green) > vs.count(blue)
        expr = join(syms, sep="}, {", enc=["check({", "})"])
      
        # make an alphametic puzzle to solve the grid
        p = SubstitutedExpression([expr], base=3, distinct=distinct, d2i=d2i, env=dict(check=check))
        for s in p.solve(verbose=0):
          yield list(s[k] for k in syms)
      
      
      # solve for layout 1
      for g1 in solve1():
        printf("g1={g1}")
        # solve for layout 2
        for g2 in solve2(g1):
          printf("g2={g2}")
          # generate the grid where each cell is different from g1 and g2
          g3 = list(3 - (c1 + c2) for (c1, c2) in zip(g1, g2))
          printf("g3={g3}")
      
          # output the grid
          d = { red: 'R', green: 'G', blue: 'B' }
          printf()
          for (i, v) in enumerate(g3):
            if i % 6 == 0: printf("[ \\")
            printf("{v} \\", v=d[v])
            if i % 6 == 5: printf("]")
      

      Solution: The squares in the top row are: blue, blue, green, red, blue, red.

      The requirements in (A) give a single layout (shown below on the left).

      The requirements (B) and (C) also give a single layout (shown below in the centre).

      And finally, the requirements (D) give a single layout (where each square is different colour to the corresponding positions the first two grids). (This is shown below on the right).


      [*] When producing layout 2 there is only a single expression, involving all 36 variables, so the “denesting” code cannot split the expressions into blocks that use fewer expressions. To fix this we add in the following expression, which mentions half the symbols (and is always true), and this allows the denesting code to do its job:

      true(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R)
      

      Like

  • Unknown's avatar

    Jim Randell 7:20 am on 20 July 2025 Permalink | Reply
    Tags:   

    Teaser 3278: Page weight 

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

    An A5 booklet is produced by printing the pages onto sheets of A4 (both sides), then binding and folding along the middle. The booklet contains two chapters. The longer first chapter runs to 34 pages.

    The pages in each chapter are numbered sequentially from 1 upwards. The front cover is page 1 of chapter one. Page 1 of chapter two follows directly after page 34 of chapter one. Any blank pages at the end of the booklet are not numbered.

    Without changing their order, the sheets can be split into two piles, where the sums of the page numbers in each pile are equal.

    How many pages does the second chapter have?

    [teaser3278]

     
    • Jim Randell's avatar

      Jim Randell 7:52 am on 20 July 2025 Permalink | Reply

      Although not explicitly stated, I am assuming the booklet consists of the minimum number of A4 sheets required to fit all pages of both chapters.

      Here is a constructive solution, that constructs the pairs of pages and organises them into A4 sheets, and then looks for a scenario where the sheets can be split as required. (This allows easy output of the page numbers on each A4 sheet, if desired).

      The following Python program runs in 60ms. (Internal runtime is 539µs).

      Run: [ @codepad ]

      from enigma import (irange, divc, div, chunk, chain, printf)
      
      # pages for chapter 1
      n1 = 34
      
      # consider the number of pages in chapter 2 (< n1)
      for n2 in irange(1, n1 - 1):
      
        # pair up the pages
        pairs = list(chunk(chain(irange(1, n1), irange(1, n2)), 2))
        # total sum of all page numbers divided by 2
        s = div(sum(map(sum, pairs)), 2)
        if s is None: continue
      
        # total number sheets of A4 required
        t = divc(n1 + n2, 4)
      
        # collect the page numbers for each sheet
        sheets = [()] * t
        for (i, p) in zip(chain(irange(t), irange(t, step=-1)), pairs):
          sheets[i] += p
      
        # can we find some sheets that sum to s ?
        x = 0
        for k in irange(t):
          x += sum(sheets[k])
          if x == s:
            # output solution
            printf("chapter 2 has {n2} pages [{t} sheets in total; first {k} sheets sum to {x}]", k=k + 1)
          if x >= s:
            break
      

      Solution: Chapter 2 has 25 pages.

      There are 15 A4 sheets, and the page numbers are as follows:

      1: (1, 2) (25, -)
      2: (3, 4) (23, 24)
      3: (5, 6) (21, 22)
      4: (7, 8) (19, 20)
      5: (9, 10) (17, 18)
      6: (11, 12) (15, 16)
      7: (13, 14) (13, 14)
      8: (15, 16) (11, 12)
      9: (17, 18) (9, 10)

      10: (19, 20) (7, 8)
      11: (21, 22) (5, 6)
      12: (23, 24) (3, 4)
      13: (25, 26) (1, 2)
      14: (27, 28) (33, 34)
      15: (29, 30) (31, 32)

      The sum of the page numbers of sheets 1-9 and sheets 10-15 both come to 460.


      If we can construct the booklet with more than the minimum required number of A4 sheets, then it is possible to construct a further solution:

      If chapter 2 has 14 pages and the booklet was printed from 17 sheets, then the sum of the page numbers on sheets 1-12 and sheets 13-17 would both come to 350 (and there would be 20 blank pages at the end of the booklet).

      This justifies the initial assumption.


      The program can be used to investigate similar puzzles where the number of pages in the first chapter is other than 34.

      An interesting case is when chapter 1 has 19 pages. There are then 2 solutions:

      Chapter 2 has 4 pages.
      The booklet consists of 6 sheets.
      The first 4 (and last 2) sheets have page numbers which sum to 100.

      Chapter 2 has 16 pages.
      The booklet consists of 9 sheets.
      The first 5 (and last 4) sheets have page numbers which sum to 163.

      Like

      • Jim Randell's avatar

        Jim Randell 9:15 am on 20 July 2025 Permalink | Reply

        Here is a faster version of my program that works with a list of the sum of pairs of pages on a leaf.

        It has an internal runtime of 124µs.

        from enigma import (irange, chunk, div, divc, seq_get, printf)
        
        # pages for chapter 1
        n1 = 34
        
        # page pair sums from chapter 1
        pairs = list(map(sum, chunk(irange(1, n1), 2)))
        t = sum(pairs)
        get = seq_get(pairs, default=0)
        
        # consider the number of pages in chapter 2 (< n1)
        for (i, n2) in enumerate(irange(1, n1 - 1), start=n1):
          # make the pair sum of the final page
          if i % 2:
            pairs[-1] += n2
          else:
            pairs.append(n2)
          t += n2
          s = div(t, 2)
          if s is None: continue
        
          # now look for sheets that sum to s, by pairing up page sums
          (x, k) = (0, len(pairs))
          if k % 2 == 0: k -= 1
          for i in irange(divc(k, 2)):
            x += get(i) + get(k - i)
            if x == s:
              # output solution
              printf("chapter 2 has {n2} pages [first {i} sheets sum to {x}]", i=i + 1)
            if x >= s:
              break
        

        Like

    • Frits's avatar

      Frits 2:45 pm on 20 July 2025 Permalink | Reply

      One loop.

      The sum of 4 page numbers per sheet can only have 2, 3 or 4 different values (per number of pages in chapter 2).

      It took a while before I figured out how the booklet was constructed.

      # one folded A4 sheet results in 4 pages
      
      t = 17 * 35        # sum(1...34)
      # number of pages in chapter 2
      for n in range(1, 34):
        # sum of all pages
        t += n
        # both <isum> as <osum> are even so <h> must also be even
        if t % 4: continue
        h = t // 2   
        
        n_sheets = (37 + n) // 4
        n_inner_sheets = 17 - n_sheets
        
        # calculate starting page number of most inner sheet
        st = 2 * ((n + 1) // 4) + 17
        
        # calculate sum of 4 outer sheet page numbers (except most outer sheet)
        osum = (2 * (n + n % 2) - 1) + 3 if n > 1 else -1
        # calculate sum of most inner sheet page numbers
        isum = 4 * st + 6 if n_sheets < 17 else osum
        
        # can we make <h> using <k> sheets (beginning from the center)?
       
        # h = a.isum + b.osum or  b = (h - a.isum) / osum
        # use maximum <n_inner_sheets> inner sheets and possible <b> outer sheets
        d, r = divmod(h, isum)
        if not r and d <= n_inner_sheets:
          print("answer:", n)
        else:  
          # h - n_inner_sheets * inner = b * osum
          b, r = divmod(h - n_inner_sheets * isum, osum)
          if not r and 0 < b <= n_sheets - n_inner_sheets:
            if osum > 0: 
              print("answer:", n)
      

      Like

  • Unknown's avatar

    Jim Randell 8:19 am on 18 July 2025 Permalink | Reply
    Tags:   

    Teaser 2518: [Empty boxes] 

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

    For an appropriate Boxing Day exercise, write a digit in each of the empty boxes so that (counting all the digits from here to the final request) the following statements are true:

    Number of occurrences of 0 = [ _ ]
    Number of occurrences of 1 = [ _ ]
    Total occurrences of 2s and 3s = [ _ ]
    Total occurrences of 4s and 5s = [ _ ]
    Total occurrences of 6s and 7s = [ _ ]
    Total occurrences of 8s and 9s = [ _ ]
    A digit you have written in another box = [ _ ]
    The average of all your written digits = [ _ ]

    What, in order, are the digits in your boxes?

    This puzzle was originally published with no title.

    [teaser2518]

     
    • Jim Randell's avatar

      Jim Randell 8:20 am on 18 July 2025 Permalink | Reply

      There are 8 boxes to be filled out with a digit (from 0 to 9), and each of the digits 0 to 9 already occurs once before the boxes are filled out.

      The first six boxes count the 10 given digits, plus those in the 8 boxes, so must sum to 18.

      The first 2 boxes must contain 1 or more, and the next 4 boxes must contain 2 or more.

      And the box with the mean must also be 2 or more.

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

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

      Run: [ @codepad ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # let the 8 empty boxes be: A B C D E F G H
      --distinct=""
      --digits="1-9"
      --invalid="1,CDEFH"
      --macro="@boxes = [A, B, C, D, E, F, G, H]"
      
      # first 2 boxes
      "@boxes.count(0) + 1 = A"
      "@boxes.count(1) + 1 = B"
      
      # next 4 boxes
      "@boxes.count(2) + @boxes.count(3) + 2 = C"
      "@boxes.count(4) + @boxes.count(5) + 2 = D"
      "@boxes.count(6) + @boxes.count(7) + 2 = E"
      "@boxes.count(8) + @boxes.count(9) + 2 = F"
      
      # final 2 boxes
      "G in [A, B, C, D, E, F, H]"
      "iavg(@boxes) = H"
      
      --template="@boxes"
      --solution=""
      
      # [optional] additional constraints to improve run time
      # the sum of the first 6 boxes must be 18
      "A + B + C + D + E + F = 18"
      # we can't write 0 in any of the boxes, so there is only 1 zero
      --assign="A,1"
      

      Solution: The digits are: 1, 2, 8, 2, 2, 3, 3, 3.

      The statements then become:

      Number of occurrences of 0 = [ 1 ]
      Number of occurrences of 1 = [ 2 ]
      Total occurrences of 2s and 3s = [ 8 ]
      Total occurrences of 4s and 5s = [ 2 ]
      Total occurrences of 6s and 7s = [ 2 ]
      Total occurrences of 8s and 9s = [ 3 ]
      A digit you have written in another box = [ 3 ]
      The average of all your written digits = [ 3 ]

      Like

      • Frits's avatar

        Frits 2:04 pm on 18 July 2025 Permalink | Reply

        @Jim, you can also add B in line 8 (ao because of line 32).

        Like

  • Unknown's avatar

    Jim Randell 8:21 am on 15 July 2025 Permalink | Reply
    Tags:   

    Teaser 2489: [Digit circle] 

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

    Your task today is to put more than two digits evenly spaced around a circle so that:

    (a) Any three adjacent digits clockwise form a prime number.

    (b) Any three adjacent digits anti-clockwise form a prime number.

    (c) If a digit occurs in the circle, then it occurs a prime number of times.

    (d) The total of all the digits around the circle is a prime.

    What is the total of all the digits?

    This puzzle was originally published with no title.

    [teaser2489]

     
    • Jim Randell's avatar

      Jim Randell 8:21 am on 15 July 2025 Permalink | Reply

      Each digit is the last digit of a 3-digit prime, so the only possible digits are 1, 3, 9, 7.

      This Python program looks for solutions using increasing numbers of digits, and stops after the smallest possible sequences are found.

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

      Run: [ @codepad ]

      from enigma import (irange, inf, primes, multiset, subsets, tuples, rev, nconcat, wrap, uniq, printf)
      
      # up to 3-digit primes
      primes.expand(999)
      
      # possible digits
      digits = [1, 3, 7, 9]
      
      # generate numbers from <k> adjacent digits of <ns> (in both directions)
      @wrap(uniq)  # skip duplicate numbers
      def adj(ns, k=3):
        for ds in tuples(ns, k, circular=1):
          yield nconcat(ds)
          yield nconcat(rev(ds))
      
      # look for solutions using <n> digits
      def solve(n):
        # choose the n digits
        for ds in subsets(digits, size=n, select='R'):
          # the sum of the digits is prime
          if not (sum(ds) in primes): continue
          # each digit occurs a prime number of times
          m = multiset.from_seq(ds)
          if not all(v in primes for v in m.values()): continue
      
          # start with the smallest number
          n0 = m.min()
          # and choose an arrangement of the rest
          for ns in subsets(m.difference([n0]), size=len, select='mP', fn=list):
            if rev(ns) < ns: continue  # skip duplicate solutions
            ns.insert(0, n0)
            # check each 3 adjacent digits form a prime (in both directions)
            if not all(x in primes for x in adj(ns)): continue
            yield ns
      
      # find the smallest possible sequences
      for n in irange(3, inf):
        s = 0
        for ns in solve(n):
          printf("[n={n}] {ns} -> {t}", t=sum(ns))
          s += 1
        if s: break
      

      Solution: The total of the digits is 29.

      The digits in the circle are: (1, 9, 1, 9, 9). Consisting of 2 occurrences of the digit 1, and 3 occurrences of the digit 9.

      The 3-digit numbers formed are: 191, 199, 919, 991. Which are all prime.

      Like

    • Ruud's avatar

      Ruud 8:03 am on 16 July 2025 Permalink | Reply

      import istr
      import collections
      import itertools
      
      primes = [i for i in istr.range(2, 1000) if i.is_prime()]
      candidates = {str(prime) for prime in primes if len(prime) == 3 and all(c in "1379" for c in prime)}
      for n in itertools.count(2):
          for x in itertools.product("1379", repeat=int(n)):
              s = "".join(x)
              if all(count in primes for count in collections.Counter(s).values()):
                  s2 = str(s + s[0:2])
                  if (
                      all(s2[i : i + 3] in candidates for i in range(len(s)))
                      and all(s2[i : i + 3][::-1] in candidates for i in range(len(s)))
                      and (sumx:=sum(map(int, x))) in primes
                  ):
                      print(s,sumx)
                      assert False
      

      Like

  • Unknown's avatar

    Jim Randell 6:24 am on 13 July 2025 Permalink | Reply
    Tags: ,   

    Teaser 3277: Croquet equipment 

    From The Sunday Times, 13th July 2025 [link] [link]

    Our non-standard croquet lawn has six hoops, at positions A to F, and a central peg at P. The equipment storage box is in the southwest corner at S, and the lawn is symmetrical in both the east-west and north-south directions. The diagram is not to scale, but D is south of the projected line from S to E.

    Also AB is half the width of the lawn. When setting out the equipment, I walk along the route SEFDPCBAS. All of the distances on my route between positions are whole numbers of feet (less than 60), and both D and E are whole numbers of feet north of the south boundary.

    What is the total distance of my route?

    [teaser3277]

     
    • Jim Randell's avatar

      Jim Randell 8:46 am on 13 July 2025 Permalink | Reply

      I get two solutions, so here is a straightforward solution to make sure I haven’t missed any thing.

      But if we disallow distances of 60ft or more between any two positions on the walk (not just adjacent positions), then we can eliminate the larger of these solutions.

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

      from enigma import (irange, ihypot, printf)
      
      M = 59  # max length of a path segment
      
      # let AB = EF = 2x
      for x in irange(1, M // 2):
        # e = distance E north from southern edge
        for e in irange(1, M):
          SE = ihypot(x, e)
          if SE is None or SE > M: continue
      
          # d = distance D is north of EF
          for d in irange(1, e - 1):
            FD = ihypot(x, d)
            if FD is None or FD > M: continue
      
            # p = distance P north of D
            for p in irange(1, M):
              SA = ihypot(x, e + d + p + p + d)
              if SA is None or SA > M: continue
      
              # total distance walked
              T = SE + SA + 2*FD + 4*x + 2*p
              printf("T={T} [SE={SE} FD={FD} SA={SA}; x={x} e={e} d={d} p={p}]")
      

      Solution: There are two viable solutions to the puzzle: 142 ft, and 220 ft.

      Apparently the size of a croquet lawn is not fixed, but it should be a rectangle with sides in the ratio of 1.25.

      The first of the solutions involves a playing area of 48 ft × 44 ft (ratio = 1.09).

      And all positions on the route are within 60 ft of S (as indicated by the dotted line).

      The second of the solutions involves a playing area of 96 ft × 42 ft (ratio = 2.29).

      And B and F are further than 60 ft from S.


      The published solution is: “142 feet”.

      But a correction was issued with Teaser 3280:

      There were in fact two possible answers: 142 and 220 feet.
      Either was accepted as correct.

      .

      Like

      • Frits's avatar

        Frits 11:38 am on 13 July 2025 Permalink | Reply

        @Jim, shouldn’t we have: e + d + p = 2.x ?

        Like

      • Jim Randell's avatar

        Jim Randell 4:19 pm on 13 July 2025 Permalink | Reply

        Alternatively, using the [[ pythagorean_triples() ]] function from the enigma.py library.

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

        from enigma import (defaultdict, pythagorean_triples, subsets, div, printf)
        
        M = 59  # maximum length of a path segment
        
        # collect (<other>, <hypot>) sides for pythagorean triples
        t = defaultdict(list)
        for (x, y, z) in pythagorean_triples(M):
          t[x].append((y, z))
          t[y].append((x, z))
        
        # consider possible x values
        for x in sorted(t.keys()):
          if 2 * x > M: break
          # FD, SE, SA all have base x, and increasing vertical sides
          ts = sorted(t[x])
          for ((d, FD), (e, SE), (a, SA)) in subsets(ts, size=3):
            p = div(a - e - 2*d, 2)
            if p is None or p < 1: continue
        
            # total distance walked
            T = SE + SA + 2*FD + 4*x + 2*p
            printf("T={T} [SE={SE} FD={FD} SA={SA}; x={x} e={e} d={d} p={p}]")
        

        Like

    • Hugo's avatar

      Hugo 10:34 am on 21 July 2025 Permalink | Reply

      If hoop P has coordinates (0, 0) then it seems
      A is at (12, -13), B is at (12, 13)
      C is at (0, 8), D is at (0, -8)
      E is at (-12, -13), F is at (-12, 13)
      S is at (-24, -22).

      It would have been kind of them to tell us that all the distances, including x and y separations, are integers.

      Like

  • Unknown's avatar

    Jim Randell 10:12 am on 11 July 2025 Permalink | Reply
    Tags: ,   

    Teaser 2475: [Stopped clock] 

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

    “I see that our long case has stopped”, said Watson, who had been reading Holmes’s monograph on chronometers. “Is there anything significant about the time?” Holmes replied: “Apart from the obvious facts that just one hand is precisely on 12, and clockwise the hands are in the order “second” , “minute”, “hour”, I see that the time read as a 3- or 4-digit number is the product of two whole numbers, the larger of which is the number of minutes clockwise from the minute hand to the hour hand”.

    At what time did the clock stop?

    This puzzle was originally published with no title.

    [teaser2473]

     
    • Jim Randell's avatar

      Jim Randell 10:12 am on 11 July 2025 Permalink | Reply

      If one of the hands is on the 12, then the clock must be showing an exact number of minutes, and so the second hand must be on 12.

      Then as we go clockwise from the second hand we encounter the minute hand (which is on an exact minute marking), and then the hour hand (which must also be on an exact minute marking, so the number of minutes must be a multiple of 12).

      This Python program considers possible hours and minutes, calculates the number of minute divisions the hour hand is ahead of the minute hand and then checks that this divides into the time (read as a 3- or 4-digit number) to give a smaller number.

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

      from enigma import (irange, cproduct, divc, div, printf)
      
      # possible hours and minutes (must be a multiple of 12)
      for (h, m) in cproduct([irange(1, 11), irange(12, 59, step=12)]):
      
        # number of minutes pointed to by the hour hand
        p = (5 * h) + (m // 12)
        # minute divisions the hour hand is ahead of the minute hand
        d = p - m
        if not (d > 0): continue
      
        # time read as a 3- or 4-digit number
        n = 100 * h + m
        r = div(n, d)
        if r is None or not (d > r): continue
      
        # output solution
        printf("{h:02d}:{m:02d} -> {n} = {d} * {r}")
      

      Solution: The clock stopped at 8:12.

      The second hand points to 12 (= 0 minutes), the minute hand to 12 minutes, and the hour hand to 8 + 12/60 hours (= 41 minutes).

      The hour hand is 29 minute divisions ahead of the minute hand, and: 812 = 29 × 28.

      Like

  • Unknown's avatar

    Jim Randell 8:25 am on 8 July 2025 Permalink | Reply
    Tags: by: Bert Brooke   

    Brainteaser 1031: Definitive darts 

    From The Sunday Times, 2nd May 1982 [link]

    We of the Private Bar of the Wapping Boar are an exclusive elite in darting fraternity; albeit we play the conventional darts of commoner folk and employ their customary board and rules. Our two opposing players throw three darts each, in turn. We require that the last (winning) dart of any game must score some double or the bull (50) — and must thereby raise the player’s score precisely to 501.

    Last evening, two of our virtuosi played an epic exhibition; a series of games were all won by the first-to-throw and each game, required the least possible number of darts. The precision was faultless and no dart thrown by the eventual winner of any game ever exceeded the score of any preceding dart in that game. Later, our President eulogised and vouched that in the suite but recently enjoyed, every possible scoring sequence of winning games (within our definition) had been played just once.

    “What about my record?” interceded that parvenu from the “public” (one we import to manage the chalks and call the 3 dart total following each throw). The low fellow claims a maximum possible of top score shouts “One hundred and eighty!!” in a demonstration match such as we’d just had; he insists his feat be recognised. The oaf must have his due if we are to use him anew, but there’s the rub — who scores for a scorer?

    Readers, please say the maximum of correct “180” shouts to be credited in our circumstance.

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

    [teaser1031]

     
    • Jim Randell's avatar

      Jim Randell 8:25 am on 8 July 2025 Permalink | Reply

      It is not possible to score 501 in fewer than 9 darts, as 60 is the maximum possible score with a single dart, and 501 / 60 > 8.

      But it is possible to score 501 in 9 darts, as 501 = 3 × 167, and 167 = 60 (treble 20) + 57 (treble 19) + 50 (bull). So we can finish the 9 dart run on a bull.

      We need to find sequences of 9 darts, with scores in non-increasing order, that score a total of 501, and finish on a double.

      And this corresponds to the winners throws. The winner went first, so the loser in each match only threw 6 darts, and as the scorer shouted “180” the maximum possible number of times, the loser must have thrown 6×60 for 2 shouts of “180”.

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

      Run: [ @codepad ]

      from enigma import (irange, union, seq_items, chunk, printf)
      
      # possible scores with 3 darts
      single = union([irange(1, 20), [25]])
      double = set(2 * x for x in single)
      treble = set(3 * x for x in single if x != 25)
      darts = sorted(union([single, double, treble]), reverse=1)
      
      # score <t> in <k> darts (finishing with a double)
      # using non-increasing subsets of darts[i..]
      def score(t, k, i=0, ss=[]):
        x = darts[i]
        # finish on a double
        if k == 1:
          if t in double and not (x < t):
            yield ss + [t]
        elif not (x * k < t):
          # score the next dart
          for (j, x) in seq_items(darts, i):
            if x < t:
              yield from score(t - x, k - 1, j, ss + [x])
      
      # score 501 in 9 darts, count the number of matches, and "180" shouts
      n = s = 0
      for ss in score(501, 9):
        n += 1
        gs = list(chunk(ss, 3))
        x = sum(sum(g) == 180 for g in gs)
        s += x
        printf("[{n}: {gs}; {x} shouts]")
      
      # total number of shouts
      t = s + 2 * n
      printf("total shouts = {t} [from {n} matches]")
      

      Solution: There were 62 shouts of “180”.

      There were 18 matches. The winners scores as follows:

      1: [(60, 60, 60), (60, 60, 60), (60, 57, 24)]; 2 shouts
      2: [(60, 60, 60), (60, 60, 60), (60, 51, 30)]; 2 shouts
      3: [(60, 60, 60), (60, 60, 60), (60, 45, 36)]; 2 shouts
      4: [(60, 60, 60), (60, 60, 60), (57, 54, 30)]; 2 shouts
      5: [(60, 60, 60), (60, 60, 60), (57, 50, 34)]; 2 shouts
      6: [(60, 60, 60), (60, 60, 60), (57, 48, 36)]; 2 shouts
      7: [(60, 60, 60), (60, 60, 60), (54, 51, 36)]; 2 shouts
      8: [(60, 60, 60), (60, 60, 60), (51, 50, 40)]; 2 shouts
      9: [(60, 60, 60), (60, 60, 57), (57, 57, 30)]; 1 shout
      10: [(60, 60, 60), (60, 60, 57), (57, 51, 36)]; 1 shout
      11: [(60, 60, 60), (60, 60, 57), (54, 54, 36)]; 1 shout
      12: [(60, 60, 60), (60, 60, 57), (54, 50, 40)]; 1 shout
      13: [(60, 60, 60), (60, 60, 51), (50, 50, 50)]; 1 shout
      14: [(60, 60, 60), (60, 57, 57), (57, 54, 36)]; 1 shout
      15: [(60, 60, 60), (60, 57, 57), (57, 50, 40)]; 1 shout
      16: [(60, 60, 60), (60, 57, 54), (50, 50, 50)]; 1 shout
      17: [(60, 60, 60), (57, 57, 57), (57, 57, 36)]; 1 shout
      18: [(60, 60, 60), (57, 57, 57), (50, 50, 50)]; 1 shout

      This accounts for 26 of the shouts.

      The loser in each match accounts for another 2 shouts per match, giving 26 + 18×2 = 62 shouts in total.

      Like

      • Frits's avatar

        Frits 12:12 pm on 10 July 2025 Permalink | Reply

        @Jim, as “darts” is non-increasing the check in line 20 can be done outside the loop (if needed at all as x always seems to be less than t).

        Like

        • Jim Randell's avatar

          Jim Randell 12:36 pm on 11 July 2025 Permalink | Reply

          @Frits: Yes, we could skip any elements that are not less than the remaining total, and then just process all the remaining elements without testing (as they are in decreasing order). But I didn’t think it was worth the additional complication.

          But we do need the test, for example, if we wanted to find a 2 dart finish from 37 we would call [[ score(37, 2) ]].

          Like

    • Frits's avatar

      Frits 6:08 pm on 8 July 2025 Permalink | Reply

      N = 9 # number of throws needed
      
      # return a valid loop structure string, indent after every "for" statement
      def indent(s):
        res, ind = "", 0
        for ln in s:
          res += " " * ind + ln + "\n"
          if len(ln) > 2 and ln[:3] == "for":
            ind += 2
        return res  
      
      syms = "ABCDEFGHIJKLMNOPQR"
      throws = ["X"] + list(syms[:N])
      varstring = "[" + ', '.join(throws[1:]) + "]"
      # possible scores with 3 darts
      single = set(range(1, 21)) | {25}
      double = {2 * x for x in single}
      treble = {3 * x for x in single if x != 25}
      
      # determine mininum for the first 8 throws
      mn = 501 - (7 * max(treble) + max(double))
      # darts to be used for the first 8 throws
      darts = sorted([x for x in single | double | treble if x >= mn], reverse=True)
      
      # generate an execution string with 8 loops
      exprs = [f"X = {max(treble)}; cnt = 0"]           # X is a dummy variable
      for i, t in enumerate(throws[1:], 1):
        if i < N:
          exprs.append(f"for {t} in [x for x in {darts} if x <= {throws[i - 1]}]:") 
          if i < N - 1:
            exprs.append(f"if sum([{', '.join(throws[1:i + 1])}]) + "
                         f"{N - i - 1} * {throws[i]} + {max(double)} < 501: break")
        else:
          # most inner loop
          exprs.append(f"{throws[N]} = 501 - sum([{', '.join(throws[1:-1])}])")   
          exprs.append(f"if {throws[N]} > {throws[N - 1]}: break")    
          exprs.append(f"if not ({throws[N]} in {double}): continue")   
          # the loser in each game accounts for another 2 shouts per game
          exprs.append(f"cnt += sum({varstring}[3 * i:3 * (i + 1)] == "
                       f"[max(treble)] * 3 for i in range(3)) + 2")
          #exprs.append(f"print({varstring})")
             
      exprs = indent(exprs)
      exprs += f"print('answer:', cnt)"
      #print(exprs)
      exec(exprs)
      

      The program generates (and executes) the following code:

      X = 60; cnt = 0
      for A in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= X]:
        if sum([A]) + 7 * A + 50 < 501: break
        for B in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= A]:
          if sum([A, B]) + 6 * B + 50 < 501: break
          for C in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= B]:
            if sum([A, B, C]) + 5 * C + 50 < 501: break
            for D in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= C]:
              if sum([A, B, C, D]) + 4 * D + 50 < 501: break
              for E in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= D]:
                if sum([A, B, C, D, E]) + 3 * E + 50 < 501: break
                for F in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= E]:
                  if sum([A, B, C, D, E, F]) + 2 * F + 50 < 501: break
                  for G in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= F]:
                    if sum([A, B, C, D, E, F, G]) + 1 * G + 50 < 501: break
                    for H in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= G]:
                      I = 501 - sum([A, B, C, D, E, F, G, H])
                      if I > H: break
                      if not (I in {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 50}): continue
                      cnt += sum([A, B, C, D, E, F, G, H, I][3 * i:3 * (i + 1)] == [max(treble)] * 3 for i in range(3)) + 2
      print('answer:', cnt)
      

      Like

  • Unknown's avatar

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

    Teaser 3276: First and last multiple 

    From The Sunday Times, 6th July 2025 [link] [link]

    Johann enjoys playing with numbers and making up numerical challenges for his elder brother Jacob. He has worked out a new challenge, which involves Jacob trying to find the whole-number square root of a six-digit number. He explained that the sum of the individual digits [of the six-digit number] is exactly five times the sum of its first and last digits. To make it easier, he tells him that no number is repeated in the six digits, there are no zeros, and the last three digits are in descending numerical order.

    What is the square root of the six digit number?

    [teaser3276]

     
    • Jim Randell's avatar

      Jim Randell 6:48 am on 6 July 2025 Permalink | Reply

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

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

      Run: [ @codepad ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # sqrt(ABCDEF) = XYZ
      --distinct="ABCDEF"
      --invalid="0,ABCDEFXZ"
      --answer="XYZ"
      
      # XYZ is the square root of ABCDEF
      "sq(XYZ) = ABCDEF"
      
      # the sum of the digits of the square is 5 times the
      # sum of the first and last digit
      "A + B + C + D + E + F == 5 * (A + F)"
      
      # the final three digits are in descending order
      "D > E > F"
      
      # [optional] performance tweaks
      --reorder="[1]"
      --invalid="1|2,X"
      

      Solution: The square root is 661.

      And so the 6-digit number is: 661^2 = 436921.

      And we have:

      4 + 3 + 6 + 9 + 2 + 1 = 25 = 5 × (4 + 1)
      9 > 2 > 1

      Like

    • GeoffR's avatar

      GeoffR 10:17 am on 6 July 2025 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:a; var 1..9:b; var 1..9:c; var 1..9:d;
      var 1..9:e; var 1..9:f; 
      var 316..999:ans; % square root of abcdef
      
      var 100000..999999: abcdef == 100000*a + 10000*b 
      + 1000*c + 100*d + 10*e + f;
      
      constraint all_different([a, b, c, d, e, f]);
      
      constraint a + b + c + d + e + f == 5 * (a + f);
      constraint d > e /\ e  > f;
      
      predicate is_sq(var int: c) =
         let {
            var 0..ceil(pow(int2float(ub(c)),(1/2))): z
         } in z*z = c;
      
      constraint ans * ans == abcdef;
      constraint is_sq(abcdef);
      
      solve satisfy;
      
      output["Square root of the six digit number = " ++  show(ans)];
      
      
      

      Like

    • Frits's avatar

      Frits 3:03 pm on 6 July 2025 Permalink | Reply

      # last 2 digits y and z  (abcdef = xyz^2)
      last2 = set(i for i in range(11, 100) 
                  if i % 10 and 9 < (m := (i * i) % 100) < 90 and m // 10 > m % 10)
      
      for yz in last2:
        # A + F <= 7 so X < 9
        for x in range(3, 9):
          xyz = 100 * x + yz
          abcdef = xyz * xyz
          if len(set(s := str(abcdef))) != 6: continue
          if s[3] <= s[4] or "0" in s: continue
          dgts6 = [int(x) for x in s]
          if sum(dgts6) != 5 * (dgts6[0] + dgts6[-1]): continue
      
          print("answer:", xyz)
      

      Like

      • Frits's avatar

        Frits 5:57 pm on 6 July 2025 Permalink | Reply

        Processing six-digit numbers to find a valid square root.

        from enigma import is_square
        
        # last 2 digits e and f  (abcdef = xyz^2)
        last2 = set(divmod(m, 10) for i in range(11, 100) 
                    if i % 10 and 9 < (m := (i * i) % 100) < 90 and m // 10 > m % 10)
        for (e, f) in last2:
          ef = 10 * e + f
          for d in range(e + 1, 10):
            _def = 100 * d + ef
            # 21 <= 5 * (a + f) <= 39  or  4 < a + f < 8
            for a in set(range(max(1, 5 - f), 8 - f))- {d, e, f}:
              # remaining for b + c
              if not (3 <= (bc := 5 * (a + f) - (a + d + e + f)) <= 17): continue
              adef = 10**5 * a + _def
              # determine missing numbers x and y (x + y = bc with x < y)
              for x in set(range(max(1, bc - 9), (bc + 1) // 2)) - {a, d, e, f}:
                if (y := bc - x) in {a, x, d, e, f}: continue
                for b, c in ((x, y), (y, x)):
                  n = adef + 10**4 * b + 10**3 * c
                  if (xyz := is_square(n)):
                    print("answer:", xyz)
        

        Like

    • Ruud's avatar

      Ruud 4:22 pm on 6 July 2025 Permalink | Reply

      import math
      import istr
      
      for i in range(int(math.sqrt(100_000)), int(math.sqrt(1_000_000))):
          if (sum((sq := istr(i * i))) == 5 * (sq[0] + sq[-1])) and sq.all_distinct() and "0" not in sq and sq[-3] > sq[-2] > sq[-1]:
              print(sq, i)
      

      Like

    • GeoffR's avatar

      GeoffR 9:17 am on 9 July 2025 Permalink | Reply

      sq6 = [ n**2 for n in range(317, 1000)    
              if len(set(str(n**2))) == 6 ]
      
      for num in sq6:
        ds = str(num)
        if '0' in ds: continue
        dig = [int(x) for  x in ds]
        if (dig [3] > dig[4] > dig[5]):
          if sum(dig[:6]) == 5 * (dig[0] + dig[5]):
            print (f"Square root is {int(num ** 0.5)}.")
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:55 am on 4 July 2025 Permalink | Reply
    Tags:   

    Teaser 2478: [Palindromic primes] 

    From The Sunday Times, 21st March 2010 [link]

    An article in The Times of January 2 commented that 2010 was the sum of four primes. As a reader pointed out in a letter on January 5, this was hardly worthy of comment: by Goldbach’s Conjecture, any even number greater than two is the sum of just two primes.

    What is worthy of note is that 2010 is the sum of four palindromic primes. I found one such set of primes and each of my two grandsons, Oliver and Sam found different sets. My set had two primes in common with Oliver’s set and two in common with Sam’s set.

    What are my four palindromic primes?

    This puzzle was originally published with no title.

    [teaser2478]

     
    • Jim Randell's avatar

      Jim Randell 10:55 am on 4 July 2025 Permalink | Reply

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

      from enigma import (primes, is_npalindrome, subsets, intersect, printf)
      
      # target value
      T = 2010
      
      # collect palindromic primes
      ps = list(p for p in primes.between(2, T) if is_npalindrome(p))
      
      # find 4-subsets with the required sum
      ts = list(ss for ss in subsets(ps, size=4) if sum(ss) == T)
      
      # choose a set for the setter
      for V in ts:
        # find other sets with 2 values in common with V
        gs = list(ss for ss in ts if ss != V and len(intersect([ss, V])) == 2)
        if len(gs) == 2:
          # output solution
          printf("V = {V} [O,S = {gs}]")
      

      Solution: Victor’s set was: (11, 151, 919, 929).

      And the grandsons sets were:

      (11, 313, 757, 929) [shares: 11, 929]
      (11, 353, 727, 919) [shares: 11, 919]

      And these are the only sets of 4 palindromic primes that sum to 2010.

      Like

    • Ruud's avatar

      Ruud 4:32 pm on 4 July 2025 Permalink | Reply

      import istr
      
      solutions = [set(p) for p in istr.combinations((i for i in istr.range(2010) if i.is_prime() and i == i[::-1]), 4) if sum(p) == 2010]
      print([solution for solution in solutions if sum(len(solution & solution1) == 2 for solution1 in solutions) == 2])
      

      Like

  • Unknown's avatar

    Jim Randell 8:42 am on 1 July 2025 Permalink | Reply
    Tags:   

    Brain teaser 1029: Staff factors 

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

    The Manager of a large company greeted his twelve new computer staff. “You have been given consecutive, five-digit, Staff Reference Numbers (SRN). I remember numbers by finding their prime factors, using mental arithmetic — pre-computer vintage. Why not try it? If you would each tell me what factors you have, without specifying them (and ignoring unity), I should be able to work out your SR numbers”.

    John said: “My number is prime.”

    Ted said ” I have two prime factors. Your number follows mine doesn’t it, Les?”

    Ian said: “I also have two, one of which squared. Alan’s just before me on the list.”

    Sam said: “One of mine is to the power four. The last two digits of my SRN give me the other prime factor.”

    Pete said: “I’ve got one factor to the power four as well. The other one is my year of birth.”

    Brian said: “My number has one prime factor cubed and two others, both squared.”

    Chris said: “I’m the only one with four factors, one of which is squared. Fred’s number is one less than mine.”

    Dave started to say: “Kevin’s SRN is the one after mine, which …” when the Manager interrupted. “I can now list all twelve!”

    List the twelve people, by initials, in increasing order of SRNs. What is Sam’s SRN?

    This was the final puzzle to go by the title “Brain teaser“. The next puzzle was “Brainteaser 1030“.

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

    [teaser1029]

     
    • Jim Randell's avatar

      Jim Randell 8:43 am on 1 July 2025 Permalink | Reply

      Instead of trying all possible 5-digit SRNs we can reduce the number of candidates considered by only considering numbers in the neighbourhood of P, as there are only a few numbers that are the 4th power of a prime multiplied by a viable prime birth year. (I considered primes between 1912 and 1982 as viable birth years).

      This Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library, to solve the puzzle given a candidate start number.

      It runs in 308ms (although I haven’t looked at optimising the evaluation order of the expressions).

      from enigma import (SubstitutedExpression, irange, primes, cache, sprintf as f, join, printf)
      
      # prime factors of n -> ((p1, e1), (p2, e2), ...)
      factors = cache(lambda n: tuple(primes.prime_factor(n)))
      
      # I has exactly 2 prime factors; one is a power of 2
      def checkI(n):
        ((p1, e1), (p2, e2)) = factors(n)
        return 2 in {e1, e2}
      
      # S has exactly 2 prime factors; one is a power of 4, and the other is the last 2 digits of S
      def checkS(n):
        ((p1, e1), (p2, e2)) = factors(n)
        return (4, n % 100) in {(e1, p2), (e2, p1)}
      
      # P has exactly 2 prime factors; one is a power of 4, and the other is his year of birth
      def checkP(n):
        ((p1, e1), (p2, e2)) = factors(n)
        return (e1 == 4 and 1911 < p2 < 1983) or (e2 == 4 and 1911 < p1 < 1983)
      
      # B has exactly 3 prime factors; a power of 3 and two powers of 2
      def checkB(n):
        ((p1, e1), (p2, e2), (p3, e3)) = factors(n)
        return sorted([e1, e2, e3]) == [2, 2, 3]
      
      # C has exactly 4 prime factors; one is a power of 2
      def checkC(n):
        ((p1, e1), (p2, e2), (p3, e3), (p3, e4)) = factors(n)
        return 2 in {e1, e2, e3, e4}
      
      def solve(start, s2d=dict()):
        # expressions to evaluate
        exprs = [
          # J is prime
          "is_prime(J + start)",
          # T has exactly 2 prime factors
          "len(factors(T + start)) == 2",
          # all the others (except C) have < 4 prime factors
          "all(len(factors(x + start)) < 4 for x in [A, D, F, K, L])",
          # adjacent values
          "T + 1 = L",
          "I - 1 = A",
          "C - 1 = F",
          "D + 1 = K",
          # checks on the remaining values
          "checkI({I} + start)",
          "checkS({S} + start)",
          "checkP({P} + start)",
          "checkB({B} + start)",
          "checkC({C} + start)", 
        ]
      
        # environment
        env = dict(
          start=start,
          factors=factors, is_prime=primes.is_prime,
          checkI=checkI, checkS=checkS, checkP=checkP, checkB=checkB, checkC=checkC,
        )
      
        # construct the puzzle
        p = SubstitutedExpression(exprs, base=12, s2d=s2d, env=env)
      
        # solve the puzzle for the offsets
        for s in p.solve(verbose=''):
          # fill out the actual numbers from the offsets
          s = dict((k, v + start) for (k, v) in s.items())
          # output the numbers in order
          fmt = (lambda fs: join((f("{p}" if e == 1 else "{p}^{e}") for (p, e) in fs), sep=", "))
          for k in sorted(s.keys(), key=s.get):
            n = s[k]
            printf("{k} = {n} [{fs}]", fs=fmt(factors(n)))
          printf()
      
      # consider possible prime birth years for P
      for y in primes.between(1912, 1982):
        # consider other prime factor, raised to the 4th power
        for p in primes:
          P = p**4 * y
          if P > 99999: break
          # consider possible start numbers
          for start in irange(max(10000, P - 11), min(99988, P)):
            solve(start, dict(P=P - start))
      

      Solution: The order is: D K A I B S T L P F C J. Sam’s SRN is 31213.

      The SRNs [along with their prime factors] are given below:

      D = 31208 [2^3, 47, 83]
      K = 31209 [3, 101, 103]
      A = 31210 [2, 5, 3121]
      I = 31211 [23^2, 59]
      B = 31212 [2^2, 3^3, 17^2]
      S = 31213 [7^4, 13]
      T = 31214 [2, 15607]
      L = 31215 [3, 5, 2081]
      P = 31216 [2^4, 1951]
      F = 31217 [19, 31, 53]
      C = 31218 [2, 3, 11^2, 43]
      J = 31219 [31219]

      So we see P was born in 1951, which is 31 years before the puzzle was set.


      With a bit of work, we can further reduce the number of cases considered significantly.

      There are only 7 candidate values for P, and there are only 11 candidate values for S. And there is only a single pair where P and S are close enough together to give a solution. This also gives a way to tackle puzzle manually.

      Like

      • Frits's avatar

        Frits 1:44 pm on 1 July 2025 Permalink | Reply

        @Jim, is there a typo in “which is 42 years before the puzzle was set”?

        Like

      • Frits's avatar

        Frits 7:19 pm on 1 July 2025 Permalink | Reply

        from enigma import is_prime, prime_factor as pfactor, ceil, subsets
        
        # possible SRN's for people in list <s>
        SRNs = lambda s: {i for i in range(s[0] - 11, s[0] + 12) 
                          if i not in s and all(abs(x - i) < 12 for x in s)}
                          
        # mandatory SRN's for people in list <s>
        m_SRNs = lambda s: {i for i in range(min(s) + 1, max(s)) if i not in s}                  
        
        mn = 1912 # minimum birth date
        
        # set of prime numbers up to 99
        P = {3, 5, 7}
        P |= {2} | {x for x in range(11, 100, 2) if all(x % p for p in P)}
        sP = sorted(P)
        
        # check last item in sequence <s>
        def check(s, ln, vs, fs4={}):
          if fs4:
            # mandatory missing numbers plus last item  may not have 4 factors
            if not (m_SRNs(s) | {s[-1]}).isdisjoint(fs4): return False
          
          # check length and exponents
          if len(pfs := list(pfactor(s[-1]))) != ln or {y for x, y in pfs} != vs:
            return False
          return True  
        
        # start with P ([p1^4, 19xx])
        for p1 in sP:
          if p1**4 * mn > 99999: break
          ns = [n for y in range(mn, 1965) if 9999 < (n := p1**4 * y) <= 99999]
          mn, mx = ns[0] - 11 , ns[-1] + 11
          # check S ([s1^4, s2 with s2 last 2 digits])
          # S = xxxxab and S = ab.s1^4,  100 * xxxx = ab.(s1^4 - 1) so s1^4 = ...1
          for s1 in sP[1:]:
            if (pow4 := s1**4) * 11 > 99999: break
            if pow4 % 10 != 1 or mx / pow4 > 99: continue
            # calculate possible numbers for S
            for s in [s for ab in range(ceil(mn / pow4), mx // pow4 + 1) 
                      if ab in P and (s := ab * pow4) % 100 == ab]:
              # only C may have exactly 4 factors
              fs4 = {x for x in SRNs([s]) if len(list(pfactor(x))) == 4}
              for j in [j for j in SRNs([s]) if is_prime(j)]:
                for c in SRNs([s, j]):
                  if not check([s, j, c], 4, {1, 2}): continue
                  for b in SRNs([s, j, c]):
                    if not check([s, j, c, b], 3, {2, 3}, fs4): continue
                    for i in SRNs([s, j, c, b]):
                      if not check([s, j, c, b, i], 2, {1, 2}, fs4): continue
                      for t in SRNs([s, j, c, b, i]):
                        if not check([s, j, c, b, i, t], 2, {1}, fs4): continue
                        for p in SRNs([s, j, c, b, i, t]):
                          if not check([s, j, c, b, i, t, p], 2, {1, 4}, fs4): continue
                          # before I is A, after T is L, before C is F
                          a, l, f = i - 1, t + 1, c - 1
                          n10 = sorted([s, j, c, b, i, t, p, a, l, f])
                          if len(set(n10)) != 10: continue
                          # K after D, find places to insert (D, K)
                          for d, k in subsets(SRNs(n10), 2):
                            if k != d + 1: continue
                            if not {d, k}.isdisjoint(fs4): continue
                            # 12 consecutive numbers
                            if ((n12 := sorted(n10 + [d, k])) == 
                                 list(range(n12[0], n12[0] + 12))):
                              # final check   
                              if not (set(n12) - {c}).isdisjoint(fs4): continue
                              map = dict(list(zip([s, j, c, b, i, t, p, a, l, f, d, k],
                                            "SJCBITPALFDK")))
                              for k, v in sorted(map.items()):
                                print(f"{v}: {k} = "
                                      f"{' * '.join(['^'.join(str(y) for y in x)
                                                     for x in pfactor(k)])}")
        

        Like

  • Unknown's avatar

    Jim Randell 7:22 am on 29 June 2025 Permalink | Reply
    Tags:   

    Teaser 3275: Against the odds 

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

    I have taken a number of playing cards (more red than black) from a standard pack of 52 cards and redistributed them in a certain way in two piles, each containing both red and black cards.

    A card is taken at random from the smaller pile and placed in the larger pile. The larger pile is shuffled; a card is taken at random from it and placed in the smaller pile.

    There is a one in eight chance that the number of red and black cards in each pile will be the same as they were originally.

    How many black cards in total were used?

    [teaser3275]

     
    • Jim Randell's avatar

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

      (See also: Teaser 2571).

      If we have two piles:

      pile 1, with b1 black cards, and r1 red cards; and:
      pile 2, with b2 black cards, and r2 red cards.

      where pile 1 (of size n1) is smaller than pile 2 (of size n2).

      Then the probability of the same colour card being chosen for both transfers is the sum of the following probabilities:

      P(both transfers black) = (b1 / n1) × ((b2 + 1) / (n2 + 1))
      P(both transfers red) = (r1 / n1) × ((r2 + 1) / (n2 + 1))

      P(both transfers same colour) = (b1 × (b2 + 1) + r1 × (r2 + 1)) / (n1 × (n2 + 1))

      The following Python program constructs all possible arrangements of cards and calculates the probability of the same coloured card moving in both directions.

      We can find the inverse of the combined probability, and this allows us to perform all the calculations using integers.

      It runs in 84ms. (Internal runtime is 22ms).

      from enigma import (irange, subsets, Decompose, cproduct, div, printf)
      
      # choose number of red and black cards (B < R)
      for (B, R) in subsets(irange(2, 26), size=2, select='C'):
      
        # split into 2 piles (pile1 < pile2)
        decompose = Decompose(2, increasing=0, sep=0)
        for ((b1, b2), (r1, r2)) in cproduct([decompose(B), decompose(R)]):
          (n1, n2) = (b1 + r1, b2 + r2)
          if not (n1 < n2): continue
      
          # calculate probability of the same coloured card in both transfers
          # r = 1/p
          r = div(n1 * (n2 + 1), b1 * (b2 + 1) + r1 * (r2 + 1))
      
          # look for solution
          if r == 8:
            printf("B={B} R={R}")
            printf("  b1={b1} r1={r1} -> {n1}; b2={b2} r2={r2} -> {n2}")
            printf("    p=1/{r}")
            printf()
      

      Solution: There were 21 black cards.

      And 23 red cards.

      The cards are distributed into piles as follows:

      pile 1 = 20 black + 1 red (21 cards)
      pile 2 = 1 black + 22 red (23 cards)

      The probability of a black card moving in both directions is:

      pB = 20/21 × 2/24 = 40/504

      And the probability of a red card moving in both directions is:

      pR = 1/21 × 23/24 = 23/504

      And we have:

      pB + pR = 63/504 = 1/8

      Like

      • Jim Randell's avatar

        Jim Randell 9:48 am on 30 June 2025 Permalink | Reply

        Or we can use the [[ SubstitutedExpression ]] solver from the enigma.py library, for a more declarative solution.

        from enigma import (SubstitutedExpression, irange, printf)
        
        # assign symbols
        macro = dict(b1='W', r1='X', b2='Y', r2='Z')
        
        # expressions to solve
        exprs = [
          # more reds than blacks; no more than 26 cards per colour
          "@b1 + @b2 < @r1 + @r2 < 27",
          # pile 1 is smaller than pile 2
          "@b1 + @r1 < @b2 + @r2",
          # probability P = 1/8; P = Pn/Pd => Pd = 8 Pn
          "(@b1 + @r1) * (@b2 + @r2 + 1) == 8 * (@b1 * (@b2 + 1) + @r1 * (@r2 + 1))",
        ]
        
        # construct the solver
        p = SubstitutedExpression(exprs,
              base=26, digits=irange(1, 25), distinct=[], macro=macro,
              answer="(@b1, @r1, @b2, @r2)",
            )
        
        # solve the puzzle
        for (b1, r1, b2, r2) in p.answers(verbose=0):
          (B, R) = (b1 + b2, r1 + r2)
          printf("B={B} R={R} [b1={b1} r1={r1}; b2={b2} r2={r2}]")
        

        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