Updates from December, 2025 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 6:32 am on 7 December 2025 Permalink | Reply
    Tags:   

    Teaser 3298: Bletchley Park 

    From The Sunday Times, 7th December 2025 [link] [link]

    Secret agent Robert Holmes was searching the hotel room of a foreign agent who was downstairs having breakfast.

    Holmes discovered a piece of paper containing the [following] text:

    DKCCVTCSZQRZYTAZXTTX

    And he thought this might be a coded message about the foreign agent’s mission, so he sent it to his code-breaking experts.

    They discovered that it was a message that had been scrambled by consistently replacing each letter of the alphabet with a different letter (no letter being used to replace more than one different letter). They decoded the message as a sentence containing four words, which they sent back to Holmes with spaces inserted between words. Holmes realised that his life was in imminent danger as soon as he read it.

    What was the decoded message?

    [teaser3298]

     
    • Jim Randell's avatar

      Jim Randell 6:33 am on 7 December 2025 Permalink | Reply

      Assuming the foreign agent communicates in English, I had a guess at to what the first two words might be, and this gave me enough letters to fill out a likely candidate for the rest of the message immediately.

      Solution: The message can be decoded as: KILL HOLMES BEFORE NOON.


      But the clear text could just be the less threatening: CALL HOLMES BEFORE NOON.

      Or it could not involve HOLMES at all: PULL MELBA STAGE CAREER.

      In fact there are lots of possible clear text substitutions consisting of four English words (although most of them don’t form a coherent sentence).

      Like

      • Frits's avatar

        Frits 8:27 am on 7 December 2025 Permalink | Reply

        @Jim, I also found the solution but wonder how to make a general program that also runs in a reasonable time.

        Like

      • Jim Randell's avatar

        Jim Randell 9:59 am on 7 December 2025 Permalink | Reply

        The following program assumes that HOLMES appears somewhere in the decoded message. And then tries to split the unused cipher text into three possible words. (Which is how I attacked the puzzle manually).

        Using a list of 61871 fairly common English words, the following program finds many possible ways to decipher the text (including the way I found earlier, which still seems to be the most likely candidate solution).

        It runs in 5.03s (using PyPy).

        from enigma import (irange, group, tuples, translate, readlines, printf)
        
        # enciphered text
        code = "DKCCVTCSZQRZYTAZXTTX"
        
        # read words into a dict (indexed by word length)
        path = "words.txt"
        with open(path, "r") as fh:
          words = group(readlines(fh, fn=str.upper, st=str.isalpha), by=len)
        
        # consistently update dict <d> mapping keys <ks> to values <vs>
        def update(d, ks, vs):
          d = dict(d)
          for (k, v) in zip(ks, vs):
            if k == v:
              return
            elif k in d:
              if v != d[k]: return
            elif v in d.values():
              return
            else:
              d[k] = v
          return d
        
        # solve cipher text <ws> = [(<n>, <text>), ...] into <n> words
        # using dict <d>
        def solve(ws, d):
          # are we done?
          if not ws:
            text = translate(code, d)
            printf("-> {text}")
          else:
            # consider the next chunk of code
            (k, t) = ws[0]
            # look for a single word
            if k == 1:
              for x in words.get(len(t), []):
                d1 = update(d, t, x)
                if d1:
                  solve(ws[1:], d1)
            elif k > 1:
              # solve the first word
              for n in irange(1, len(t) + 1 - k):
                for x in words.get(n, []):
                  d1 = update(d, t[:n], x)
                  if d1:
                    solve([(k - 1, t[n:])] + ws[1:], d1)
        
        # look for a run of 6 different letters to be "HOLMES"
        for (i, vs) in enumerate(tuples(code, 6)):
          if len(set(vs)) != 6: continue
          d = dict(zip(vs, "HOLMES"))
          # split the text at HOLMES
          (a, b) = (code[:i], code[i + 6:])
          if not a:
            solve([(3, b)], d)
          elif not b:
            solve([(3, a)], d)
          else:
            solve([(1, a), (2, b)], d)
            solve([(2, a), (1, b)], d)
        

        You will need to provide a list of candidate words in [[ words.txt ]] (or change the definition of path to point to such a list).

        The [[ readlines() ]] function is a recent addition to the enigma.py library.

        Like

  • Unknown's avatar

    Jim Randell 8:25 am on 5 December 2025 Permalink | Reply
    Tags:   

    Teaser 2517: [Christmas alphametic] 

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

    Finley is excited about Christmas. At school, he has made a card showing a triangular-shaped Christmas tree with a fairy on top. With this in mind, please decipher:

    XMAS + TREE = FAIRY

    where different letters stand consistently for different digits, and where TREE is a triangular number (i.e., is the sum of some consecutive integers from 1 upwards).

    What number is XMAS?

    This puzzle was originally published with no title.

    [teaser2517]

     
    • Jim Randell's avatar

      Jim Randell 8:26 am on 5 December 2025 Permalink | Reply

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

      It runs in 78ms. (The internal runtime of the generated code is 1.2ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "XMAS + TREE = FAIRY"
      
      "is_triangular(TREE)"
      
      --answer="XMAS"
      

      Solution: XMAS = 7209.

      The sum is: 7209 + 3655 = 10864.

      And: TREE = 3655 = tri(85) = sum(1..85).

      Like

    • Ruud's avatar

      Ruud 2:14 pm on 5 December 2025 Permalink | Reply

      To run this code, istr 1.1.12 is required.

      import peek
      import istr
      
      for tree in istr.range(1000, 10000):
          if tree.is_triangular():
              t, r, e, e0 = tree
              if e == e0 and tree[:3].all_distinct():
                  digits_without_tree = set(istr.digits()) - {t, r, e}
                  for x, m, a, s, f, i, y in istr.permutations(digits_without_tree):
                      if (xmas := x | m | a | s) + tree == (fairy := f | a | i | r | y):
                          peek(xmas, tree, fairy)
      

      More compact, but much less efficient:

      import peek
      import istr
      
      for x, m, a, s, t, r, e, f, i, y in istr.permutations(istr.digits()):
          if (tree := t | r | e | e).is_triangular() and (xmas := x | m | a | s) + tree == (fairy := f | a | i | r | y):
              peek(xmas, tree, fairy)
      

      Like

    • CB Root's avatar

      CB Root 6:19 pm on 14 December 2025 Permalink | Reply

      Here is some (rather amateurish) Javascript code for your delectation:

      function myFunction() {
        
        var carry;
        var s,e,y;
        var SE;
        var a,r;
        var AE;
        var m,i;
        var x,t;
        var XT;
        var f;
        var XMAS,TREE,FAIRY;
      
        for(s=0;s<=9;s++){
          for(e=0;e<=9;e++){
            SE = s+e;
            y=SE%10;
            if(y>=0 && y<=9){
              carry=Math.floor(SE/10);
              for(a=0;a<=9;a++){
                AE=carry+a+e;
                r=AE%10;
                if(r>=0 && r<=9){
                  carry=Math.floor(AE/10);
                  for(m=0;m<=9;m++){
                    i = carry + m + r;
                    carry=Math.floor(i/10);
                    if(i>=0 && i<=9){
                      for(x=1;x<=9;x++){
                        for(t=0;t<=9;t++){
                          XT = carry + x + t;
                          if(XT%10==a){
                            for(f=1;f<=9;f++){
                              if(allUnique([s,e,y,a,r,m,i,x,t,f])){
                                XMAS = parseInt(x.toString()+m.toString()+a.toString()+s.toString());
                                TREE = parseInt(t.toString()+r.toString()+e.toString()+e.toString());
                                FAIRY = parseInt(f.toString()+a.toString()+i.toString()+r.toString()+y.toString());
                                if(isTriangular(TREE)){
                                  if(FAIRY==(XMAS+TREE)){
                                    Logger.log(' xmas= '+x.toString()+m.toString()+a.toString()+s.toString());
                                    Logger.log(' tree= '+t.toString()+r.toString()+e.toString()+e.toString());
                                    Logger.log('fairy= '+f.toString()+a.toString()+i.toString()+r.toString()+y.toString());
                                  }
                                }
                               }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
      
      function allUnique(arrVal){
      
        var al = arrVal.length-1;
      
        var pp,qq;
      
        for(pp=0;pp<=al;pp++){
          for(qq=0;qq<=al;qq++){
            if(pp!=qq){
              if(arrVal[pp]==arrVal[qq]){
                return false;
              }
            }
          }
        }
        return true;
      }
      
      function isTriangular(testNo){
      
        var product = testNo * 8;
        product = product + 1;
        if(Math.sqrt(product)==Math.floor(Math.sqrt(product))){
          return true;
        }
        else{
          return false;
        }
      }
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:59 am on 15 December 2025 Permalink | Reply

        Hi, thanks for leaving a comment.

        Unfortunately I’m not sure your code made it through the WordPress comments system unscathed. (Unprotected code often loses sections between < and > before I see it and cannot be fixed).

        If you leave a comment with your code between:

        [code]
        <your code here>
        [/code]

        I will fix it up in the original comment.

        Like

    • CB Root's avatar

      CB Root 11:46 am on 15 December 2025 Permalink | Reply

      OK, sorry about that. Here follows my (slightly updated ) code bracketed as requested.

      [see code above]

      Like

    • GeoffR's avatar

      GeoffR 3:08 pm on 15 December 2025 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Check TREE is a 4-digit triangular number
      var 1..140: n;
      
      constraint TREE = n * (n + 1) div 2;
      constraint TREE >= 1000 /\ TREE <= 9999;
      
      var 1..9:X; var 1..9:T; var 1..9:F; var 0..9:M;
      var 0..9:A; var 0..9:S; var 0..9:R; var 0..9:E;
      var 0..9:I; var 0..9:Y;
      
      constraint all_different ([X,T,F,M,A,S,R,E,I,Y]);
      
      var 1000..9999:XMAS = 1000*X + 100*M + 10*A + S;
      var 1000..9999:TREE = 1000*T + 100*R + 11*E ;
      var 10000..99999:FAIRY = 10000*F + 1000*A + 100*I + 10*R + Y;
      
      constraint XMAS + TREE == FAIRY;
      
      solve satisfy;
      output ["XMAS = " ++ show(XMAS) ];
      
      % XMAS = 7209
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 8:55 am on 2 December 2025 Permalink | Reply
    Tags:   

    Teaser 2469: [Football league] 

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

    Our football league has six teams, A, B, C, D, E and F, who play each other once, earning 3 points for a win, 1 for a draw. Of last season’s 20 goals, the best were in Primadona’s hat-trick in C’s derby against D. At one stage, when C and D had played all their games, but A and B each still had two in hand, the league order was A, B, C, D, E, F (goal differences separate teams tying on points). But A’s morale was shattered because, in the end, C won the league (the number of goals scored also being needed for the final decision).

    What were the scores in C’s matches? (C v A, C v B, C v D, C v E, C v F)?

    This puzzle was originally published with no title.

    [teaser2469]

     
    • Jim Randell's avatar

      Jim Randell 8:55 am on 2 December 2025 Permalink | Reply

      The wording of the puzzle suggests that in the final order C had the same points and goal difference as (at least) one of the other teams, and was declared winner based on goals scored.

      I think this puzzle (and probably Teaser 2516) are more easily dealt with manually. But there is a certain challenge in constructing a programmatic solution.

      My program adopts the following strategy:

      [1] Allocate match outcomes for the “one stage”, when C & D have played all their matches, A & B have 2 to play, the league order is A, B, C, D, E, F.

      [2] Allocate the remaining match outcomes such that C is at the top of the table.

      [3] Allocate goals to the outcomes such that 3 goals are scored by (at least) one side in the CvD match.

      [4] Allocate any remaining goals, so that 20 goals are allocated in total.

      [5] Check C wins the league (but has the same points and goal difference as another team) and that the ordering of “one stage” is valid.

      It runs in 5.3s (using PyPy).

      from enigma import (Football, subsets, update, cproduct, peek, is_decreasing, zip_eq, join, printf)
      
      # scoring system
      football = Football(games="wdlx", points=dict(w=3, d=1))
      
      teams = "ABCDEF"
      keys = sorted(x + y for (x, y) in subsets(teams, size=2))
      
      # find keys for each of the teams
      dks = dict((t, list(k for k in keys if t in k)) for t in teams)
      
      # allocate match outcomes for the "one stage"
      def stage1():
        # C and D have played all their games
        for (AD, BD, CD, DE, DF) in football.games("wdl", repeat=5):
          D = football.table([AD, BD, CD, DE, DF], [1, 1, 1, 0, 0])
          for (AC, BC, CE, CF) in football.games("wdl", repeat=4):
            C = football.table([AC, BC, CD, CE, CF], [1, 1, 0, 0, 0])
            if not (C.points >= D.points): continue
      
            # A and B each have two games unplayed (x)
            for (AB, BE, BF) in football.games("wdlx", repeat=3):
              B = football.table([AB, BC, BD, BE, BF], [1, 0, 0, 0, 0])
              if not (B.x == 2 and B.points >= C.points): continue
              for (AE, AF) in football.games("wdlx", repeat=2):
                A = football.table([AB, AC, AD, AE, AF], [0, 0, 0, 0, 0])
                if not (A.x == 2 and A.points >= B.points): continue
      
                # the remaining game (may be unplayed)
                for EF in football.games("wdlx"):
                  E = football.table([AE, BE, CE, DE, EF], [1, 1, 1, 1, 0])
                  if not (D.points >= E.points): continue
                  F = football.table([AF, BF, CF, DF, EF], [1, 1, 1, 1, 1])
                  if not (E.points >= F.points): continue
      
                  # return match outcomes (at "one stage")
                  yield dict(zip(keys, [AB, AC, AD, AE, AF, BC, BD, BE, BF, CD, CE, CF, DE, DF, EF]))
      
      # allocate remaining matches
      def stage2(ms):
        # find unplayed matches
        xs = list(k for (k, v) in ms.items() if v == 'x')
        # allocate match outcomes
        for vs in football.games("wdl", repeat=len(xs)):
          ms1 = update(ms, xs, vs)
          # calculate the new table
          (A, B, C, D, E, F) = (football.extract_table(ms1, t) for t in teams)
          # C is now at the top of the table
          if not (C.points >= max(A.points, B.points, D.points, E.points, F.points)): continue
      
          # return match outcomes (final) and unplayed games (at "one stage")
          yield (ms1, xs)
      
      # allocate goals to the matches; return scorelines
      def stage3(ms):
        # minimum goals
        gs = dict(w=(1, 0), d=(0, 0), l=(0, 1))
        # allocate minimum goals
        ss = dict((k, gs[v]) for (k, v) in ms.items())
        # one side scores 3 goals in the CvD match
        gs = dict(w=[(3, 0), (4, 3)], d=[(3, 3)], l=[(3, 4), (0, 3)])
        k = 'CD'
        for v in gs[ms[k]]:
          yield update(ss, [(k, v)])
      
      # allocate remaining goals (to make 20 in total); return scorelines
      def stage4(ms, ss):
        # calculate number of goals remaining
        r = 20 - sum(sum(x) for x in ss.values())
        if r == 0:
          yield ss
        elif r > 0:
          # chose matches and teams for the remaining goals
          for kts in subsets(cproduct([keys, (0, 1)]), size=r, select='R'):
            # make an updated set of scores
            (ks, ss_) = (set(), ss)
            for (k, t) in kts:
              ks.add(k)
              v = ss_[k]
              ss_ = update(ss_, [(k, update(v, [(t, v[t] + 1)]))])
            # check we haven't changed any outcomes
            if all(peek(football.outcomes([ss_[k]], [0])) == ms[k] for k in ks):
              yield ss_
      
      # calculate (<points>, <goal-difference>, <goals-for>) score for team <t>
      def score(t, ms, ss):
        T = football.extract_table(ms, t)
        (gf, ga) = football.extract_goals(ss, t)
        return (T.points, gf - ga, gf)
      
      # check a scenario
      def check(ms, ss, xs):
        # check C wins the league
        (A, B, C, D, E, F) = (score(t, ms, ss) for t in teams)
        # C is at the top of the table
        others = (A, B, D, E, F)
        if not C > max(others): return
        # but has the same number of points _and_ goal difference as another team
        if not any(zip_eq(C, X, first=2) for X in others): return
      
        # check the positions with the matches in <xs> unplayed
        ms = update(ms, xs, ['x'] * len(xs))
        ss = update(ss, xs, [None] * len(xs))
        scores = tuple(score(t, ms, ss) for t in teams)
        # check the order is A, B, C, D, E, F
        if not is_decreasing(scores, strict=1): return
      
        # this is a valid scenario
        return scores
      
      # put it all together ...
      for ms1 in stage1():
        for (ms, xs) in stage2(ms1):
          for ss3 in stage3(ms):
            for ss in stage4(ms, ss3):
              # extract (<pts>, <diff>, <for>) scores for a valid scenario
              scores = check(ms, ss, xs)
              if scores:
                printf("unplayed = {xs}", xs=join(xs, sep=", ", sort=1))
                printf("scores = {scores}")  # scores at "one stage"
                football.output_matches(ms, ss)  # final results
      

      Solution: The scores in C’s matches are: C v A = 0-1; C v B = 0-1; C v D = 3-3; C v E = 1-0; C v F = 2-0.

      The full set of results is:

      A vs B = (d) 0-0
      A vs C = (w) 1-0
      A vs D = (w) 2-0
      A vs E = (l) 0-1
      A vs F = (l) 0-1
      B vs C = (w) 1-0
      B vs D = (w) 1-0
      B vs E = (l) 0-1
      B vs F = (l) 0-1
      C vs D = (d) 3-3
      C vs E = (w) 1-0
      C vs F = (w) 2-0
      D vs E = (w) 1-0
      D vs F = (w) 1-0
      E vs F = (d) 0-0

      The final order was:

      C: 2w1d = 7 points; for = 6; against = 5; difference = +1
      A: 2w1d = 7 points; for = 3; against = 2; difference = +1
      B: 2w1d = 7 points; for = 2; against = 2; difference = 0
      E: 2w1d = 7 points; for = 2; against = 2; difference = 0
      D: 2w1d = 7 points; for = 5; against = 6; difference = −1
      F: 2w1d = 7 points; for = 2; against = 3; difference = −1

      i.e. C > A > B = E > D > F.

      Note that B and E cannot be separated in the final order by points, goal difference, or goals scored.

      At the intermediate stage the following games were unplayed:

      A v E; A v F; B v E; B v F; and maybe E v F

      Giving the following orders:

      A: 3 played; 2w1d = 7 points; for = 3; against = 0; difference = +3
      B: 3 played; 2w1d = 7 points; for = 2; against = 0; difference = +2
      C: 5 played; 2w1d = 7 points; for = 6; against = 5; difference = +1
      D: 5 played; 2w1d = 7 points; for = 5; against = 6; difference = −1

      E: 3 played; 1d = 1 point; for = 0; against = 2; difference = −2
      F: 3 played; 1d = 1 point; for = 0; against = 3; difference = −3

      or (if E v F is unplayed):

      E: 2 played; 0 points; for = 0; against = 2; difference = −2
      F: 2 played; 0 points; for = 0; against = 3; difference = −3

      In either case the order is: A > B > C > D > E > F.

      Like

  • Unknown's avatar

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

    Teaser 3297: 6oker 

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

    6oker is a 6-card version of poker. Like 5-card poker the rank of a hand is based on the number of distinct variants of that type possible from a 52-card pack. Fewer variants gives a higher (winning) rank. For example, a running flush ({A, 2, 3, 4, 5} to {10, J, Q, K, A} in one suit) has 40 variants, beating four-of-a-kind (e.g., four aces with another card) which has 624 variants.

    Playing 6oker, Al and Di held hands of different rank. Each comprised only two card values and no aces, jacks, queens or kings (e.g., four 3s and two 6s). These four values had no common prime factors.

    Ignoring suits, if you were told just Al’s hand you couldn’t be sure of Di’s, but if you were told just Di’s you could be sure of Al’s.

    Who won? Ignoring suits, give Al’s hand.

    [teaser3297]

     
    • Jim Randell's avatar

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

      With 6 cards of two different values you must be holding either 2 of one value and 4 of the other (936 variants), or holding 3 of each value (1248 variants). (I wrote some code to check all possible 6-card hands to ensure I got these numbers correct).

      So the first type of hand is a higher rank than the second. And one type is Al’s and the other is Di’s (and whoever has the higher rank wins).

      This Python program generates the possible values in each hand, and uses the [[ filter_unique() ]] function from the enigma.py library to determine possible distributions of cards in each hand.

      It runs in 71ms. (Internal runtime is 788µs).

      from enigma import (
        irange, subsets, is_coprime, diff, filter_unique,
        item, intersect, ulambda, sprintf, printf
      )
      
      # generate possible values in the hands
      def generate():
        # find 4 card values with that are pairwise coprime
        for vs in subsets(irange(2, 10), size=4):
          if not is_coprime(*vs): continue
      
          # choose 2 values for A
          for A in subsets(vs, size=2):
            D = diff(vs, A)
            yield (A, D)
      
      # possible distribution of values
      hs = list()
      for ((a1, a2), (d1, d2)) in generate():
        # record possible hands (value, quantity)
        hs.extend([
          (((a1, 3), (a2, 3)), ((d1, 2), (d2, 4))),
          (((a1, 3), (a2, 3)), ((d1, 4), (d2, 2))),
          (((a1, 2), (a2, 4)), ((d1, 3), (d2, 3))),
          (((a1, 4), (a2, 2)), ((d1, 3), (d2, 3))),
        ])
      
      # format a hand
      fmt = ulambda('((v1, q1), (v2, q2)): sprintf("{q1}x {v1}s + {q2}x {v2}s")')
      
      # if we were just told A we couldn't deduce D
      hs1 = filter_unique(hs, f=item(0), g=item(1)).non_unique
      
      # if we were just told D we could deduce A
      hs2 = filter_unique(hs, f=item(1), g=item(0)).unique
      
      # the answer must be in the intersection
      for (A, D) in intersect([hs1, hs2]):
        win = ("A" if D[0][1] == 3 else "D") # whoever has "3 and 3" is the loser
        # output scenario
        printf("A=({A}) D=({D}); win = {win}", A=fmt(A), D=fmt(D))
      

      Solution: Di won. Al was holding three 5s and three 7s.

      Di is holding two cards of one value, and four cards of another value. The values are one of:

      2s and 3s
      2s and 9s
      3s and 4s
      3s and 8s
      4s and 9s
      8s and 9s

      Like

      • Frits's avatar

        Frits 7:55 pm on 2 December 2025 Permalink | Reply

        @Jim,

        Is there a special reason why you determine the intersection?
        I assume list “hs2” can be built directly from list “hs1”.

        Like

        • Jim Randell's avatar

          Jim Randell 8:43 am on 3 December 2025 Permalink | Reply

          @Frits: hs1 is the set of possible hands (ignoring suits) where if we knew A’s hand we could not deduce D’s hand. hs2 is the set of possible hands where if we knew D’s hand we could deduce A’s hand. For a valid solution to the puzzle the hands must satisfy both these conditions, so we look at the intersection of these two sets (i.e. the elements that are in both sets).

          Like

    • Pete Good's avatar

      Pete Good 7:55 pm on 30 November 2025 Permalink | Reply

      Jim, I agree that 3 + 3 has a higher rank than 4 + 2 so the player with 3 + 3 wins, but the comment in your program says that whoever has 3 and 3 is the loser?!

      Like

      • Jim Randell's avatar

        Jim Randell 9:48 pm on 30 November 2025 Permalink | Reply

        @Pete: I calculated that “4 and 2” has fewer variants than “3 and 3”, so “4 and 2” is the better hand.

        Like

    • Pete Good's avatar

      Pete Good 11:13 am on 1 December 2025 Permalink | Reply

      Thanks Jim. I misunderstood the ranking process described in the teaser.

      Like

  • Unknown's avatar

    Jim Randell 9:08 am on 28 November 2025 Permalink | Reply
    Tags:   

    Teaser 2474: [Flipping counters] 

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

    Imagine that you have 64 small round counters, black on one side and white on the other, and you have to place them all on the different squares of a chessboard, so that those on the black squares all finish white side up and those on the white squares finish black side up.

    Note that as each counter is placed on the chessboard, you must turn over any counters already on the two, three or four adjacent squares.

    In completing this task, how many times would you turn counters over?

    This puzzle was originally published with no title.

    [teaser2474]

     
    • Jim Randell's avatar

      Jim Randell 9:10 am on 28 November 2025 Permalink | Reply

      (See also: Enigma 1256, Enigma 1767, both also set by Bob Walker).

      For an N×M array, counters will be turned over N×(M − 1) + M×(N − 1) times. In this case N = M = 8.

      Solution: 112 counters are turned over.

      We can use the code from Enigma 1767 to generate a sequence of moves for an 8×8 grid:

      % python3 enigma1767b.py 8 8
      64: [63, 62, 61, 60, 59, 58, 57, 55, 54, 53, 52, 51, 50, 49, 48, 56, 47, 46, 45, 44, 43, 42, 41, 39, 38, 37, 36, 35, 34, 33, 32, 40, 31, 30, 29, 28, 27, 26, 25, 23, 22, 21, 20, 19, 18, 17, 16, 24, 8, 9, 10, 11, 12, 13, 14, 7, 15, 5, 6, 3, 4, 1, 2, 0]
      

      At the end of this procedure each counter is placed, and then turned over 0 or 2 times. So when we first place a counter we do so with the desired final face uppermost.

      Like

  • Unknown's avatar

    Jim Randell 7:42 am on 25 November 2025 Permalink | Reply
    Tags:   

    Teaser 2516: [Football league] 

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

    Six teams — Ayes, Bees, Cees, Dees, Ease and Fees — played each other once in a football league, with three points for a win and one for a draw. Ayes finished top, with the order of the teams being alphabetical. The difference in points between each pair of adjacent teams was the same. The 13 goals scored included hat-tricks from the Dees striker in two matches. In one game, the Cees goalkeeper let in two before being substituted.

    What were Cees’ five results (in the order CvA, CvB, CvD, CvE, CvF)?

    This puzzle was originally published with no title.

    [teaser2516]

     
    • Jim Randell's avatar

      Jim Randell 7:43 am on 25 November 2025 Permalink | Reply

      The points form an arithmetic sequence, from low (F) to high (A) = (f, f + x, f + 2x, f + 3x, f + 4x, f + 5x).

      So the total number of points is: 6f + 15x.

      There are 6 teams, and 15 matches in all. And there are 13 goals scored in total.

      We know 6 of the goals are involved in two of D’s matches, which leaves just 7 goals remaining to be allocated. So the maximum number of games that are won/lost is 9, meaning there must be at least 6 matches that are drawn. And in order for the 5 teams to have different points totals there can be no more than 11 drawn matches.

      The following Python program considers the possible numbers for matches that are drawn (and the rest must be won/lost), calculates the total number of points allocated, and constructs possible arithmetic sequences corresponding to this total. It then uses the [[ Football() ]] helper class from the enigma.py library to allocate match outcomes that give the required points totals, and then allocates the 13 goals accordingly.

      It runs in 239ms. (Internal runtime is 173ms).

      from enigma import (Football, irange, div, rev, seq_get, subsets, ordered, diff, update, fail, sprintf, printf)
      
      # scoring system
      football = Football(games="wdl", points=dict(w=3, d=1))
      
      # allocate match outcomes for teams <ts>, points <pts>,
      # with <w> total wins and <d> total draws
      def matches(ts, pts, w, d, i=0, ms=dict()):
        # look for the next team
        t = seq_get(ts, i)
        # are we done?
        if t is None:
          if w == 0 and d == 0:
            yield ms
        else:
          # find unallocated keys involving this team
          ks = list(ordered(t, x) for x in ts if x != t)
          ks = diff(ks, ms)
          # consider match outcomes for the unallocated games
          for vs in football.games(repeat=len(ks)):
            ms_ = update(ms, ks, vs)
            # extract the table and check we have achieved the required points
            T = football.extract_table(ms_, t)
            if T.points != pts[i]: continue
            # and have not exceeded the number of wins/draws
            (w_, d_) = (w - T.w, d - T.d)
            if (w_ < 0 or d_ < 0): continue
            # solve for the remaining teams
            yield from matches(ts, pts, w_, d_, i + 1, ms_)
      
      # solve for points <pts> with <w> matches won/lost and <d> matches drawn
      def solve(pts, w, d):
        # fill out matches (note: each draw is counted by both sides)
        for ms in matches("ABCDEF", pts, w, 2 * d):
          # extract keys for C and D
          (Cs, cs) = football.extract_keys(ms, 'C')
          (Ds, ds) = football.extract_keys(ms, 'D')
      
          # allocate minimum possible goals for the matches
          gs = dict(w=(1, 0), d=(0, 0), l=(0, 1))
          ss0 = dict((k, gs[v]) for (k, v) in ms.items())
      
          # choose 2 matches for D to have hat-tricks
          gs = [dict(w=(3, 0), d=(3, 3), l=(3, 4)), dict(w=(4, 3), d=(3, 3), l=(0, 3))]
          for kts in subsets(zip(Ds, ds), size=2):
            ss = update(ss0, ((k, gs[t][ms[k]]) for (k, t) in kts))
            # find the number of remaining goals to allocate
            r = 13 - sum(sum(v) for v in ss.values())
            if r < 0: continue
            if r > 0: fail(msg=sprintf("distribute {r} remaining goals"))
      
            # C concedes (at least) 2 goals in (at least) one match
            if not any(ss[k][1 - t] >= 2 for (k, t) in zip(Cs, cs)): continue
      
            # output scenario
            football.output_matches(ms, ss)
      
      # consider number of drawn matches (between 6 and 11)
      for d in irange(6, 11):
        # the remaining matches are won/lost
        w = 15 - d
        # total points allocated
        t = 2*d + 3*w
        # consider points difference between teams
        for x in [1, 2, 3]:
          f = div(t - 15*x, 6)
          if f is None or f < 0: continue
          pts = rev(f + i*x for i in irange(6))
          # attempt to solve for this sequence of points
          printf("[d={d}: w={w} -> t={t}; x={x} f={f} -> pts = {pts}]")
          solve(pts, w, d)
      

      Fortunately it turns out that after the minimum possible goals have been allocated to the matches, there are no “discretionary” goals remaining to allocate, so I didn’t bother to write that section of code.

      Solution: The scores in Cee’s matches were: CvA = 0-1; CvB = 1-0; CvD = 0-3; CvE = 1-0; CvF = 0-0.

      The full list of match outcomes and scores is:

      A vs B = (d) 0-0
      A vs C = (w) 1-0
      A vs D = (w) 1-0
      A vs E = (d) 0-0
      A vs F = (d) 0-0
      B vs C = (l) 0-1
      B vs D = (w) 1-0
      B vs E = (w) 1-0
      B vs F = (d) 0-0
      C vs D = (l) 0-3
      C vs E = (w) 1-0
      C vs F = (d) 0-0
      D vs E = (l) 0-1
      D vs F = (w) 3-0
      E vs F = (d) 0-0

      And the points are: A = 9, B = 8, C = 7, D = 6, E = 5, F = 4.

      The program actually runs fine considering the full range of possible draws (d = 0..15), so we don’t need to do any pre-analysis.

      Like

    • Frits's avatar

      Frits 4:58 am on 30 November 2025 Permalink | Reply

      rev = lambda s: s[::-1]
      opp = lambda n: 1 if n == 1 else 3 - n
      # return sorted tuple
      stup = lambda x, y: (x, y) if x < y else (y, x)
      tri = lambda n: n * (n + 1) // 2
      
      # remove elements from <s2> from <s1>
      # eg diff_lists([1,3,1,2], [2,0,1]) results in [1,3]
      def diff_lists(s1, s2):
        s = s1.copy()
        for x in s2:
          if x in s1:
            s.remove(x)
        return s 
      
      # return a permutation of sequence <s> where s may have duplicate elements   
      def permutations_non_unique(s, k, ss=()):
        if k == 0:
          yield ss
        else:
          for v in set(s):
            i = s.index(v)
            # list without first occurrence of <v> 
            s_ = s[:i] + s[i + 1:]
            yield from permutations_non_unique(s_, k - 1, ss + (v, ))
      
      # generate a list of 15 wins/losses/draws
      def gen_games(k, s, ps, ss=[]):
        if k == -1:
          yield ss
        else:
          # reverse the games that are already known
          r = tuple(opp(x[4 - k]) for x in ss)
          sumr = sum(r)
          # get <k> more games
          for p in permutations_non_unique(s, k):
            # check total points per team
            if sumr + sum(p) == ps[5 - k]:
              yield from gen_games(k - 1, diff_lists(s, p), ps, ss + [r + p])
      
      # generate the scores of a team
      def solve_team(gs, r, k, mxNotD, mxD, ngs, sss, ss=[]):
        if k == 0:
          yield ss, ngs
        else:
          g, i = gs[0], 5 - k
          mn = 1 if g == 3 else 0
          mx = mxD if r == 3 or i == 3 else mxNotD
          if i < r: # we know the opponents score
            o = sss[i][r - 1]
            if g == 3:
              mn = o + 1
            elif g == 0:
              mx = min(mx, o - 1)
            else:
              mn, mx = o, o 
          
          for sc in range(mn, mx + 1):
            if sc > ngs: break
            yield from solve_team(gs[1:], r, k - 1, mxNotD, mxD, 
                                  ngs - sc, sss, ss + [sc])   
        
      # generate the scores of all teams    
      def solve(gss, k, mxNotD, mxD, ngs=0, ss=[]):
        if k == 6:
          if ngs == 0:
            yield ss
        else:
          gs = gss[0] 
          for sc, ngs_ in solve_team(gs, k, 5, mxNotD, mxD, ngs, ss):
            if k == 3:
              # The Dees had 2 hat-trick games
              if sum(x > 2 for x in sc) < 2: continue 
            yield from solve(gss[1:], k + 1, mxNotD, mxD, ngs_, ss + [sc])
      
      sols = set()        
      # two matches have at least 3 goals so we can have 9 wins at most
      for w in range(1, 10):
        d = 15 - w
        tp = w + 30   # total points: 3 * w + 2 * d
        # possible increments of the arithmetic sequence (ap, bp ... fp)
        for inc in range(1, 4):
          fp, r = divmod(tp - 15 * inc, 6)
          # we have 13 - 6 = 7 goals to divide
          # for games without the Dees determine the maximum goals per win
          maxNonD = 7 - (w - 3) 
          if r or fp < 0 or maxNonD < 1: continue
          # the Dees can have 5 wins at most, leaving (w - 5) wins for other teams
          # so for D we have 13 - 3 (other hat-trick) - 3 (non-hat-trick) - (w - 5) 
          # or 12 - w maximum goals per win
          maxD = 12 - w
          # check if both hat-trick games must be both wins
          if (ht_wins := 13 - 3 * 3 < w - 2):
            if fp + 2 * inc < 6: continue
          # points for the six teams
          ps = [fp + x * inc for x in range(5, -1, -1)]
          ws, ds = [0, 3] * w, [1] * d
          
          # generate a list of 15 wins/losses/draws
          for gs in gen_games(5, ws + ds, ps):
            # check hat-trick games
            if ht_wins and sum(x == 3 for x in gs[3]) < 2: continue
            # assign scores to games
            for goals in solve(gs, 0, maxNonD, maxD, 13, []):
              vsC = [gs[2] for i, gs in enumerate(goals) if i != 2]
              # in one game the Cees goalkeeper let in two before being substituted
              if max(vsC) < 2: continue
              sols.add(tuple(zip(goals[2], vsC)))
              
      print("answer:", ' or '.join(str(s)[1:-1] for s in sols)) 
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:17 am on 30 November 2025 Permalink | Reply

        @Frits: Your code gives a result of 1-1 in the CvB match. But the correct result is 1-0. (See my comment above for the complete set of results).

        Like

        • Frits's avatar

          Frits 3:51 am on 1 December 2025 Permalink | Reply

          @Jim,

          I found that out myself today (when comparing it to Brian’s output).

          Please use:

          vsC = [gs[2 if i > 2 else 1] for i, gs in enumerate(goals) if i != 2]
          

          Like

  • Unknown's avatar

    Jim Randell 6:17 am on 23 November 2025 Permalink | Reply
    Tags:   

    Teaser 3296: Woolly jumpers 

    From The Sunday Times, 23rd November 2025 [link] [link]

    Having racked my brains all day trying to devise a suitable Teaser, I woke with my mind racing, just after 4am according to my digital clock. I couldn’t get back to sleep, so I decided to try counting sheep. I imagined them to be numbered starting with the figures shown on the clock, then counting upwards. Each sheep jumped through a particular gap in a fence according to the number of prime factors of its number. Repetitions were counted, so that sheep 405 (= 3×3×3×3×5) jumped through the fifth gap.

    The last thing I remember noticing before eventually falling asleep was that, for the first time, five consecutive sheep had jumped through the same gap.

    What was the number of the last of these five sheep?

    [teaser3296]

     
    • Jim Randell's avatar

      Jim Randell 6:27 am on 23 November 2025 Permalink | Reply

      The function that counts the number of prime divisors (including repeats) of a number n is known as the “big omega” function = 𝛀(n) [@wikipedia].

      This Python program organises the numbers from 401 upwards into clumps with the same number of prime factors, and stops when it finds the first clump with (at least) 5 elements. Short and sweet (and fast)!

      It runs in 69ms. (Internal runtime is 399µs).

      from enigma import (irange, inf, prime_factors, clump, icount, printf)
      
      # count the number of prime factors in <n> (including repeats)
      n_prime_factors = lambda n: icount(prime_factors(n))
      
      # clump numbers together that have the same number of prime factors
      for ns in clump(irange(401, inf), n_prime_factors):
        k = len(ns)
        if k >= 5:
          printf("{k} -> {ns}; last = {n}", n=ns[4])
          break
      

      Solution: The number of the fifth sheep was 606.

      601 and 607 are prime, but between them each of 602 – 606 have exactly 3 prime factors:

      602 = 2 × 7 × 43
      603 = 3 × 3 × 67
      604 = 2 × 2 × 151
      605 = 5 × 11 × 11
      606 = 2 × 3 × 101

      Liked by 1 person

      • Tony Smith's avatar

        Tony Smith 10:47 am on 30 November 2025 Permalink | Reply

        All that is necessary for 601 and 607 is that 601 does not have the same number of prime factors as 602-606.

        Like

    • Frits's avatar

      Frits 6:58 am on 23 November 2025 Permalink | Reply

      def n_prime_factors(n):
        i = 2
        t = 0
        while i * i <= n:
          if n % i:
            i += 1
          else:
            n //= i
            t += 1
        return t + 1 if n > 1 else t
      
      p, t = -1, 0
      for hh in range(4, 24):
        h100 = 100 * hh
        for mm in range(1 if hh == 4 else 0, 60):
          if (n := n_prime_factors(h100 + mm)) == p:
            t += 1
            # five consecutive sheep had jumped through the same gap?
            if t == 5:
              print("answer:", h100 + mm)
              break
          else:
            t = 1
          p = n  
        else: # no break
          continue
        break  
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:34 am on 23 November 2025 Permalink | Reply

        @Frits: The puzzle text says that the starting number comes from the clock. Not that all numbers come from the clock. So I don’t think it is right to skip 460 .. 499 etc.

        Like

        • Frits's avatar

          Frits 10:44 am on 23 November 2025 Permalink | Reply

          @Jim, you are right.

          Here is a program with fewer calls to n_prime_factors().

          def n_prime_factors(n):
            i = 2
            t = 0
            while i * i <= n:
              if n % i:
                i += 1
              else:
                n //= i
                t += 1
            return t + 1 if n > 1 else t
          
          '''
          .   n-3
          A   n-2
          .   n-1
          B   n
          .   n+1 
          C   n+2
          .
          '''
          
          p = -1
          n = 402
          while True:
            if (npf := n_prime_factors(n)) == p:
              # five consecutive sheep had jumped through the same gap?
              # numbers n-1 and n+1 must have the same number of prime factors
              if all(n_prime_factors(x) == p for x in [n - 1, n + 1]):
                if n_prime_factors(n - 3) == p:
                  print("answer:", n + 1)
                  break
                elif n_prime_factors(n + 2) == p:
                  print("answer:", n + 2)
                  break
            p = npf  
            n += 2
          

          Like

    • Ruud's avatar

      Ruud 7:52 am on 23 November 2025 Permalink | Reply

      import peek
      from sympy import factorint
      
      
      def count_prime_factors(n: int) -> int:
          factors = factorint(n)
          return sum(factors.values())
      
      
      def next_time(t, n):
          return t + n + (((t + n) % 100) > 59) * 40
      
      
      t = 400
      while True:
          gaps = {count_prime_factors(next_time(t, i)) for i in range(5)}
          if len(gaps) == 1:
              peek(next_time(t, 4))
              break
          t = next_time(t, 1)
      

      Like

    • Ruud's avatar

      Ruud 1:28 pm on 23 November 2025 Permalink | Reply

      I think @Jim Randell is right that the numbers are nit the clock times, which I had assumed as well.
      That makes the code much simpler and can even be done as a one liner):

      import sympy
      import itertools
      
      print(next(t + 4 for t in itertools.count(400) if len({sum(sympy.factorint(t).values()) for t in range(t, t + 5)}) == 1))
      

      Like

  • Unknown's avatar

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

    Teaser 2498: [Party trick] 

    From The Sunday Times, 8th August 2010 [link]

    Keith and I used to do a party trick. We had a set of cards labelled A, B, C, D, E, F, G and H. We would ask someone to choose three cards without Keith seeing. I would pass Keith one of the chosen cards, followed by a second. Amazingly, he would announce what the third chosen card was. Keith and I have phenomenal memories, so I then thought of a way to improve the trick. On each of the two occasions, I pass a card with either my left or right hand. Using this extra piece of trickery, we increased the number of cards in the set, ending with a letter way beyond H.

    What is the letter on the last card?

    This puzzle was originally published with no title.

    [teaser2498]

     
    • Jim Randell's avatar

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

      See also: Enigma 1214 (which was set by Keith Austin).

      There are 3 cards chosen, so the setter has P(3, 2) = 6 ways they can choose to present two of the cards to Keith. If each of these ways is used to identify the remaining card there can be up to 6 + 2 = 8 cards in the pack. And this is the initial scenario.

      In the second part of the puzzle each of the two passes can be made with the left (L) or right (R) hand, so each of the 6 ways of presenting the cards can have 4 variations (LL, LR, RL, RR), so there are now a total of 6 × 4 = 24 different ways of identifying the missing card. So there can be up to 26 cards in the pack. Hence:

      Solution: The last card has the letter Z on it.

      Like

  • Unknown's avatar

    Jim Randell 9:55 am on 18 November 2025 Permalink | Reply
    Tags:   

    Teaser 2483: [Cricket scores] 

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

    George and Martha were watching cricket. After the visitors’ innings, George said: “How odd! Every time two batsmen were together, the partnership was broken when the team’s total was the product of their two batting-order numbers. But one partnership created a stand of over 50”. “And”, said Martha, “each time an odd-numbered batsman was out, the next batsman out was even-numbered. This continued until the dismissal of batsman 11”. The home side’s innings saw a similar pattern, but their biggest stand was higher.

    What was (a) the home team’s biggest stand; (b) the result of the match?

    This puzzle was originally published with no title.

    [teaser2483]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 18 November 2025 Permalink | Reply

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

      from enigma import (defaultdict, subsets, tuples, printf)
      
      # solve for pairs <a> and <b>, next batsman <n>
      def solve(a=1, b=2, n=3, ts=list(), bs=list()):
        if n < 13:
          # total score when this pair ends
          t = a * b
          if ts and t < ts[-1]: return
          if n == 12:
            # either of the final pair is dismissed
            yield (ts + [t], bs + [(a, b)])
          else:
            # a is dismissed
            if not (a % 2 != 0 and bs and bs[-1] % 2 == 1):
              yield from solve(b, n, n + 1, ts + [t], bs + [a])
            # b is dismissed
            if not (b % 2 != 0 and bs and bs[-1] % 2 == 1):
              yield from solve(a, n, n + 1, ts + [t], bs + [b])
      
      # record scenarios by maximum pair score
      rs = defaultdict(list)
      
      # find possible scores
      for (ts, bs) in solve():
        # find the maximum score for a pair
        m = max(y - x for (x, y) in tuples(ts, 2))
        if not (m > 50): continue
        printf("[totals = {ts}; batsmen = {bs}; m = {m}]")
        rs[m].append((ts, bs))
      
      # choose max pair scores for visitors and home teams
      for (v, h) in subsets(sorted(rs.keys()), size=2):
        printf("\nmax pair: visitors = {v}, home = {h}")
        for (ts, bs) in rs[v]:
          printf("visitors: totals = {ts}; batsmen = {bs}")
        for (ts, bs) in rs[h]:
          printf("home: totals = {ts}; batsmen = {bs}")
      

      Solution: (a) The home team’s biggest stand was 81; (b) The result of the match was a tie (i.e. both teams achieved the same number of runs).

      The visitor’s scoresheet is one of the following:

      1 & 2: stand = 2; total = 2 [1 dismissed]
      2 & 3: stand = 4; total = 6 [2 dismissed]
      3 & 4: stand = 6; total = 12 [4 dismissed]

      or:

      1 & 2: stand = 2; total = 2 [2 dismissed]
      1 & 3: stand = 1; total = 3 [1 dismissed]
      3 & 4: stand = 9; total = 12 [4 dismissed]

      then:

      3 & 5: stand = 3; total = 15 [5 dismissed]
      3 & 6: stand = 3; total = 18 [6 dismissed]
      3 & 7: stand = 3; total = 21 [7 dismissed]
      3 & 8: stand = 3; total = 24 [8 dismissed]
      3 & 9: stand = 3; total = 27 [3 dismissed]
      9 & 10: stand = 63; total = 90 [10 dismissed]
      9 & 11: stand = 9; total = 99

      And the home team’s scoresheet is:

      1 & 2: stand = 2; total = 2 [2 dismissed]
      1 & 3: stand = 1; total = 3 [3 dismissed]
      1 & 4: stand = 1; total = 4 [4 dismissed]
      1 & 5: stand = 1; total = 5 [5 dismissed]
      1 & 6: stand = 1; total = 6 [6 dismissed]
      1 & 7: stand = 1; total = 7 [7 dismissed]
      1 & 8: stand = 1; total = 8 [8 dismissed]
      1 & 9: stand = 1; total = 9 [1 dismissed]
      9 & 10: stand = 81; total = 90 [10 dismissed]
      9 & 11: stand = 9; total = 99

      Each team has a final total of 99, and so the match is tied.

      Like

  • Unknown's avatar

    Jim Randell 7:32 am on 16 November 2025 Permalink | Reply
    Tags:   

    Teaser 3295: Always as the crow flies 

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

    I have a map showing the location of four castles. All of the distances between the castles are different two-digit whole numbers of miles, as the crow flies. Alton is due north of Sawley; Derry is furthest west. Fenwick is due east of Derry. Alton and Derry are the shortest distance apart, while the distance from Alton to Sawley is the largest possible to comply with all the other information.

    Again, as the crow flies:

    How far is a round trip of the castles (A to F to S to D to A)?

    “As the crow flies” refers to the shortest (i.e. straight line) distance between two points.

    [teaser3295]

     
    • Jim Randell's avatar

      Jim Randell 11:15 am on 16 November 2025 Permalink | Reply

      The following code generates candidate orthodiagonal quadrilaterals [@wikipedia] with distinct 2-digit values for the sides (AD, AF, DS, FS), and also distinct 2-digit diagonals (AS, DF) such that AS is the largest possible value for the given sides. From these candidates we can then choose those with the maximum possible vertical diagonal (AS).

      I added the [[ triangle_height() ]] function to the enigma.py library.

      It runs in 437ms. (Internal runtime is 363ms).

      from enigma import (enigma, Accumulator, irange, subsets, divisors_pairs, sq, div, printf)
      
      Q = enigma.Rational()
      
      # set parameters for triangle_height(), is_triangle(), as_int()
      is_square_q = enigma.partial(enigma.is_square_q, F=Q)
      triangle_height = enigma.partial(enigma.triangle_height, div=Q, sqrt=is_square_q)
      is_triangle = enigma.partial(enigma.is_triangle, fn=enigma.gt(0))
      as_int = enigma.partial(enigma.as_int, default=None)
      
      # generate candidate orthodiagonal quadrilaterals
      def generate():
        # choose the shortest side (AD) and an adjacent side (DS)
        for (AD, DS) in subsets(irange(10, 99), size=2):
          # calculate distances for the other two sides [AD^2 + FS^2 = AF^2 + DS^2]
          for (x, y) in divisors_pairs(sq(DS) - sq(AD)):
            (AF, FS) = (div(y - x, 2), div(x + y, 2))
            if (not AF) or (not FS) or not (AD < AF < 100 and AD < FS < 100): continue
            if len({AD, DS, AF, FS}) != 4: continue
      
            # look for max AS that gives an integer DF
            for AS in irange(min(AD + DS, AF + FS, 99), AD + 1, step=-1):
              # calculate DF
              x1 = triangle_height(AS, AD, DS)
              x2 = triangle_height(AS, AF, FS)
              if (not x1) or (not x2): continue
              DF = as_int(x1 + x2)
              if (not DF) or not (AD < DF < 100): continue
              if len({AD, DS, AF, FS, AS, DF}) != 6: continue
      
              # check triangles; use [[ fn=enigma.ge(0) ]] above to allow collinearity
              tris = [(AS, AD, DS), (AS, AF, FS), (DF, AD, AF), (DF, DS, FS)]
              if not all(is_triangle(*ss) for ss in tris): continue
      
              # return (<sides>, <diags>)
              yield ((AD, DS, AF, FS), (AS, DF))
              break  # we only need the maximal AS
      
      # look for maximal vertical diagonal (AS)
      r = Accumulator(fn=max, collect=1)
      printf("sides = (AD, DS, AF, FS), diags = (AS, DF)")
      for (ss, ds) in generate():
        p = sum(ss)
        r.accumulate_data(ds[0], (ss, ds, p))
        printf("sides = {ss}, diags = {ds} -> perimeter = {p} [{r.count}]")
      
      # output solution
      printf("\nmax AS = {r.value}")
      for (ss, ds, p) in r.data:
        printf("sides = {ss}, diags = {ds} -> perimeter = {p}")
      

      I suspect the setter intends no three of the castles to be collinear (even though this condition is not mentioned in the puzzle text). But if we want to allow such arrangements, we can use [[ fn=enigma.ge(0)) ]] in line 8. However this gives multiple maximal configurations.

      Solution: The round trip is 234 miles long.


      Assuming no three of the castles are collinear, there are 11 possible arrangements, and the maximum value of AS is 84:

      AD = 40, DS = 68, AF = 51, FS = 75; sum = 234
      AS = 84, DF = 77

      The diagonals are divided into 84 = 24 + 60 and 77 = 32 + 45, so the quadrilateral is composed of four Pythagorean triangles:

      (24, 32, 40) = 8×(3, 4, 5)
      (24, 45, 51) = 3×(8, 15, 17)
      (32, 60, 68) = 4×(8, 15, 17)
      (45, 60, 75) = 15×(3, 4, 5)

      There are 4 candidate convex quadrilaterals (which are also cyclic), the one given in the answer, and its reflection in a NW-SE line (so the diagonals are AS = 77, DF = 84).

      The other is:

      AD = 25, DS = 52, AF = 39, FS = 60; sum = 176
      AS = 63, DF = 56

      This can also be mirrored in a NW-SE line to give a further candidate (with diagonals AS = 56, DF = 63).

      These sides also give a concave quadrilateral with diagonals AS = 33, DF = 56, but obviously this does not have the longest possible AS distance for these sides. (In fact this is the only arrangement of sides that gives multiple integer AS values).


      The other 9 candidate solutions are concave. And the largest AS value among these is:

      AD = 13, DS = 75, AF = 40, FS = 84; sum = 212
      AS = 68, DF = 51

      This arrangement is not composed of Pythagorean triangles.


      However, if we allow three of the castles to be collinear then there are an additional 51 possible arrangements to give a total of 62 candidate arrangements. And there is an additional solution where AS = 84, where the round trip is 224 miles long:

      AD = 13, DS = 85, AF = 35, FS = 91; sum = 224
      AS = 84, DF = 48

      This arrangement is composed of two Pythagorean triangles: (13, 84, 85) and (35, 84, 91) = 7×(5, 12, 13).


      The published answer is just: “234 miles”.

      Like

  • Unknown's avatar

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

    Teaser 2487: [Alphametic sum] 

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

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

    THE + TOP + PAPER = TIMES

    and this TEASER is odd.

    What is the number TEASER?

    This puzzle was originally published with no title.

    [teaser2487]

     
    • Jim Randell's avatar

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

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

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

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

      Solution: TEASER = 809205.

      The alphametic expression corresponds to two possible sums:

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

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

      Like

    • Ruud's avatar

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

      Note that this extremely brute force solution requires istr 1.11.1 .

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

      Like

  • Unknown's avatar

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

    Teaser 2481: [Coach trip] 

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

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

    In which state were we sitting in the best seat?

    This puzzle was originally published with no title.

    [teaser2481]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

      There appear to be two possible scenarios:

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

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

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

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

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

      Which means being in the best seat in state 12.

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

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

      The seats are laid out as:

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

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

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

      Which means being in the best seat in state 11.

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

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

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


      If the list of states visited had been given as:

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

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

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

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

      Like

  • Unknown's avatar

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

    Teaser 3294: Four by four 

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

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

    How many pupils are there in the class?

    [teaser3294]

     
    • Jim Randell's avatar

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

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

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

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

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

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

      Solution: There are 19 pupils in the class.

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

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

      Like

    • Frits's avatar

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

      Version 1.

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

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

      Like

      • Frits's avatar

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

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

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

        Like

        • Brian Gladman's avatar

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

          @Frits
          The version on which you comment above was:

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

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

          Like

    • Frits's avatar

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

      Version 2.

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

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

      Like

    • Alex.T.Sutherland's avatar

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

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

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

      Let me know what you think.

      Like

      • Jim Randell's avatar

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

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

        Like

        • Alex.Sutherland's avatar

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

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

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

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

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

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

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

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

                               Regards
                                      A.T.Sutherland.

          Like

          • Jim Randell's avatar

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

            @Alex:

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

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

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

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

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

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

            Like

  • Unknown's avatar

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

    Teaser 2480: [I saw three ships] 

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

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

    How far does Akagi sail before she meets Kaga?

    This puzzle was originally published with no title.

    [teaser2480]

     
    • Jim Randell's avatar

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

      See my comments on: Puzzle #13.

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

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

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

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

      Solution: The ships meet after travelling 80 km.

      Like

  • Unknown's avatar

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

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

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

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

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

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

    What are they?

    This puzzle was originally published with no title.

    [teaser2512]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

      Like

    • ruudvanderham's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 3293: Three sisters 

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

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

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

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

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

    [teaser3293]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

      Jay’s list is:

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

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

      Kay’s list is:

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

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

      Like

    • Jim Olson's avatar

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

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

      Like

      • Jim Randell's avatar

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

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

        Like

  • Unknown's avatar

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

    Teaser 2470: [Peter’s birthday] 

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

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

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

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

    What is Peter’s date of birth?

    This puzzle was originally published with no title.

    [teaser2470]

     
    • Jim Randell's avatar

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

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

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

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

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

      Which was a Thursday.

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

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

      Like

    • Ruud's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 2497: [Measuring jugs] 

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

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

    What are the capacities of the two jugs?

    This puzzle was originally published with no title.

    [teaser2497]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

      There are a couple of shortcuts we could use:

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

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

      Like

    • Frits's avatar

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

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

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

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

      Like

  • Unknown's avatar

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

    Teaser 3292: Doctor Hoo’s TARDSI 

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

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

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

    [teaser3292]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      Like

      • Jim Randell's avatar

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

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

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

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

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

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

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

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

        The parent bipartite graph linking readings is as shown:

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

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

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

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

        This leaves us with the following simplified graph:

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

        Like

    • Frits's avatar

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

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

      Like

      • Frits's avatar

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

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

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

        Like

  • Unknown's avatar

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

    Teaser 2495: [Alphametic] 

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

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

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

    What is the value of THREE?

    This puzzle was originally published with no title.

    [teaser2495]

     
    • Jim Randell's avatar

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

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

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

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

      Solution: THREE = 42911.

      We have:

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

      Like

      • Ruud's avatar

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

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

        Like

    • GeoffR's avatar

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

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

      Like

    • ruudvanderham's avatar

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

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

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

      Like

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