Updates from September, 2025 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:44 pm on 4 September 2025 Permalink | Reply
    Tags:   

    Teaser 2332: Schoolwork 

    From The Sunday Times, 3rd June 2007 [link]

    Five girls were each given £4 to spend on rubbers, pencils and pens (not necessarily all three). Rubbers cost 20p each, pencils 30p, pens 40p. They each bought 12 items, spending the entire £4.

    Sue bought the same number of pencils as Tina did rubbers. Tina had as many pens as Ursula had pencils. Viola had as many rubbers as Wendy had pens. And just one girl had the same number of pens as Tina had pencils.

    How many of each item did Ursula buy?

    [teaser2332]

     
    • Jim Randell's avatar

      Jim Randell 4:45 pm on 4 September 2025 Permalink | Reply

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

      It runs in 129ms. (Internal runtime of the generated program is 1.5ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #    rb  pl  pn
      # S:  A   B   C
      # T:  D   E   F
      # U:  G   H   I
      # V:  J   K   L
      # W:  M   N   P
      #
      --distinct=""
      --base=13  # allow quantities 0-12
      
      # each girl spent 400p and bought 12 items
      --code="girl = lambda rb, pl, pn: (rb + pl + pn == 12) and (2 * rb + 3 * pl + 4 * pn == 40)"
      "girl(A, B, C)"
      "girl(D, E, F)"
      "girl(G, H, I)"
      "girl(J, K, L)"
      "girl(M, N, P)"
      
      # equivalences
      "B = D"  # S.pl = T.rb
      "F = H"  # T.pn = U.pl
      "J = P"  # V.rb = W.pn
      
      # "just one girl bought the same number of pens as T did pencils"
      "[C, F, I, L, P].count(E) == 1"
      
      --template="(A B C) (D E F) (G H I) (J K L) (M N P)"
      --solution=""
      
      --answer="(G, H, I)"
      

      Solution: Ursula bought: 1 rubber; 6 pencils; 5 pens.

      The full solution is:

      S: 3 rubbers; 2 pencils; 7 pens
      T: 2 rubbers; 4 pencils; 6 pens
      U: 1 rubber; 6 pencils; 5 pens
      V: 4 rubbers; 0 pencils; 8 pens
      W: 0 rubbers; 8 pencils; 4 pens

      Like

      • Jim Randell's avatar

        Jim Randell 11:24 am on 5 September 2025 Permalink | Reply

        Here is a solution using the [[ choose() ]] function from the enigma.py library.

        It runs in 70ms (with an internal runtime of 387µs).

        from enigma import (Record, decompose, choose, icount_exactly as count, nl, printf)
        
        # look for combinations of 12 items with a cost of 400p
        vs = list()
        for (rb, pl, pn) in decompose(12, 3, increasing=0, sep=0, min_v=0):
          if 2 * rb + 3 * pl + 4 * pn == 40:
            vs.append(Record(rb=rb, pl=pl, pn=pn))
        
        # selection functions (in order) for each girl
        fns = [
          None,  # S
          (lambda S, T: S.pl == T.rb),  # T
          (lambda S, T, U: T.pn == U.pl),  # U
          None,  # V
          (lambda S, T, U, V, W: V.rb == W.pn),  # W
        ]
        
        # find candidate solutions
        for (S, T, U, V, W) in choose(vs, fns):
          # "just one girl bought the same number of pens as T did pencils"
          if not count((S, T, U, V, W), (lambda x, t=T.pl: x.pn == t), 1): continue
          # output solution
          printf("S: {S}{s}T: {T}{s}U: {U}{s}V: {V}{s}W: {W}", s=nl)
          printf()
        

        Like

    • Frits's avatar

      Frits 6:36 pm on 4 September 2025 Permalink | Reply

      from itertools import product
      
      N, T = 12, 400
      pa, pb, pc = 20, 30, 40
      
      # each girl spent 400p and bought 12 items and
      # bought <a> rubbers, <b> pencils and <c> pens
       
      # quantity options for the girls    
      sols = [(a, b, c) for a in range(min(N, T // pa) + 1)
              for b in range(min(N - a, (T - a * pa) // pb) + 1)
              if T - pa * a - pb * b == pc * (c := N - a - b)]   
      
      # determine quantities for all girls
      for S, T, U, V, W in product(sols, repeat=5):
        # Sue bought the same number of pencils as Tina did rubbers
        if S[1] != T[0]: continue
        # Tina had as many pens as Ursula had pencils
        if T[2] != U[1]: continue
        # Viola had as many rubbers as Wendy had pens
        if V[0] != W[2]: continue
        # just one girl bought the same number of pens as T did pencils
        if [x[2] for x in (S, T, U, V, W)].count(T[1]) != 1: continue
        print("answer:", U)
      

      Like

    • Ruud's avatar

      Ruud 7:43 am on 5 September 2025 Permalink | Reply

      import types
      
      combinations = [
          types.SimpleNamespace(rubbers=rubbers, pencils=pencils, pens=pens)
          for rubbers in range(20)
          for pencils in range(14)
          for pens in range(11)
          if pens * 40 + pencils * 30 + rubbers * 20 == 400 and pens + pencils + rubbers == 12
      ]
      
      for s in combinations:
          for t in combinations:
              if s.pencils == t.rubbers:
                  for u in combinations:
                      if t.pens == u.pencils:
                          for v in combinations:
                              for w in combinations:
                                  if v.rubbers == w.pens:
                                      if sum(girl.pens == t.pencils for girl in (s, t, u, v, w)) == 1:
                                          for name in "Sue Tina Ursula Viola Wendy".split():
                                              print(f"{name:7} {locals()[name[0].lower()]}")
      

      Like

  • Unknown's avatar

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

    Teaser 2488: [Birthdays] 

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

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

    How old are the five children?

    This puzzle was originally published with no title.

    [teaser2488]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

      Summing the five ages and reversing gives:

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

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

      With the 4 ages B – E:

      8 + 3 + 7 + 5 = 23 → 32

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

      With the 3 ages C – E:

      3 + 7 + 5 = 15 → 51

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

      With the 2 ages D – E:

      7 + 5 = 12 → 21

      And 21 is divisible by 7, but not 5.

      Like

    • Frits's avatar

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

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

      Like

    • GeoffR's avatar

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

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

      Like

    • Ruud's avatar

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

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

      Like

    • Ruud's avatar

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

      As a one-liner:

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

      Like

  • Unknown's avatar

    Jim Randell 6:46 am on 31 August 2025 Permalink | Reply
    Tags:   

    Teaser 3284: Cricket square 

    From The Sunday Times, 31st August 2025 [link] [link]

    We recently played a thrilling cricket match against the league leaders. Each team had two innings alternately (our opponents going first) and the combined total of runs scored in each innings determined the winner. Curiously both our opponents’ scores were 3-digit squares, the six digits all being different. The total of those two scores was also a square.

    After our first innings (in which we scored a 3-digit prime number of runs), we had a lead, but after their second innings we trailed by a 3-digit prime number (less than 250). The six digits in these two primes were all different. Our second innings score was also a 3-digit prime number but we lost the match by a prime number of runs.

    How many runs did we score in our second innings?

    [teaser3284]

     
    • Jim Randell's avatar

      Jim Randell 7:04 am on 31 August 2025 Permalink | Reply

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

      This run file executes in 154ms. (Internal runtime of the generated code is 4.7ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # opponents scores in each innings are 3-digit square numbers
      "is_square(ABC)"  # = first innings
      "is_square(DEF)"  # = second innings
      # and their total number of runs is also a square number
      "is_square(ABC + DEF)"  # = opponents total
      
      # setters first and second innings score are 3-digit prime numbers
      "is_prime(UVW)"  # = first innings
      "is_prime(XYZ)"  # = second innings
      
      # setters team took the lead after their first innings
      "UVW > ABC"
      
      # after the opponents second innings the setters team trailed
      # by a 3-digit prime number of runs (= RST; < 250)
      "ABC + DEF - UVW = RST"
      "is_prime(RST)"
      "RST < 250"
      
      # but the setters team lost the match by a prime number of runs
      "is_prime(ABC + DEF - UVW - XYZ)"
      
      # distinct digits
      --distinct="ABCDEF,RSTUVW"
      
      # required answer is XYZ
      --answer="XYZ"
      
      # [optional] neaten up output
      --template="ABC UVW DEF XYZ" # scores in each innings
      --solution=""
      

      Solution: In the second innings, the setters team scored 239 runs.

      The opponents scores were 324 and 576 (both square numbers, and the sum is 900, also a square number), but we don’t know which order they were scored in.

      The setters team scores 659 (a prime number) in their first innings (and so were in lead lead after their first innings, but behind by 241 (a prime number) after the opponents second innings.

      In their second innings, the setters team scored 239 (a prime number), which gives them a total of 898, so they lost the game by 2 runs (a prime number).

      Like

      • Ruud's avatar

        Ruud 2:51 pm on 31 August 2025 Permalink | Reply

        import peek
        import istr
        
        for they1, they2 in istr.product((x for x in istr.range(100, 1000) if x.is_square()), repeat=2):
            if (they1 + they2).is_square() and (they1 | they2).all_distinct():
                for us1, us2 in istr.product((x for x in istr.range(100, 1000) if x.is_prime()), repeat=2):
                    if us1 > they1:
                        score3 = they1 + they2 - us1
                        if score3.is_prime() and len(score3) == 3 and score3 < 250:
                            if (they1 + they2 - us1 - us2).is_prime():
                                if (score3 | us1).all_distinct():
                                    peek(they1, they2, us1, us2)
        

        Like

    • GeoffR's avatar

      GeoffR 8:48 am on 2 September 2025 Permalink | Reply

      As an exercise, this posting is formatted to PEP-8 or the Python Enhancement Proposal. It does seem to make code more organised and readable. I used Jim’s names for variables.

      from enigma import is_prime
      
      # three digit primes 
      primes = [n for n in range(101, 1000, 2) if is_prime(n)]
      # three digit squares
      sq3 = [x ** 2 for x in range(10, 32)]
      # squares up to 2000
      sq34 = [x ** 2 for x in range(10, 44)]
      
      # opponents first score
      for ABC in sq3:
        A, B, C = (int(x) for x in str(ABC))
        if len(set(str((ABC)))) == 3:
          
          # our first score
          for UVW in primes:
            if UVW <= ABC:
              continue
            U, V, W = (int(x) for x in str(UVW))
            
            # opponents second score
            for DEF in sq3:
              if DEF == ABC or ABC + DEF not in sq34:
                continue
              
              # ABC and DEF combined have six different digits
              D, E, F = (int(x) for x in str(DEF))
              if len(set((A, B, C, D, E, F))) != 6:
                continue
              
              # after second score
              if not (100 < (RST := ABC + DEF - UVW) < 250 and is_prime(RST)):
                continue
             
              # UVW and RST combined have six different digits
              R, S, T = (int(x) for x in str(RST))
              if len(set((U, V, W, R, S, T ))) != 6:
                continue
      
              # our second score
              for XYZ in primes:
                # we lost the match by a prime number of runs
                if XYZ != UVW and is_prime(ABC + DEF - UVW - XYZ):
                  print(f"Home team scores(1st and 2nd): {UVW} and {XYZ}.")
                  print(f"Visiting team scores(1st and 2nd): {ABC} and {DEF}.")
                  print()
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:23 pm on 28 August 2025 Permalink | Reply
    Tags: ,   

    Teaser 2486: [Meet at the station] 

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

    Pat drove to the station at his regular speed to meet his brothers, John and Joe, whose train was due at 6pm. But John had caught an earlier train and, arriving half an hour early, decided to walk home at a steady pace. Pat waved as he passed John (a whole number of minutes before 6pm), but drove on to the station. Pat collected Joe on time and, a whole number of minutes later, they set off for home. Pat and Joe overtook John 13 minutes after Pat had waved to him.

    At what time did Pat and Joe overtake John?

    This puzzle was originally published with no title.

    [teaser2486]

     
    • Jim Randell's avatar

      Jim Randell 4:23 pm on 28 August 2025 Permalink | Reply

      Suppose John walks at a speed of 1 unit per minute.

      At a minutes before 6pm Pat passes John. At this time John has walked a distance of (30 − a) units from the station. And in 13 minutes time John will have walked an additional 13 distance units, so will be a distance (43 − a) units from the station.

      After Pat passes John he travels the remaining distance (30 − a) to the station, in a time of a minutes (to arrive at 6pm).

      So his velocity is: (30 − a)/a.

      If the Pat and John leave the station at b minutes after 6pm, then the time taken to catch up with John is:

      c = (43 − a) / (30 − a)/a

      and:

      a + b + c = 13

      (where a, b, c are all positive integer values).

      So we can consider possible integer values for a and calculate the corresponding values for c and b.

      This can be done manually or programatically.

      This Python program runs in 75ms. (Internal runtime is 33µs).

      from enigma import (irange, div, printf)
      
      for a in irange(1, 5):
        c = div((43 - a) * a, (30 - a))
        if c is None: continue
        b = 13 - (a + c)
        if b > 0:
          printf("a={a} -> c={c} b={b}")
      

      Solution: Pat and Joe caught up with John at 6:09 pm.

      Pat initially passed John at 5:56 pm, before arriving at the station 4 minutes later (at 6 pm) to pick up Joe.

      They left the station at 6:03 pm and 6 minutes later (at 6:09 pm) passed John again.

      Like

  • Unknown's avatar

    Jim Randell 8:28 am on 26 August 2025 Permalink | Reply
    Tags:   

    Teaser 2476: [Parking spaces] 

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

    Ann, Beth, Carol, Dave and Eric rent flats on each of the five floors of a block. Each has a parking space in which are their five makes of car, in five colours.

    Ann does not live in the basement, whose tenant owns the MG. Ann’s car is not the silver Audi or the blue car, which belongs to the second-floor tenant. The Ford (not black) is the first-floor tenant’s. The ground-floor tenant’s car is white and Eric’s is not black. One of the girls owns the Vauxhall, and Dave (who lives one floor below Carol) owns the Toyota.

    Whose cars are red, white and blue respectively?

    This puzzle was originally published with no title.

    [teaser2476]

     
    • Jim Randell's avatar

      Jim Randell 8:29 am on 26 August 2025 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to solve the puzzle.

      We assign numeric values from 0 to 4 to the floors (this makes it easier to code the condition that D is one floor below C), and then assign each of the floor values to the remaining groups of attributes: name, colour, make.

      The program runs in 71ms. (Internal runtime is 8.4ms).

      from enigma import (SubstitutedExpression, sprintf, printf)
      
      # we assign floor values (0 .. 4 = basement .. 3rd) to each of the groups:
      #   name (A, B, C, D, E)
      #   make
      #   colour
      
      # floors (basement, ground, 1st, 2nd, 3rd)
      floor = (FB, FG, F1, F2, F3) = (0, 1, 2, 3, 4)
      
      # symbols and labels for the groups
      name = (A, B, C, D, E) = "ABCDE"
      make = (MM, MA, MF, MV, MT) = "KLMNP"  # MG, Audi, Ford, Vauxhall, Toyota
      colour = (CS, CB, CK, CW, CR) = "QRSTU"  # silver, blue, black, white, red
      
      # construct the expressions
      exprs = list(sprintf(x) for x in [
        # "A does not live in the basement"
        "{FB} != {A}",
        # "basement owns the MG"
        "{FB} == {MM}",
        # "A's car is not the silver Audi" => "the Audi is silver"
        "{MA} == {CS}", "{MA} != {A}", "{CS} != {A}",
        # "A's car is not blue"
        "{CB} != {A}",
        # "the blue car belongs to the second floor"
        "{CB} == {F2}",
        # "the Ford is not black" (!)
        "{MF} != {CK}",
        # "the Ford belongs to the first floor"
        "{MF} == {F1}",
        # "the ground floor car is white"
        "{FG} == {CW}",
        # "E's car is not black"
        "{CK} != {E}",
        # "the Vauxhall belongs to A, B, or C"
        "{MV} in {{ {A}, {B}, {C} }}",
        # "D lives one floor below C"
        "{D} + 1 == {C}",
        # "D owns the Toyota"
        "{MT} == {D}",
      ])
      
      # construct the solver
      p = SubstitutedExpression(exprs, base=5, distinct=[name, make, colour])
      
      # labels for output
      floors = ["basement", "ground floor", "1st floor", "2nd floor", "3rd floor"]
      names = ["Anne", "Beth", "Carol", "Dave", "Eric"]
      colours = ["silver", "blue", "black", "white", "red"]
      makes = ["MG", "Audi", "Ford", "Vauxhall", "Toyota"]
      output = lambda f, n, c, m: printf("{f}: {n}, {c} {m}")
      
      # solve the puzzle
      for s in p.solve(verbose=0):
        # collect attributes by floor
        ss = list([f] for f in floors)
        for (xs, ts) in [(name, names), (colour, colours), (make, makes)]:
          for (x, t) in zip(xs, ts):
            ss[s[x]].append(t)
        # output solution
        for vs in ss: output(*vs)
        printf()
      

      Solution: Eric’s car is red; Anne’s car is white; Dave’s car is blue.

      The complete description is:

      basement: Beth, black, MG
      ground floor: Anne, white, Vauxhall
      1st floor: Eric, red, Ford
      2nd floor: Dave, blue, Toyota
      3rd floor: Carol, silver, Audi

      Like

    • Frits's avatar

      Frits 7:43 pm on 27 August 2025 Permalink | Reply

      Reusing code from Tantalizer 66.

      deepcopy = lambda s: [x.copy() for x in s]
      n_elements = lambda s: sum(len(y) for x in s for y in x)
      
      # update values in positions <ps> for column <col>
      def update(ps, vals):
        col = colno[vals[0]] # column for the values in <vals>
        for i in range(5):
          if i in ps:
            lst[i][col] = vals
          else:
            # remove values from other positions
            remove(i, [x for x in lst[i][col] if x in vals]) 
      
      # remove values <vals> from lst[pos][col]      
      def remove(pos, vals):
        if not vals: return
        col = colno[vals[0]] # column for the values in <vals>
        ln = len(lst[pos][col])
        lst[pos][col] = [x for x in lst[pos][col] if x not in vals]  
        if not lst[pos][col]:
          error = True   # no value remained
        if ln > 1 and len(lst[pos][col]) == 1:
          update([pos], lst[pos][col])
      
      # propagate uniqueness of value <v>
      def unique(col, v):
        # check for uniqueness
        if len(s := [i for i in range(5) if v in lst[i][col]]) == 1:
          update(s, [v])     
      
      # values <v1> and <v2> belong together
      def linked(v1, v2):
        c1 = colno[v1] 
        c2 = colno[v2] 
        # if v1 and v2 don't both exist for a position remove all of them
        for i in range(5):
          if not(v1 in lst[i][c1] and v2 in lst[i][c2]):
            remove(i, [v1])
            remove(i, [v2])
      
        unique(c1, v1)
        unique(c2, v2)  
      
      # values <v1> and <v2> don't belong together
      def not_linked(v1, v2):
        c1 = colno[v1] 
        c2 = colno[v2] 
        # remove other value if a position has a set value for <v1> or <v2> 
        for i in range(5):
          if lst[i][c1] == [v1]:
            remove(i, [v2])
          if lst[i][c2] == [v2]:
            remove(i, [v1])
      
      # try all options for person <p> and list <s>
      def solve(k, p, s, ss=[]):
        global lst
        if (n := n_elements(lst)) == 15:
          yield lst
        elif k == 0:
          # we have to try options for another person
          p = ss[1]
          s = deepcopy(lst[p])
          yield from solve(3, p, s, ss[1:])
        else:
          if len(s[k - 1]) == 1:  # one value
            yield from solve(k - 1, p, s, ss)
          else:
            save = deepcopy(lst)
            # try all values
            for v in s[k - 1]:
              update([p], [v])
              # check/update relations until <lst> is no longer updated
              n = check()   
              if not error:
                yield from solve(k - 1, p, s, ss)
              # back track all updates
              lst = deepcopy(save)          
      
      # check/update relations until <lst> is no longer updated
      def check():
        global error
        error = False
        n, prev = 75, 76
        
        while n < prev and not error:
          linked("basement", "MG")        # "basement owns the MG"
          linked("Audi", "silver")        # "A's car is not the silver Audi"
          linked("blue", "second-floor")  # "the blue car belongs to the second floor"
          not_linked("Ford", "black")     # "the Ford is not black"
          linked("Ford", "first-floor")   # "the Ford belongs to the first floor"
          linked("ground-floor", "white") # "the ground floor car is white"
          # "D lives one floor below C"
          for fl in lst[D][0]:
            if floor[floor.index(fl) + 1] not in lst[C][0]:
              remove(D, [fl])
          for fl in lst[C][0]:
            if floor[floor.index(fl) - 1] not in lst[D][0]:
              remove(C, [fl])    
      
          # calculate number of elements in flattened list
          prev, n = n, n_elements(lst)
          if n < 15:
            error = True
        return n
      
      floor  = "basement ground-floor first-floor second-floor third-floor".split()
      make   = "MG Audi Ford Vauxhall Toyota".split()
      colour = "silver blue black white red".split()
      names  = "Anne Beth Carol Dave Eric".split()
      A, B, C, D, E = range(5)
      
      # dictionary column number per item
      colno = {x: 0 if x in floor else 1 if x in colour else 2 
                 for x in floor + make + colour}
      
      # make an initial list per person with all possibilities
      lst = [[floor, colour, make] for _ in range(5)]
      error = False
      
      # initial constraints
      remove(A, ["basement"])         # "A does not live in the basement"
      remove(A, ["Audi"])             # "A's car is not the silver Audi"
      remove(A, ["silver", "blue"])   # "A's car is not blue"
      remove(E, ["black"])            # "E's car is not black"
      # "the Vauxhall belongs to A, B, or C"
      remove(D, ["Vauxhall"])
      remove(E, ["Vauxhall"])
      # "D lives one floor below C"
      remove(D, ["third-floor"])
      remove(C, ["basement"])
      update([D], ["Toyota"])         # "D owns the Toyota"
      
      # check/update relations until <lst> is no longer updated
      n = check()    
      if error: 
        print("no solution")
      elif n == 15:
        for name, vals in zip(names, lst):
          print(f"{name}:", ", ".join(x[0] for x in vals))
      elif n > 15:
        # start with person for which the most is known
        ns = sorted([(sum(len(y) for y in x), i) for i, x in enumerate(lst)])
        ns = [i for x, i in ns if x > 3]
        # try all options 
        for s in solve(3, ns[0], deepcopy(lst[ns[0]]), ns):
          for name, vals in zip(names, lst):
            print(f"{name}:", ", ".join(x[0] for x in vals))
          print("answer:", [[name for name, vals in zip(names, lst) 
                if col in vals[1]][0] for col in ["red", "white", "blue"]])
      

      Like

  • Unknown's avatar

    Jim Randell 6:11 am on 24 August 2025 Permalink | Reply
    Tags:   

    Teaser 3283: Die hard? 

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

    I have three identical standard dice (1 to 6 spots on each face, with 1 opposite 6, 2 opposite 5 and 3 opposite 4). I have placed them together in a row on a table and looked at the row from various sides. Regarding each face of a die as a digit (so that, for example, five spots would be regarded as a 5), I can read three 3-digit primes; one along the front of the row, one (the largest of the three) along the top of the row, and one along the back viewed from behind. Furthermore, the total number of  spots on the 11 visible faces is also a prime.

    What, in increasing order, are the three 3-digit primes?

    [teaser3283]

     
    • Jim Randell's avatar

      Jim Randell 6:26 am on 24 August 2025 Permalink | Reply

      See also: Teaser 2598.

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

      It assumes the dice are “right-handed”, but you get the same answer if you use “left-handed” dice.

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

      Run: [ @codepad ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # layout (viewed from above):
      #
      #       I H G
      #     +-------+
      #   X | A B C | Y
      #     +-------+
      #       D E F
      
      --digits="1-6"
      --distinct="ADIX,BEH,CFGY"
      
      # check the top
      "is_prime(ABC)"
      "ABC > max(DEF, GHI)"
      
      # the front and back
      "D + I = 7" "E + H = 7" "F + G = 7"
      "is_prime(DEF)"
      "is_prime(GHI)"
      
      # total number of exposed spots is also prime
      "is_prime(sum([A, B, C, D, E, F, G, H, I, X, Y]))"
      
      # generate die corners (clockwise around a corner)
      --code="""
      def die_corners(x, y, z, k=7):
        for (y1, z1) in tuples([y, z, k - y, k - z, y], 2):
          for t in [(x, y1, z1), (k - x, k - z1, k - y1)]:
            yield from repeat(rotate, t, 2)
      """
      --code="corners = set(die_corners(3, 2, 1))"  # (3, 2, 1) = RH; (1, 2, 3) ]] = LH
      
      # check corner configurations
      "(A, D, X) in corners"
      "(A, X, I) in corners"
      "(C, Y, F) in corners"
      "(C, G, Y) in corners"
      
      --answer="ordered(ABC, DEF, GHI)"
      --template="(A B C) (D E F) (G H I) (X Y)"
      --solution=""
      

      Solution: The 3-digit primes are: 433, 443, 661.

      Here is a layout using right-handed dice:

      The sum of the spots on the exposed faces is 41.

      The die in the middle can be rotated through 180° to give another layout with 433 on the front and 443 on the back, but this doesn’t change the answer to the puzzle.

      With left-handed dice the layout is the same, but with the 5 on the left end and the 2 on the right end.

      Like

      • Jim Randell's avatar

        Jim Randell 9:25 am on 24 August 2025 Permalink | Reply

        Here is a Python program that finds the same layout(s).

        It has an internal runtime of 499µs.

        from enigma import (primes, nsplit, tuples, repeat, rotate, find, seq_items, chain, nconcat, printf)
        
        # generate die corners (clockwise around a corner) from example (x, y, z)
        def die_corners(x, y, z, k=7):
          for (y1, z1) in tuples([y, z, k - y, k - z, y], 2):
            for t in [(x, y1, z1), (k - x, k - z1, k - y1)]:
              yield from repeat(rotate, t, 2)
        
        # make map (x, y) -> z for (x, y, z) clockwise values round a corner
        # [use: (3, 2, 1) for RH dice; (1, 2, 3) for LH dice]
        corner = dict(((x, y), z) for (x, y, z) in die_corners(3, 2, 1))
        
        # look for 3-digit primes that can be formed using the digits 1-6
        digits = { 1, 2, 3, 4, 5, 6 }
        prs = list(ds for ds in map(nsplit, primes.between(111, 666)) if digits.issuperset(ds))
        
        # choose a prime for the front
        for (i, front) in enumerate(prs):
          # determine the number on the back
          back = tuple(7 - x for x in reversed(front))
          j = find(prs, back)
          if j == -1: continue
          # choose a larger number for the top
          for (_, top) in seq_items(prs, max(i, j) + 1):
        
            # check for valid dice
            xs = tuple(corner.get((t, f)) for (t, f) in zip(top, front))
            if None in xs: continue
        
            # left and right exposed faces
            (x, y) = (xs[0], corner.get((front[-1], top[-1])))
            # sum of exposed faces is a prime
            if not (sum(chain(top, front, back, (x, y))) in primes): continue
        
            # output solution
            ns = sorted(map(nconcat, (top, front, back)))
            printf("{ns} [top={top} front={front} back={back}; left={x} right={y}]")
        

        Like

    • Frits's avatar

      Frits 9:37 am on 24 August 2025 Permalink | Reply

      #       I H G
      #     +-------+
      #   X | A B C | Y
      #     +-------+
      #       D E F
       
      # given two dice faces anti-clockwise at a vertex, find the third
      # face at this vertex (using a western die if 'same' is True)
      def third_face(fi, se, same=True):
        f, s = int(fi), int(se)
        if s in {f, 7 - f}:
          raise ValueError
        oth = [i for i in range(1, 7) if i not in {f, s, 7 - f, 7 - s}]
        return oth[((f < s) == ((s - f) % 2)) == (same == (s + f > 7))]
       
      dgts = "123456"
      # opposite side dictionary
      opp = {str(i): str(7 - i) for i in range(1, 7)}  
          
      # determine valid 3-digit primes
      prms1 = {3, 5, 7}
      prms1 |= {x for x in range(11, 52, 2) if all(x % p for p in prms1)}
      prms = {str(i) for i in range(101, 667, 2) if all(i % p for p in prms1) and
              all(x in dgts for x in str(i))}
       
      # front and back: DEF and IHG
      fb = [(DEF, GHI[::-1]) for DEF in prms if DEF[0] not in "16" and
            (GHI := (''.join(opp[x] for x in DEF))[::-1]) in prms]
      
      # combine front DEF and back IHG with top ABC
      ftb = [(f, t, b) for t in prms for f, b in fb if t > f and t > b[::-1]
             and all(y not in {x, z} for x, y, z in zip(f, t, b))]       
      
      # determine side faces X and Y and check for prime total of 11 spots
      ftbxy = [(f, t, b, x, y) for (f, t, b) in ftb if 
               (x := third_face(f[0], t[0])) + (y := third_face(t[2], f[2])) +
               sum(sum(int(b) for b in a) for a in (f, b, t)) in prms1]
      
      sols = set(tuple(sorted([f, t, b[::-1]])) for f, t, b, _, _ in ftbxy)
      print("answer:", ' or '.join(str(s) for s in sols))
      

      Like

  • Unknown's avatar

    Jim Randell 7:32 am on 22 August 2025 Permalink | Reply
    Tags:   

    Teaser 2485: [Circular field] 

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

    Jack and Katy have inherited a circular field, with North Gate (N) at the northernmost point and East Gate (E), South Gate (S) and West Gate (W) at the appropriate points.

    Jack’s share of the field is one hectare in area. For each point P in his share, [exactly] three of the angles NPE, EPS, SPW and WPN are acute. For each point in Katy’s share, however, fewer than three of those angles are acute.

    How far is it between North Gate and East Gate?

    This puzzle was originally published with no title.

    [teaser2485]

     
    • Jim Randell's avatar

      Jim Randell 7:32 am on 22 August 2025 Permalink | Reply

      If we draw a circle and consider the angle subtended by the diameter with any point P, then:

      If P is on the circumference of the circle, then the angle is 90°.

      If P is inside the circle, then the angle is >90°.

      If P is outside the circle, then the angle is <90° (i.e. actute).

      So we can draw circles with diameters of NE, ES, SW, WN, and then Jack’s region is any point that is outside exactly 3 of the circles, and Katy’s region is any point that is outside fewer than 3 of the circles.

      The situation is this:

      Jack’s area (blue) consists of points that are outside exactly 3 of the circles, and Katy’s area (green) consists of points that are outside exactly 2 of the circles.


      If we suppose the distance we are interested in is 2d:

      Then the area of the semi-circle with diameter NE, radius = d is: (1/2) 𝛑 d^2.

      And this area is the same as that of the triangle NOE and two half-petals (= 1 petal):

      (1/2) 𝛑 d^2 = d^2 + P

      2P = (𝛑 − 2) d^2

      And Katy’s area (K) consists of 4 petals:

      K = 4P = 2 (𝛑 − 2) d^2

      The radius of the entire circular field is: OE = d √2.

      And so the total area of the field is then: 2 𝛑 d^2.

      And so Jack’s area (J) is:

      J = 2 𝛑 d^2 − 2 (𝛑 − 2) d^2

      J = 4 d^2

      And this is equal to 1 hectare = 10_000 sq m.

      4 d^2 = 10_000 sq m
      d^2 = 2500 sq m
      d = 50 m

      And the distance we are interested in is NE = 2d, hence:

      Solution: The distance between the N gate and E gate is 100 m.

      And Katy’s area is:

      K = 2 (𝛑 − 2) d^2
      K ≈ 5708 sq m

      So, only 57% the size of Jack’s area.

      Like

  • Unknown's avatar

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

    Teaser 2468: [Entry codes] 

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

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

    What was the old code?

    This puzzle was originally published with no title.

    [teaser2468]

     
    • Jim Randell's avatar

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

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

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

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

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

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

      Solution: The old code was: 6921.

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

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

      or:

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

      Like

      • Jim Randell's avatar

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

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

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

        Like

    • Frits's avatar

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

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

      Like

    • GeoffR's avatar

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

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

      Like

    • Ruud's avatar

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

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

      Like

    • Ruud's avatar

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

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

      Like

    • GeoffR's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 3282: Not necessarily in the right order 

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

    For one composition, Skaredahora labelled the 12 notes in a standard octave from O to Z in some order, repeating cyclically. Each chord comprised four notes and had the same interval pattern: this meant the successive intervals between notes, when starting at the lowest note, ascending, and finishing with the addition of a note an octave above the lowest. The intervals all exceeded 1 and never increased. For instance, if the notes in an octave in ascending order were OQSYTVWXRPZU, then a chord of QWXZ would have an interval pattern of 5,1,3,3, which would fail for two reasons (an interval of 1 and an increase from 1 to 3).

    His chords (with notes in random order) were QPWR, WVQO, RXQS, VYRO, STUP and UVYW.

    Starting from O, what are the notes of the octave in ascending order?

    [teaser3282]

     
    • Jim Randell's avatar

      Jim Randell 7:26 am on 17 August 2025 Permalink | Reply

      The following Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library to assign values to the notes, such that each chord consists of non-decreasing intervals. And we then look for solutions where the chords share a common interval pattern.(This structure came about because initially I didn’t realise all the patterns had to be the same. So when I got multiple solutions, I re-read the puzzle, and then added a test at the end to make sure the chords found had the same pattern).

      It runs in 103ms. (Internal runtime is 32ms).

      from enigma import (
        SubstitutedExpression, tuples, is_decreasing, rotate, repeat,
        seq_all_same_r, sprintf, join, map2str, cache, printf
      )
      
      # calculate a valid pattern for a chord
      @cache
      def pattern(ns):
        # put the notes in ascending order, and add in the octave note
        ns = sorted(ns)
        ns.append(ns[0] + 12)
        # calculate the differences between adjacent notes
        ds = tuple(y - x for (x, y) in tuples(ns, 2))
        if 1 in ds: return
        # look for an order that gives non-increasing differences
        for xs in repeat(rotate, ds, len(ds) - 1):
          if is_decreasing(xs, strict=0):
            return xs
      
      # check a valid interval can be constructed
      check = lambda *ns: pattern(ns) is not None
      
      # chords to check
      chords = ["QPWR", "WVQO", "RXQS", "VYRO", "STUP", "UVYW"]
      
      # construct a solver for the puzzle
      exprs = list(sprintf("check({ns})", ns=join(crd, sep=", ")) for crd in chords)
      
      # assign notes O - Z values from 0 - 11 (with O = 0)
      p = SubstitutedExpression(
        exprs, base=12, symbols="OPQRSTUVWXYZ",
        env=dict(check=check), s2d=dict(O=0),
      )
      
      # find solutions with valid patterns
      for s in p.solve(verbose=0):
        # look for chords with a common pattern
        r = seq_all_same_r(pattern(tuple(s[x] for x in crd)) for crd in chords)
        if r.same:
          # output solution (notes and pattern)
          printf("{s} -> {r.value}", s=map2str(s))
      

      Solution: The notes in ascending order are: O P Y Q Z W S V X U R T.

      And so the chords all have an interval pattern of (5, 3, 2, 2). Here they are with the notes in ascending order:

      W R P Q
      V O Q W
      R Q S X
      Y V R O
      P S U T
      U Y W V

      Like

    • GeoffR's avatar

      GeoffR 11:36 am on 25 September 2025 Permalink | Reply

      AI code seems to have progressed significantly in the last few months, in my experience of testing it with teasers. Here is a solution, with all the code being generated by Chat GPT5, except the timer code at the beginning and end of the code from the enigma.py library.

      
      
      from enigma import Timer
      timer = Timer('ST3282', timer='E')
      timer.start()
      
      from itertools import permutations
      
      letters = ["O","P","Q","R","S","T","U","V","W","X","Y","Z"]
      chords = ["QPWR","WVQO","RXQS","VYRO","STUP","UVYW"]
      
      # All non-increasing 4-part partitions of 12 with parts >= 2
      patterns = [
          (6,2,2,2),
          (5,3,2,2),
          (4,4,2,2),
          (4,3,3,2),
          (3,3,3,3),
      ]
      
      def solve():
          for pat in patterns:
              a,b,c,d = pat
              offsets = [0, a, a+b, a+b+c]  # positions of the
              # 4 notes relative to chord's lowest note
      
              # build placements for each chord: list of dicts
              # letter -> absolute position (0..11)
              placements_by_chord = {}
              
              for ch in chords:
                  placements = []
                  seen = set()  # dedupe identical mappings
                  for perm in permutations(ch):
                      if "O" in perm:
                          # If 'O' is in this permutation it forces
                          # the base p so that O maps to 0
                          idxO = perm.index("O")
                          p = (-offsets[idxO]) % 12
                          mapping = {perm[i]: (p + offsets[i]) % 12 \
                                     for i in range(4)}
                          key = tuple(sorted(mapping.items()))
                          if key not in seen:
                              seen.add(key)
                              placements.append(mapping)
                      else:
                          # O not in chord: try all base positions
                          for p in range(12):
                              mapping = {perm[i]: (p + offsets[i]) \
                                         % 12 for i in range(4)}
                              key = tuple(sorted(mapping.items()))
                              if key not in seen:
                                  seen.add(key)
                                  placements.append(mapping)
                  placements_by_chord[ch] = placements
      
              # order chords by fewest placements to improve pruning
              chord_list = sorted(chords, \
                                  key=lambda c: len(placements_by_chord[c]))
      
              # anchor O at position 0
              assignment = {"O": 0}
              used_pos = {0}
      
              def backtrack(i):
                  if i == len(chord_list):
                      return dict(assignment)
                  ch = chord_list[i]
                  for mapping in placements_by_chord[ch]:
                      conflict = False
                      # quick conflict tests
                      for L, pos in mapping.items():
                          if L in assignment and assignment[L] != pos:
                              conflict = True;
                              break
                          if L not in assignment and pos in used_pos:
                              conflict = True;
                              break
                      if conflict:
                          continue
                      # apply
                      added = []
                      for L, pos in mapping.items():
                          if L not in assignment:
                              assignment[L] = pos
                              used_pos.add(pos)
                              added.append(L)
                      res = backtrack(i + 1)
                      if res:
                          return res
                      # undo
                      for L in added:
                          used_pos.remove(assignment[L])
                          del assignment[L]
                  return None
      
              sol = backtrack(0)
              if sol:
                  # fill missing letter (the one not appearing in
                  # any chord) into the free position
                  assigned_letters = set(sol.keys())
                  missing_letter = (set(letters) - assigned_letters).pop()
                  free_pos = (set(range(12)) - set(sol.values())).pop()
                  sol[missing_letter] = free_pos
      
                  # build octave list by position, rotate so O is first
                  order_by_pos = [None] * 12
                  for L, p in sol.items():
                      order_by_pos[p] = L
                  iO = order_by_pos.index("O")
                  octave = order_by_pos[iO:] + order_by_pos[:iO]
                  return pat, octave
          return None, None
      
      pattern, octave = solve()
      if octave:
          print("pattern:", pattern)
          print("octave starting at O:", " ".join(octave))
      else:
          print("No solution found.")
      
      timer.stop()
      timer.report()
      
      # pattern: (5, 3, 2, 2)
      # octave starting at O: O P Y Q Z W S V X U R T
      # [ST3282] total time: 0.0317849s (31.78ms)
      
      
      

      Like

      • Frits's avatar

        Frits 10:00 am on 26 September 2025 Permalink | Reply

        If I prefix the teaser text with “Write a Python program to solve the following puzzle:” the online ChatGPT generated slow code with no answer.

        Like

      • Frits's avatar

        Frits 10:28 am on 26 September 2025 Permalink | Reply

        The online ChatGPT version is the 4o version (may 2024). The version is not easy to find. I had to ask ChatGPT for it’s own version.

        Like

    • GeoffR's avatar

      GeoffR 12:13 pm on 26 September 2025 Permalink | Reply

      @Frits: I am using ChatGPT5 as an upgrade. However, usage is limited and its reverts to 4o version with continued use.

      Like

  • Unknown's avatar

    Jim Randell 10:46 am on 15 August 2025 Permalink | Reply
    Tags:   

    Teaser 2492: [Paper cut] 

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

    I started with a rectangular piece of paper and made a [straight] cut across it, dividing it into a triangle and a pentagon. Then I discarded the triangle and measured the lengths of the sides of the pentagon, all of which were whole numbers of centimetres.

    I remember that the lengths of the shortest two sides [of the pentagon] were 17 cm and 19 cm, and that the length of the longest side was 33 cm.

    What were the lengths of the other two sides?

    This puzzle was originally published with no title.

    [teaser2492]

     
    • Jim Randell's avatar

      Jim Randell 10:46 am on 15 August 2025 Permalink | Reply

      To make a rectangle into a pentagon with a single cut, we cut one of the corners off, which means we discard a right-angled triangle. And all sides of the pentagon have integer lengths, which means so do the sides of the triangle, so its sides are a Pythagorean triple.

      This Python program runs in 60ms. (Internal runtime is 91µs).

      from enigma import (irange, cproduct, pythagorean_triples, ordered, printf)
      
      # the hypotenuse of the removed triangle cannot be more than 33
      for (x, y, z) in pythagorean_triples(33):
        if z < 17: continue  # or less than 17
        # consider possible rectangles (a, b)
        for (a, b) in cproduct([irange(x + 17, 33), irange(y + 17, 33)]):
          # the sides of the pentagon
          P = ordered(a, b, a - x, b - y, z)
          if not (P[0] == 17 and P[1] == 19 and P[-1] == 33): continue
          # output solution
          printf("rectangle={R} triangle={T} -> pentagon={P}", R=(a, b), T=(x, y, z))
      

      Solution: The remaining sides of the pentagon have lengths of 20 cm and 31 cm.

      The scenario is this:

      Like

    • Frits's avatar

      Frits 2:54 pm on 15 August 2025 Permalink | Reply

      from enigma import pythagorean_triples
      
      m1, m2, M = 17, 19, 33
       
      # the hypotenuse of the removed triangle cannot be more than M
      for (x, y, z) in pythagorean_triples(33):
        if z < m1: continue
        # consider possible rectangles (w, h)
        for w in range(m1 + x, M + 1):
          lo, mi, hi = sorted([w, w - x, z])
          if mi < m2: continue
          if {m1, m2, M}.isdisjoint({lo, mi, hi}): continue 
          if hi != M: 
            h_rng = [M] 
          elif lo != m1: 
            h_rng = [m1 + y] if m1 + y <= M else []
          else:
            h_rng = range(m1 + y, M + 1)
          
          for h in h_rng:
            # five sides of the pentagon
            s1, s2, s3, s4, s5 = sorted([lo, mi, hi, h, h - y])
            if not (s1 == m1 and s2 == m2 and s5 == M): continue
            print("answer:", [s3, s4])
      

      Like

  • 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

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