Updates from August, 2026 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:14 am on 11 August 2026 Permalink | Reply
    Tags:   

    Teaser 2390: [Near miss] 

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

    Sam enjoys “alphanumeric” puzzles, which are sums where different letters consistently represent different digits. So I challenged him to find an addition sum where letter substitutions gave:

    SUNDAY + TIMES = TEASER

    I knew that this was impossible, but Sam found a near miss. He found the sum of two five-digit numbers where the letter substitutions gave:

    SUND* + TIMES = TEASER

    The * indicates where I have missed out a consonant.

    What was that consonant?

    This puzzle was originally published with no title.

    [teaser2390]

     
    • Jim Randell's avatar

      Jim Randell 8:15 am on 11 August 2026 Permalink | Reply

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

      We solve the alphametic expression:

      SUNDX + TIMES = TEASER

      where each letter stands for a different digit, except X is the same as one of D, M, N, R, S, T.

      We then determine which of the consonants X is the same as and output the solution(s).

      It runs in 68ms. (Internal runtime is 5.4ms).

      from enigma import (SubstitutedExpression, peek, printf)
      
      # the alphametic sum to solve
      expr = "SUNDX + TIMES = TEASER"
      
      p = SubstitutedExpression.split_sum(
        expr,
        # symbols other than X are distinct
        distinct="ADEIMNRSTU",
        # X is the same as one of the other consonants
        extra=["X in {D, M, N, R, S, T}"],
      )
      
      for s in p.solve(verbose=0):
        X = s['X']
        k = peek(k for k in "DMNRST" if s[k] == X)
        printf("{s}; X={k}", s=p.substitute(s, expr))
      

      Solution: The consonant replaced by the * is S.

      The four possible solutions to the sum SUNDS + TIMES = TEASER are:

      85498 + 17308 = 102806
      87498 + 15308 = 102806
      85398 + 17408 = 102806
      87398 + 15408 = 102806

      U and I are {5, 7} (in some order), and N and M are {3, 4} (in some order). So UN + IM = 127.

      Like

    • Ruud's avatar

      Ruud 10:54 am on 11 August 2026 Permalink | Reply

      import istr
      
      for t, i, m, e, s, a, r in istr.permutations(range(10), 7):
          if len(sund_ := istr(":=teaser") - istr(":=times")) == 5:
              sund_.decompose("SundX")
              if t and s == S and istr("=timesarund").all_distinct() and X in (t, m, s, r, n, d):
                  for letter in "tmsrnd":
                      if istr(f"={letter}") == X:
                          print(f"*={letter.upper()}   {sund_} + {times} = {teaser}")
      
      

      Like

    • Frits's avatar

      Frits 7:03 pm on 11 August 2026 Permalink | Reply

      Optimizing code lead to a manual solution.

      '''
       SUNDX
       TIMES
       ----- +
      TEASER 
      '''
      
      T = 1 # obvious
      E = 0 # as carry + S + T >= 10 and T = 1
      D = 9 # if X + S < 10 then D would have to be 0 as well
      S = 8 # S can be 8 or 9 (carry + S + T >= 10) but D is already 9
      
      # 1 + M + N = S or M + N = 7 
      # U + I + A must be even as A = U + I, M + N = 7 so T + E + S + R + D 
      # must be even as well (sum symbols = 45) meaning R has to be even 
      # --> X has to be even as well, now we know value 7 must be in {U, I, A}
      # U + I >= 10 (as S = 8 and a carry is needed)
      # if sorted(U, I, A) = [x, y, 7] then y + 7 = 10 + x or y - x = 3 and 
      # y >= 5 as x >= 2 so [x, y, 7] is [3, 6, 7] or [2, 5, 7] 
      # that means R can only be resp. 45 - 18 - 7 - 16 and 45 - 18 - 7 - 14 or 
      # resp. 4 or 6. 
      # if R = 4 (and X is 6) then value 6 is not used by symbols S, T, D, E and R
      # so consonants M or N must have value 6. This is not possible as M + N = 7 
      # and T = 1.
      # What remains is R = 6 (and X is 8) so {U, I, A, M, N} = {2, 3, 4, 5, 7}
      # a valid solution is {M, N} = {3, 4}, A = 2 and {U, I} = {5, 7}
      #
      # answer: consonant S
      

      Like

  • Unknown's avatar

    Jim Randell 6:47 am on 9 August 2026 Permalink | Reply
    Tags:   

    Teaser 3333: Hey, diddle, diddle … 

    From The Sunday Times, 9th August 2026 [link] [link]

    Kat and Fidel each held an investment bond. They were over the moon when these matured with different four-digit values (whole numbers of dollars). Unfortunately, a fraction (equivalent to under ten per cent, and the same for both bonds) was deducted as fees, leaving four-digit whole-number nett values. “Fees, what a diddle!” complained Kat. Fidel agreed, adding that in another sense each maturity value was a DIDDLE. Kat was puzzled. Fidel explained that a DIDDLE was a whole number with Different Increasing Digits (left to right) that is DivisibLe Exactly by each digit.

    Later, walking his little dog, Fidel stopped at Withespoon’s “Prancing Cow” pub, where he told his friend, Dishran, these things, but no values. Dishran, an accountant, knew the “fees” fraction and calculated the values with certainty.

    Find the nett values.

    [teaser3333]

     
    • Jim Randell's avatar

      Jim Randell 7:03 am on 9 August 2026 Permalink | Reply

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

      from enigma import (irange, subsets, nconcat, divf, fraction, group, item, printf)
      
      # generate possible k-digit DIDDLEs
      def diddles(k):
        # consider 4 different increasing digits
        for ds in subsets(irange(1, 9), size=k, select='C'):
          # form the number
          n = nconcat(ds)
          # check it is divisible by each of the digits
          if not all(n % d == 0 for d in ds): continue
          # return the number
          yield n
      
      # generate possible (<gross>, <net>, <fee-fraction>) values
      def generate():
        # consider possible gross values (= 4-digit DIDDLEs)
        for G in diddles(4):
          # consider possible fee amounts (less than 10% of G)
          for F in irange(1, divf(G - 1, 10)):
            # return (<gross>, <net>, <fee-fraction)>)
            yield (G, G - F, fraction(F, G))
      
      # group possible values by fee fraction
      g = group(generate(), by=item(2), f=item(0, 1))
      
      # look for a fraction that gives exactly 2 values
      for ((a, b), vs) in g.items():
        if len(vs) == 2:
          printf("fee = {a}/{b} -> vs = {vs}")
      

      Solution: [To Be Revealed]

      Like

    • Ruud's avatar

      Ruud 8:08 am on 9 August 2026 Permalink | Reply

      import peek
      import istr
      import collections
      
      collect = collections.defaultdict(list)
      
      for gross in istr.range(length=4):
          if gross.is_increasing() and all(gross.divided_by(c) for c in gross):
              for fraction in range(11, int(gross)):
                  if gross.is_divisible_by(fraction):
                      collect[fraction].append(gross)
      for fraction, grosses in collect.items():
          if len(grosses) == 2:
              peek(fraction)
              for gross in grosses:
                  nett = gross - gross // fraction
                  peek(nett, gross)
      

      Like

      • Ruud's avatar

        Ruud 8:12 am on 9 August 2026 Permalink | Reply

        `divided_by` could also read `is_divisible_by` which is arguably a bit more clear.

        Like

    • Frits's avatar

      Frits 12:45 pm on 9 August 2026 Permalink | Reply

      I didn’t want to publish a similar solution.

      from math import gcd
      from itertools import combinations
      from functools import reduce
      
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
      
      # 4-digit maturity values for Kat and Fidel
      mvs = [mv for c in combinations(range(1, 10), 4) 
              if all((mv := d2n(c)) % dgt == 0 for dgt in c)]
      
      # assume (minimal) fraction is a / b then k % b = 0 and f % b = 0
      # collect all valid denominators <b> that can be factors of <k> and <f>
      for b in {gcd(k, f) for k, f in combinations(mvs, 2)}:
        # check all fractions a / b
        for a in range(1, b):
          nett = [v - dm[0] for v in mvs if (dm := divmod(a * v, b))[1] == 0 and
                                             dm[0] <= v // 10]
          if len(nett) == 2:
             print("answer:", nett)  
      

      Like

  • Unknown's avatar

    Jim Randell 3:23 pm on 7 August 2026 Permalink | Reply
    Tags:   

    Teaser 2389: [Piles of cards] 

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

    Dan takes a standard pack of playing cards and divides them into three unequal piles. From the first he picks a card at random. It turns out to be black, and indeed the ratio of black to red in that pile was 2 to 1. If he placed that black card in the second (smaller) pile, then the ratio of red to black in that pile would be 4 to 1. If, instead, he placed it in the third pile (more black than red), then the ratio of black to red would be some other whole number to 1.

    How many cards were there in each pile?

    This puzzle was published in The Sunday Times using the style that continued until 26th January 2014 (Teaser 2679).

    This puzzle was originally published with no title.

    [teaser2389]

     
    • Jim Randell's avatar

      Jim Randell 3:24 pm on 7 August 2026 Permalink | Reply

      Here is a straightforward programmed solution.

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

      from enigma import (irange, div, printf)
      
      N = 26  # number of red/black cards
      
      # consider the number of red cards in pile 1
      for r1 in irange(1, N):
        # pile 1 is black:red = 2:1
        b1 = 2 * r1
        if b1 > N: break
      
        # consider the number of black cards in pile 2
        for b2 in irange(0, N - b1):
          # if a black card is added to pile 2, the ratio is black:red = 1:4
          r2 = 4 * (b2 + 1)
          # pile 1 has more cards than pile 2
          (n1, n2) = (b1 + r1, b2 + r2)
          if not (n1 > n2): break
      
          # calculate the number of cards in pile 3
          (b3, r3) = (N - b1 - b2, N - r1 - r2)
          # pile 3 has more black cards than red
          if not (b3 > r3 > 0): continue
          # the numbers of cards in the piles are not equal
          n3 = b3 + r3
          if n3 == n1 or n3 == n2: continue
      
          # if a black card is added to pile 3, the ratio is black:red = k:1
          k = div(b3 + 1, r3)
          # but k is not 4
          if k is None or k == 4: continue
      
          # output solution
          printf("1: {n1} ({b1}b + {r1}r); 2: {n2} ({b2}b + {r2}r); 3: {n3} ({b3}b + {r3}r); k={k}")
      

      Solution: The first pile had 27 cards. The second pile had 19 cards. The third pile had 6 cards.

      The piles are:

      pile 1 = 18 black + 9 red = 27 cards (ratio = 2:1)
      pile 2 = 3 black + 16 red = 19 cards (+ 1 black, ratio = 1:4)
      pile 3 = 5 black + 1 red = 6 cards (+ 1 black, ratio = 6:1)

      There is a second candidate solution:

      pile 1 = 16 black + 8 red = 24 cards (ratio = 2:1)
      pile 2 = 3 black + 16 red = 19 cards (+ 1 black, ratio = 1:4)
      pile 3 = 7 black + 2 red = 9 cards (+ 1 black, ratio = 4:1)

      But this is disqualified as adding a black card to pile 3 brings the black:red ratio to 8:2 = 4:1, but this ratio has to be different from the red:black ratio of pile 2.

      Like

      • Jim Randell's avatar

        Jim Randell 9:38 pm on 7 August 2026 Permalink | Reply

        And using the [[ SubstitutedExpression ]] solver from the enigma.py library gives a run-file that executes in 80ms (and the internal runtime of the generated code is 180µs).

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        --base=27  # allows values = 1 - 26
        --distinct=""
        
        # pile 1 = A black + B red
        # pile 2 = C black + D red
        # pile 3 = E black + F red
        
        # all cards are used
        "A + C + E == 26"
        "B + D + F == 26"
        
        # pile 1 is 2 black to 1 red
        "2 * B = A"
        
        # pile 2 + 1 black is 4 red to 1 black
        "4 * (C + 1) = D"
        
        # pile 3 has more black than red
        "E > F"
        
        # pile 3 + 1 black is k black to 1 red; but k != 4
        "ediv(E + 1, F) != 4"
        
        # pile 1 is bigger than pile 2
        "A + B > C + D"
        
        # and pile 3 is different from 1 and 2
        "E + F not in { A + B, C + D }"
        
        # answer is the size of the piles
        --answer="(A + B, C + D, E + F)"
        
        # [optional] neaten up output
        --template=""
        

        Like

    • Ruud's avatar

      Ruud 3:57 pm on 7 August 2026 Permalink | Reply

      Very brute force, but still running in milliseconds …

      import peek
      
      for p1 in range(3, 52, 3):
          red1 = p1 // 3
          black1 = p1 * 2 // 3
          for p2 in range(4, 52, 5):
              red2 = (p2 + 1) * 4 // 5
              black2 = (p2 + 1) // 5
              for ratio in range(52):
                  if ratio not in (2, 4):
                      for p3 in range(ratio, 52, ratio + 1):
                          red3 = (p3 + 1) * 1 // (ratio + 1)
                          black3 = (p3 + 1) * ratio // (ratio + 1)
                          if p1 + p2 + p3 == 52 and p2 < p1 and black3 - 1 > red3 and len({p1, p2, p3}) == 3 and red1 + red2 + red3 == black1 + black2 + black3 - 2:
                              peek(ratio, p1, p2, p3, red1, black1, red2, black2, red3, black3)
      

      Like

    • Frits's avatar

      Frits 7:21 pm on 8 August 2026 Permalink | Reply

      '''
      E + 1 = k . F (k != 4) 
      27 - A - C = k . (26 - B - D)
      k =  (27 - 2B - C) / (22 - B - 4C) 
      
      so B < 22 - 4C and B < (26 - C) / 2 as k > 1
      
      E > F
      26 - A - C > 26 - B - D  or  -2B - C > -B - 4C - 4  or  B < 3C + 4
      
      5C + 4 < 3B < 3.(22 - 4C)
      5C + 4 equals 66 - 12C if C = 62/17 = 3.65
      '''
      for C in range(1, 4):
        D = 4 * (C + 1)
        # A + B > C + D  or 3B > 5C + 4  
        mnB = (5 * C + 4) // 3 + 1 
        mxB = min((3 * C + 4, 22 - 4 * C, ceil((26 - C) / 2)))
        
        # C even -> B odd (results from k formula)
        if (stpB := 2 -  C % 2) == 2:
          mnB += (mnB % 2 == 0)
        for B in range(mnB, mxB, stpB):
          # E + 1 = k.F        (k != 4) 
          k, r = divmod(27 - (A := 2 * B) - C, 22 - B - 4 * C)
          if r or k == 4 or k < 2: continue
          E = 26 - A - C
          F = 26 - B - D
          if not (E > F > 0): continue
          # three unequal piles
          if (p3 := E + F) in {p1 := A + B, p2 := C + D}: continue
          print(f"answer: {p1}, {p2} and {p3}")
      

      Like

  • Unknown's avatar

    Jim Randell 2:35 pm on 5 August 2026 Permalink | Reply
    Tags:   

    Teaser 2396: [Consecutive numbers] 

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

    Consider the numbers 24 and 25. The first has an even number of factors (8) and the second has an odd number (3).

    Today you need to find a higher pair of consecutive numbers (each less than a million), such that once again the sum of their numbers of factors is odd; the number of factors of their sum is odd; the odd number has an odd number of odd digits; the even number has an even number of even digits.

    What is the odd number of the pair?

    This puzzle was originally published with no title.

    [teaser2396]

     
    • Jim Randell's avatar

      Jim Randell 2:36 pm on 5 August 2026 Permalink | Reply

      We can attack the problem in a straightforward fashion, by just searching for a viable pair of consecutive numbers, but it is not very efficient.

      The following Python program runs in 1.47s (using PyPy).

      from enigma import (irange, tau, tuples, nsplit, printf)
      
      # generate (number, tau) pairs
      def generate(a, b):
        for n in irange(a, b):
          yield (n, tau(n))
      
      # check parity constraint for numbers <ns>
      def check(ns):
        for n in ns:
          # parity of n
          p = n % 2
          # count the number of digits in n, with parity p
          k = sum(1 for d in nsplit(n) if d % 2 == p)
          # the number of digits should have parity p
          if k % 2 != p: return
        # looks good
        return True
      
      # consider consecutive pairs of numbers
      for ((a, ta), (b, tb)) in tuples(generate(25, 999999), 2):
        # the sum of their numbers of factors is odd
        if not ((ta + tb) % 2 == 1): continue
        # the number of factors of their sum is odd
        if not (tau(a + b) % 2 == 1): continue
        # the odd number has an odd number of odd digits
        # the even number has an even number of even digits
        if not check((a, b)): continue
      
        # output solution
        printf("({a}, {b})")
      

      But with a bit of analysis we can do better:

      Divisors of a number come in pairs, so most numbers have an even number of divisors. The exception is when one of the divisor pairs is repeated, (i.e. (d, d)) and so the number itself is a perfect square (i.e. d²).

      The sum of the number of divisors of the numbers is odd, so one of the numbers (n or (n + 1)) must be a perfect square.

      So the numbers are either (x², x² + 1) or (x² − 1, x²).

      And the sum of the numbers must also have an odd number of divisors, so must be an odd perfect square.

      So we either have:

      (n, n + 1) = (x², x² + 1) and
      2x² + 1 = (2y + 1)²

      x² = 2y(y + 1)

      or:

      (n, n + 1) = (x² − 1, x²) and
      2x² − 1 = (2y + 1)²

      x² = 2y(y + 1) + 1

      The following Python program starts by considering increasing y values, and calculates corresponding x values.

      It has an internal runtime of just 548µs.

      from enigma import (irange, inf, isqrt, sq, nsplit, printf)
      
      # check parity constraint for numbers <ns>
      def check(ns):
        for n in ns:
          # parity of n
          p = n % 2
          # count number of digits in n with parity p
          k = sum(1 for d in nsplit(n) if d % 2 == p)
          # the number of digits should also have parity p
          if k % 2 != p: return False
        # looks good
        return True
      
      # consider increasing y values
      for y in irange(1, inf):
        Y = 2 * y * (y + 1)
        # calculate viable x^2 values
        x2 = sq(isqrt(Y + 1))
        if x2 > 999999: break
        # calculate the numbers we are interested in
        if x2 == Y + 1: ns = (x2 - 1, x2)
        elif x2 == Y: ns = (x2, x2 + 1)
        else: continue
        # check and output solution
        if check(ns):
          printf("{ns}")
      

      Solution: The odd number in the pair is 970225.

      The pair of numbers is: (970224, 970225) = (985² − 1, 985²).

      970224 (even) has 4 even digits, and 80 divisors.

      970225 (odd) has 3 odd digits, and 9 divisors.

      The sum of the numbers of divisors is 89 (odd)

      The sum of the numbers, 1940449, also has 9 divisors (odd).


      We can use even more analysis to find larger solutions with the required property efficiently:

      By writing X = 2x, Y = 2y + 1 we can rewrite the equations as:

      X² − 2Y² = ±2

      These are forms of Pell’s equation, that can be solved using the pells.py library [link].

      The following Python program can be used to find much larger solutions to the puzzle (default is the first 10).

      import pells
      from enigma import (sq, nsplit, merge, first, fail, arg, printf)
      
      # check parity constraint for numbers <ns>
      def check(ns):
        for n in ns:
          # parity of n
          p = n % 2
          # count number of digits in n with parity p
          k = sum(1 for d in nsplit(n) if d % 2 == p)
          # the number of digits should also have parity p
          if k % 2 != p: return False
        # looks good
        return True
      
      def solve():
        # consider solutions to the pells equation: X^2 - 2.Y^2 = [+2, -2]
        for (X, Y) in merge(pells.diop_quad(1, -2, k) for k in [+2, -2]):
          # recover the k value (+2 or -2)
          k = X*X - 2*Y*Y
          # calculate the numbers we are interested in
          x2 = sq(X//2)
          if k == -2: ns = (x2, x2 + 1)
          elif k == +2: ns = (x2 - 1, x2)
          else: fail()
          # check and return solution
          if check(ns): yield ns
      
      # find the smallest N solutions
      N = arg(10, 0, int)
      for ns in first(solve(), N):
        printf("{ns}")
      

      Like

    • Ruud's avatar

      Ruud 8:01 am on 6 August 2026 Permalink | Reply

      from functools import cache
      
      
      @cache
      def number_of_digits_with_parity(n, parity):
          return sum(c in '13579' for c in str(n)) if parity else sum(c in '02468' for c in str(n))
      
      
      @cache
      def number_of_factors(n):
          return len(set(x for tup in ([i, n // i] for i in range(1, int(n**0.5) + 1) if n % i == 0) for x in tup))
      
      
      for i1, i2 in zip(range(25, 1000000), range(26, 1000000)):
          if (
              (number_of_factors(i1) + number_of_factors(i2)) % 2 == 1
              and number_of_factors(i1 + i2) % 2 == 1
              and number_of_digits_with_parity(i1, 0) % 2 == 0
              and number_of_digits_with_parity(i2, 1) % 2 == 1
          ):
              print(i1, i2)
      
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 10:13 am on 6 August 2026 Permalink | Reply

        @Ruud: How do you know the smaller number is the even one?

        Perhaps the tests should be:

        number_of_digits_with_parity(i1, i1 % 2) % 2 == i1 % 2 and
        number_of_digits_with_parity(i2, i2 % 2) % 2 == i2 % 2
        

        (In fact, from the recurrence relations for solutions to the corresponding Pell’s equations we can show that solution pairs are always (<even>, <odd>), so a brute force approach won’t miss solutions if it just consider these pairs).

        Like

  • Unknown's avatar

    Jim Randell 6:41 am on 2 August 2026 Permalink | Reply
    Tags:   

    Teaser 3332: As easy as one, two, three 

    From The Sunday Times, 2nd August 2026 [link] [link]

    “telladom shoodlat quoz veedku quoz” means 87.

    “rimjy veedku rimjy ump telladom” means 50.

    Counting is quite easy in this traditional dialect. Words mean different positive whole numbers called “minor” or “major”. All minors have lower values than all majors. In the counting name for a number, two minors never occur in succession, and the majors occur in order of strictly decreasing value (so majors cannot be repeated). A minor followed by a major means the product of those two values (although 1 is never used as a minor here). The value of the whole expression is the sum of all products and all the other individual values.

    So an English-ish example, with all “majors” underlined, would be “thousand six ten six” for 1066.

    What are the values of quoz, rimjy, shoodlat, telladom, ump, veedku (in that order)?

    [teaser3332]

     
    • Jim Randell's avatar

      Jim Randell 8:03 am on 2 August 2026 Permalink | Reply

      Here is a Python solution that finds the required answer.

      It does, however, make the assumption that the minor values are no more than 10, and that the major values are no more than 20. (Which are not unreasonable, given the numbers specified).

      It first determines which symbols can be minor and major, using the sequences given, and then assigns values to each symbol such that the required values are made.

      The following Python program runs in 190ms. (Internal runtime is 108ms).

      from enigma import (
        irange, subsets, tuples, diff, seq_all_different, is_increasing, rev, map2str, printf
      )
      
      # symbols used
      syms = "QRSTUV"
      
      # numbers and their values
      nums = { "TSQVQ": 87, "RVRUT": 50 }
      
      # evaluate <num> using minor/major values from <vm> and <vM>
      def evaluate(num, vm, vM):
        (r, p) = (0, 0)
        for k in num:
          # is it a minor?
          v = vm.get(k)
          if v is not None:
            assert (not p) # can't have 2 minors in a row
            # this is a minor, just remember it
            p = v
          else:
            # this is a major
            v = vM.get(k)
            # if there is a previous minor, multiply it up
            if p: v *= p
            r += v
            p = 0
        # is there is a standalone minor at the end?
        if p: r += p
        return r
      
      # check given example
      assert (evaluate('M6X6', { '6': 6 }, { 'M': 1000, 'X': 10 }) == 1066)
      
      # find min/maj values that give assignments in <nums>
      def solve(mins, majs, nums, u_min=10, u_maj=20):
        # consider possible increasing max minor values
        for m in irange(len(mins) + 1, u_min):
          # allocate the remaining minor values
          for ss in subsets(irange(2, m - 1), size=len(mins) - 1, fn=list):
            ss.append(m)
            # allocate minor values
            for ms in subsets(ss, size=len, select='P'):
              vm = dict(zip(mins, ms))
      
              # now choose major values
              for Ms in subsets(irange(m + 1, u_maj), size=len(majs)):
                vM = dict(zip(majs, Ms))
                # check the sums
                if not all(evaluate(num, vm, vM) == n for (num, n) in nums.items()): continue
                # return the minor/major values
                yield (vm, vM)
      
      # check minor/major assignments are compatible with the specified numbers nums
      def check(num, mins, majs):
        # there are never 2 minor symbols together
        if any(mins.issuperset(t) for t in tuples(num, 2)): return False
        # majors appear in order
        if not seq_all_different(diff(num, mins)): return False
        # looks OK
        return True
      
      # choose which of the symbols are minor
      for mins in subsets(sorted(set(syms)), fn=set):
        majs = diff(syms, mins)
        # check for viable minor/major assignment
        if not all(check(num, mins, majs) for num in nums.keys()): continue
      
        # choose an ordering for the major values (largest to smallest)
        for mvs in subsets(majs, size=len, select='P'):
          # check that majors appear in order in each sum
          if not all(is_increasing((mvs.index(x) for x in diff(num, mins)), strict=1) for num in nums.keys()): continue
          # find values for minor/major symbols
          for (vm, vM) in solve(sorted(mins), rev(mvs), nums):
            # output solution
            printf("minor = {vm}; major = {vM}", vm=map2str(vm), vM=map2str(vM))
      

      Solution: The values are: quoz = 5, rimjy = 3, shoodlat = 16, telladom = 2, ump = 6, veedku = 10.

      The minors are: quoz, rimjy, telladom (values: 5, 3, 2).

      The majors are: shoodlat, ump, veedku (values: 16, 6, 10).

      The numbers are then:

      “telladom shoodlat quoz veedku quoz” = (2 × 16) + (5 × 10) + 5 = 87.
      “rimjy veedku rimjy ump telladom” = (3 × 10) + (3 × 6) + 2 = 50.


      With some analysis we can show that the above is the only solution.

      For the sequences TSQVQ and RVRUT, using the fact that majors cannot be repeated in a sequence we see Q and R must be minors, and we cannot have 2 consecutive minors, so S, U, V must be majors. Which only leaves T.

      If T is a major, then, as majors are strictly decreasing in each sequence we have:

      T > S > V > U > T

      which is not possible, and so T must be a minor.

      (In my program above, these assignments are determined by considering the symbol sequences given).

      The numbers are therefore:

      (T×S) + (Q×V) + Q = 87
      (R×V) + (R×U) + T = 50

      From these we can determine that T ∈ [2..8], and so we can perform an exhaustive search to find the required answer.

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

      from enigma import (irange, divisors_pairs, decompose, div, printf)
      
      # minor = QTR; major = S > V > U
      #
      # R(V + U) + T = 50
      # T.S + Q(V + 1) = 87
      
      # solve: R(V + U) + T = 50
      for T in irange(2, 8):
        for (R, UV) in divisors_pairs(50 - T):
          if R < 2 or R == T: continue
          for (U, V) in decompose(UV, 2, increasing=1, sep=1, min_v=max(T, R) + 1):
      
            # solve T.S + Q(V + 1) = 87
            for Q in irange(2, U - 1):
              if Q == T or Q == R: continue
              N = 87 - Q * (V + 1)
              if N < T * (V + 1): break
              S = div(N, T)
              if S is None or not (S > V): continue
      
              # output solution
              printf("minor = (T={T}, R={R}, Q={Q}); major = (S={S}, V={V}, U={U})")
      

      Like

      • Frits's avatar

        Frits 10:47 am on 3 August 2026 Permalink | Reply

        @Jim,

        “(although 1 is never used as a minor here)”. I read this to only count when dealing with a product. This leaves value 1 still possible for a trailing minor (although in this teaser after a little analysis this is also impossible)

        Like

        • Jim Randell's avatar

          Jim Randell 11:21 am on 3 August 2026 Permalink | Reply

          @Frits: Could be. I just read it as meaning that 1 does not occur as a minor in this puzzle. (Although you would presumably have to have a minor that corresponds to 1 if you want to be able to start counting at 1). But as you say, in this puzzle there is no minor that does not appear as part of a product, so we know none of the minors under consideration can be 1.

          Like

    • Alex.T.Sutherland's avatar

      Alex.T.Sutherland 6:52 pm on 7 August 2026 Permalink | Reply

      Method:-
      1. Define minor = 0 and major =1. Pattern = binary order of the minor/major
      2. Generate a 32×5 binary matrix for numbers 0 to 31.
      3. Remove the rows containing adjacent zeros to give a matrix B (13×5).
      4. Considering the number 50.
      It must have the pattern 0 1 0 1 x.
      (The 1st and 3rd values are equal therefore cannot be a major hence are minors.
      The 2nd and 4th must be a major ,can’t be adjacent to a minor.
      The ‘x’ could be either but is less than the forth value.)
      5. Using this pattern find the list of 5 numbers (using the appropriate rules)
      that give 50. (I have a list of 20).Only needs to be calculated once.(List_50)

      6. Considering The number 87.
      The matrix B can be reduced by keeping only those where with the third and
      fifth digits are equal.
      B is reduced to 8×5 patterns from the 13×5.
      7. Iterate each of the 8 pattern lists with the List_50 using the numbers common to both
      as the matching criteria . ie N87(1) = N50(5) & N87(4)= N50(2).
      A unique solution is found on the first iteration.

      8. The answer gives the sum of the digits in both 50 & 87 as 62.
      The maximum minor value is a single digit.
      Time:- ~ 26ms

      Like

  • Unknown's avatar

    Jim Randell 3:03 pm on 31 July 2026 Permalink | Reply
    Tags:   

    Teaser 2409: [Five fours] 

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

    In this addition sum, digits have been replaced consistently by letters, with different letters for different digits:

    What is the number NOW?

    This puzzle was originally published with no title.

    [teaser2409]

     
    • Jim Randell's avatar

      Jim Randell 3:04 pm on 31 July 2026 Permalink | Reply

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

      It runs in 78ms. (Internal runtime of the generated code is 1.4ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "FOUR * 32 = TWENTY"
      
      --answer="NOW"
      

      Or you can just call the solver directly from the command line:

      % python3 -m enigma SubstitutedExpression "FOUR * 32 = TWENTY" --answer="NOW"
      (FOUR * 32 = TWENTY) (NOW)
      (6379 * 32 = 204128) (130) / E=4 F=6 N=1 O=3 R=9 T=2 U=7 W=0 Y=8
      NOW = 130 [1 solution]
      

      Or we can keep the sum as an addition sum (as it is originally presented), and use the [[ SubstitutedExpression.split_sum ]] solver on it.

      The following (slightly longer) run-file executes in 76ms. (Internal runtime of the generated code is 260µs).

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "FOUR + FOUR + FOUR0 + FOUR0 + FOUR0 = TWENTY"
      
      --distinct="EFNORTUWY"
      --literal="0"
      --answer="NOW"
      

      Solution: NOW = 130.

      Like

    • Ruud's avatar

      Ruud 5:06 pm on 31 July 2026 Permalink | Reply

      import peek
      import istr
      
      for four in istr.range(length=4):
          twenty = 32 * four
          if len(twenty) == 6 and twenty[0] == twenty[4] and (twenty[1:] | four).all_distinct():
              now = twenty[3] | four[1] | twenty[1]
              peek(now, four, twenty)
      

      Like

  • Unknown's avatar

    Jim Randell 10:20 am on 28 July 2026 Permalink | Reply
    Tags:   

    Teaser 2412: [Triangular lawn] 

    From The Sunday Times, 14th December 2008 [link]

    I have a triangular lawn with sides a whole number of metres in length. Its perimeter is 54 metres. A circular water feature is equidistant from each of the three sides. I have measured the distance from the centre of the water feature to each of the corners of the lawn: in two cases, the distance is a whole number of metres.

    What is the length of the shortest side of the lawn?

    This puzzle was originally published with no title.

    [teaser2412]

     
    • Jim Randell's avatar

      Jim Randell 10:20 am on 28 July 2026 Permalink | Reply

      Starting with a triangle with sides a, b, c. Then the circular pond is concentric with the incircle of the triangle, so we are interested in the distances from the incentre (= I) to the vertices of the triangle (= u, v, w).

      The semi-perimeter (= s) of the triangle is given by:

      s = (a + b + c) / 2

      And the lengths of the sides of the triangle can be written:

      a = y + z
      b = x + z
      c = x + y

      s = x + y + z
      x = s − a
      y = s − b
      z = s − c

      And so the distances we are interested in are given by:

      u = hypot(r, s − a)
      v = hypot(r, s − b)
      w = hypot(r, s − c)

      We can calculate area (= A) of the triangle using Heron’s formula:

      A = √(s (s − a) (s − b) (s − c))

      And the inradius (= r) can then be calculated:

      r = A / s

      The following Python program considers possible sides of the triangle (a, b, c), and looks for scenarios where 2 of the distances from the incentre to the vertices are whole numbers.

      It runs in 76ms. (Internal runtime is 665µs).

      from enigma import (Rational, decompose, sq, is_square, sqrt, seq2str, printf)
      
      Q = Rational()
      
      # semi-perimeter
      s = 27
      # consider possible sides of the triangle (a, b, c); a + b + c = 2s
      for (a, b, c) in decompose(2*s, 3, increasing=1, sep=0):
        # if the area of the triangle is A, use Heron's formula to calculate A^2
        A2 = s * (s - a) * (s - b) * (s - c)
        # check for valid triangles
        if not (A2 > 0): continue
        # inradius = r, r = A/s, calculate r^2
        r2 = Q(A2, sq(s))
      
        # calculate the squares of the distances from the incentre to the vertices
        d2s = tuple(r2 + sq(s - v) for v in (a, b, c))
      
        # and look for situations where exactly 2 of these are perfect squares
        k = sum(1 for d2 in d2s if is_square(d2))
        if k != 2: continue
      
        # output solution
        ds = tuple(sqrt(d2) for d2 in d2s)
        printf("a={a} b={b} c={c} -> A2={A2} r2={r2} -> d2s={d2s} ds={ds}", d2s=seq2str(d2s), ds=seq2str(ds))
      

      Solution: The shortest side of the lawn is 6 m.

      And the other two sides are 24 m each. (So the triangle is isosceles).

      Giving distances from the incentre to the vertices of 4 m, 4 m, 21.166 m (= 8√7).

      Like

      • Jim Randell's avatar

        Jim Randell 11:48 am on 2 August 2026 Permalink | Reply

        If at least one of the (r² + (sv)²) is a square number (to give a whole number root), then it follows that r² must be an integer, and so each of u², v², w² is also a whole numbers.

        And we can simplify the calculations of u², v², w² to be just in terms of a, b, c:

        u² = (s − a)bc / s
        v² = (s − b)ca / s
        w² = (s − c)ab / s

        The program can then be simplified slightly to:

        from enigma import (decompose, div, tuples, is_square, sqrt, seq2str, printf)
        
        # semi-perimeter
        s = 27
        # consider possible sides of the triangle (a, b, c); a + b + c = 2s
        for (a, b, c) in decompose(2*s, 3, increasing=1, sep=0):
          # check for valid triangles
          if not (a + b > c): continue
        
          # calculate the squares of the distances from the incentre to the vertices
          d2s = tuple(div((s - i) * j * k, s) for (i, j, k) in tuples((a, b, c), 3, circular=1))
          if None in d2s: continue
        
          # look for situations where exactly 2 of these are perfect squares
          k = sum(1 for d2 in d2s if is_square(d2))
          if k != 2: continue
        
          # output solution
          ds = tuple(sqrt(d2) for d2 in d2s)
          printf("a={a} b={b} c={c} -> d2s={d2s} ds={ds}", d2s=seq2str(d2s), ds=seq2str(ds))
        

        Like

    • Frits's avatar

      Frits 1:54 pm on 28 July 2026 Permalink | Reply

      Doing most of the checks in decompose().

      from math import prod
      
      # decompose number <t> into <k> non-decreasing numbers and semi-perimeter logic
      def decompose(t, k, m=0, sp=0, s=[]):
        if k == 1:
          if not s or t >= s[-1]:
            s.append(t)
            # r^2 = (sp - a) * (sp - b) * (sp - c) / sp must be a positive integer
            p = prod(sp - x for x in s)
            if p > 0 and p % sp == 0:
              yield s, p // sp
        else:
          for n in range(m, (t // k) + 1):
            yield from decompose(t - n, k - 1, n, sp, s + [n])
      
      # semi-perimeter
      s = 27
      # consider possible sides of the triangle (a, b, c); a + b + c = 2s
      for (a, b, c), r2 in decompose(2 * s, 3, 1, s):
        # calculate the squares of the distances from the incentre to the vertices
        d2s = [r2 + (s - v)**2 for v in (a, b, c)]
      
        # and look for situations where exactly 2 of these are perfect squares
        if sum(round(rt := d2**.5) == rt for d2 in d2s) != 2: continue
      
        print("answer:", min(a, b, c))
      

      Like

  • Unknown's avatar

    Jim Randell 6:22 am on 26 July 2026 Permalink | Reply
    Tags:   

    Teaser 3331: Salarium solace 

    From The Sunday Times, 26th July 2026 [link] [link]

    During his last campaign in Germany, Centurion Ulpius lost more than one-third of his original 80 legionaries. The remaining number of men was prime. On their return to Rome, the Legate ordered his Prefect to share out the leftover salt stock to Ulpius’s remaining men, as a reward for their bravery.

    The Prefect must ensure each man receives an identical amount. Using his scales he could accurately halve any quantity of salt. He divided the initial amount into two equal piles repeatedly until the number of piles exceeded the number of men. The men then received a pile each. The remaining piles were again each halved and distributed in the same way until only one pile remained.

    The Prefect returned home with this pile, one in 16,384 of the original stock.

    How many of Ulpius’s men returned to Rome?

    [teaser3331]

     
    • Jim Randell's avatar

      Jim Randell 6:25 am on 26 July 2026 Permalink | Reply

      Here is a constructive solution that follows the procedure for primes up to 53.

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

      from enigma import (primes, printf)
      
      # target fraction (1/F)
      F = 16384
      
      # perform the procedure
      def solve(n, F=F):
        # initial number of piles (= k), and fraction for each pile (= 1/f)
        k = f = 1
        while f < F:
          # increase the number of piles until it exceeds the number of men
          while not (k > n):
            k *= 2
            f *= 2
          # n piles are distributed to the men
          k -= n
          # are we done?
          if k == 1: return f
      
      # consider possible remaining number of men (= n)
      for n in primes.between(2, 53):
        f = solve(n)
        if f == F:
          printf("n={n} -> f={f}")
      

      Solution: 43 of Ulpius’ men returned to Rome.

      Each man received: 1/64 + 1/256 + 1/512 + 1/1024 + 1/2048 + 1/4096 + 1/16384 = 381/16384 of the original amount of salt.

      So the total amount of salt distributed to the men was: 43 × 381/16384 = 16383/16384. Leaving 1/16384 remaining.


      Manually:

      The salt is divided into 16384 equal portions, of which 1 is retained.

      So the remaining 16383 portions are divided equally between the (prime number) of men.

      It is straightforward to determine the prime factorisation of 16383 (= 2^14 − 1):

      2^14 − 1 = (2^7 − 1) × (2^7 + 1) = 127 × 129

      We can do trial division of primes up to 11 on each factor (or note that 127 is a Mersenne prime, and 129 is clearly divisible by 3), so the prime factorisation of 16383 is:

      16383 = 127 × 43 × 3

      Of these prime factors only 3 and 43 are viable candidate primes for the number of men (i.e. less than (2/3) × 80 = 53 + 1/3).

      For each case we can calculate the multiplicative order of 2 modulo n (we can use the procedure described in the puzzle text to do this, and count the number of times the piles are divided):

      m_order(2, 3) = 2
      m_order(2, 43) = 14

      And only 43 gives the required answer of 14.

      Like

      • Jim Randell's avatar

        Jim Randell 8:50 am on 26 July 2026 Permalink | Reply

        For n men we are are looking to find the multiplicative order of 2 modulo n, i.e. the smallest k such that 2^k mod n = 1 [@wikipedia]. And we are interested in the case where k = 14 (as 2^14 = 16384).

        This gives rise to a shorter program:

        from enigma import (primes, irange, printf)
        
        # target fraction (1/2^M)
        M = 14  # 2^14 = 16384
        
        # find k st. 2^k mod n = 1
        def solve(n, m=M):
          for k in irange(1, m):
            if pow(2, k, n) == 1:
              return k
        
        # consider possible number of men (= n)
        for n in primes.between(2, 53):
          k = solve(n)
          if k == M:
            printf("n={n} -> k={k}")
        

        or even shorter:

        from enigma import (primes, irange, printf)
        
        # target fraction (1/2^M)
        M = 14
        F = 2**M
        
        # consider possible number of men (= n)
        for n in primes.between(2, 53):
          if F % n == 1 and all(pow(2, k, n) != 1 for k in irange(1, M - 1)):
            printf("n={n} -> k={M}")
        

        See also: Teaser 2406.

        Like

    • Frits's avatar

      Frits 1:16 pm on 26 July 2026 Permalink | Reply

      My first attempt was pretty similar to Jim’s first program.
      I rewrote it a little bit in order not to calculate powers for every prime number.

      from math import log
      
      N = 16384
      
      # remaining number of men (< 2/3) was prime
      P = {3, 5, 7}
      P |= {2} | {x for x in range(11, (2 * 80 - 1) // 3 + 1, 2) 
                  if all(x % p for p in P)}
      
      # powers of 2 (only highest power > remaining number of men)
      pow2 = [(i, 2**i)for i in range(1, 7)]
      target = log(N, 2) # assuming N is a power of 2
      
      # return first power to overshoot the prime number
      nxt = lambda n, p: next((i, pow) for i, pow in pow2 if n * pow > p) 
      
      # remaining number of men
      for p in P:
        n, k = 1, 0
        # perform repeated doubling of piles
        while k < target:
          k_, n_ = nxt(n, p)
          k += k_
          
          # until only one pile remained
          if (n := n * n_ - p) == 1: break
        
        # check if only one pile remained
        if k == target and n == 1:
          print(f"answer: {p}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:07 am on 24 July 2026 Permalink | Reply
    Tags:   

    Teaser 2426: [Magic squares] 

    From The Sunday Times, 22nd March 2009 [link]

    My grandson has learnt about 3 × 3 Magic Squares (tables of nine different positive whole numbers with each row of three, column of three and diagonal of three adding to the same sum). To illustrate, he placed 100 in the centre of a 3 × 3 table and 1 somewhere else in the table.

    He asked me to choose any other number lower than 200 and said he would place it in the table and complete it to make a Magic Square.

    This would have been possible for most numbers, but I unkindly chose my age, knowing the task would then be impossible.

    How old am I?

    This puzzle was originally published with no title.

    This completes the archive of Teaser puzzles from 2009. There is now a complete archive of Teaser puzzles from January 2009 to the most recent puzzle published in July 2026 (this represents 916 puzzles), as well as 454 earlier Teaser puzzles.

    [teaser2426]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 24 July 2026 Permalink | Reply

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

      It runs in 108ms. (Internal runtime is 34ms).

      from enigma import (SubstitutedExpression, irange, union, diff, seq2str, printf)
      
      #  A B C
      #  D E F
      #  G H I
      
      # possible numbers in the grid
      N = 199
      digits = irange(1, N)
      
      # we have E = 100
      # 1 is either placed in a corner (A) or edge (B)
      
      # magic constant is 3E = 300
      eqs = [
        "A + B + C == 300", # row 1
        "D + E + F == 300", # row 2
        "G + H + I == 300", # row 3
        "A + D + G == 300", # col 1
        "B + E + H == 300", # col 2
        "C + F + I == 300", # col 3
        "A + E + I == 300", # diag 1
        "C + E + G == 300", # diag 2
      ]
      
      # consider placing 1 at A or B
      for q in "AB":
      
        # make a puzzle for this magic square
        p = SubstitutedExpression(
          eqs,
          base=N + 1, digits=digits,
          s2d={ 'E': 100, q: 1 },
          verbose='',
        )
      
        # collect all numbers that can appear in the square
        ns = union(s.values() for s in p.solve())
      
        # most numbers should be possible
        if not (2 * len(ns) > N - 2): continue
      
        # output impossible numbers
        xs = diff(digits, ns)
        printf("{q}=1 -> impossible = {xs}", xs=seq2str(xs))
        printf()
      

      The 1 must be placed in an edge cell (i.e. not a corner cell), and then the following numbers cannot appear in a square:

      34, 67, 133, 166

      (Note that these form two pairs (34, 166) and (67, 133), each with a sum of 200).

      The most likely of these to be the age of someone who has a grandson interested in magic squares (so, maybe aged around 10), is 67.

      Solution: The setter is 67.

      Like

    • Frits's avatar

      Frits 5:11 pm on 25 July 2026 Permalink | Reply

      #  A B C
      #  D E F
      #  G H I
       
      # assume A = 1 or B = 1
      # A = 1 is invalid as it causes both C < 100 and G < 100 and C+E+G < 300
      # B = 1 leads to 
      
      #   A       1    299 - A
      # 399-2A   100   2A - 199
      # A - 99   199   200 - A 
      
      used = set()
      for a in range(101, 199):
        ns = {a, 1, 299 - a, 399 - 2 * a, 100, 2 * a - 199, a - 99, 199, 200 - a}
        # nine different positive whole numbers 
        if len(ns) == 9:
          used |= ns
      
      print(f"answer: {''.join(str(n) for n in range(45, 100) if n not in used)}")    
      

      Like

  • Unknown's avatar

    Jim Randell 7:02 am on 22 July 2026 Permalink | Reply
    Tags: , ,   

    Teaser 2422: [Water tank] 

    From The Sunday Times, 22nd February 2009 [link]

    Farmer Barfoot is building a concrete tank to conserve rain water. The tank has a horizontal rectangular base, vertical walls and an open top, the internal dimensions being whole numbers or feet. The contract price is £1 per sq ft of inside wall area, £2 per sq ft of inside floor area.

    For the volume required he choose dimensions that give the lowest possible overall cost. This cost will be a number of pounds whose digits are consecutive and in order (I do not know whether they are in increasing or decreasing order).

    What is the volume of the tank in cubic feet?

    This puzzle was originally published with no title.

    There are now 4000 puzzles posted between Enigmatic Code and S2T2.

    [teaser2422]

     
    • Jim Randell's avatar

      Jim Randell 7:03 am on 22 July 2026 Permalink | Reply

      I think the wording of this puzzle is poorly chosen.

      But if we ignore the condition that the dimensions of the tank must be an integer number of feet until we get to the end, then we can solve it in the way I think the setter intended.

      We start by considering tanks that have a minimal cost for a given volume:

      For a tank with base x by y and a height z the cost for the tank is given by:

      S = 2(x + y)z + 2xy

      S = 2(xy + xz + yz)

      (This assumes the price is continuous, so fractional square feet of wall/floor are charged proportionally).

      Applying the AM-GM inequality [ @wikipedia ] we get:

      (xy + xz + yz)/3 ≥ (xy . xz . yz)^(1/3)

      xy + xz + yz ≥ 3 (xyz)^(2/3)

      with equality when x = y = z.

      The total cost of the tank is therefore:

      S = 2(xy + xz + yz) ≥ 6 (xyz)^(2/3)

      with equality when x = y = z.

      If the volume of the tank is fixed at a value V:

      V = xyz

      we see the cost of the tank is:

      S = 2(xy + xz + yz) ≥ 6 V^(2/3)

      and we achieve the minimum cost when x = y = z, i.e. when the tank is a perfect cube.

      And the cost is then:

      S = 6x²

      (This is the same as showing for cuboids of a given volume the minimum surface area possible is achieved with a cube).

      We now introduce the fact that the sides of the tank are all whole numbers.

      The problem then is to find a positive integer x (which gives a volume of x³) where 6x² is a sequence of consecutive digits (in ascending or descending order). (We ignore the trivial case where the cost is a single digit).

      The following Python program runs in 66ms. (Internal runtime is 345µs).

      from enigma import (irange, tuples, nconcat, rev, div, is_square, cb, printf)
      
      # solve for a cost of S
      def solve(S):
        x = is_square(div(S, 6))
        if x is not None:
          V = cb(x)
          dims = (x,) * 3
          printf("vol = {V}; cost = {S}; dims = {dims}")
      
      digits = list(irange(0, 9))
      
      # choose 2-10 digits
      for k in irange(2, 10):
        for ds in tuples(digits, k):
          if ds[0] != 0: solve(nconcat(ds))
          if ds[-1] != 0: solve(nconcat(rev(ds)))
      

      Unfortunately there are two possible solutions:

      % python3 teaser2422b.py
      vol = 27; cost = 54; dims = (3, 3, 3)
      vol = 13824; cost = 3456; dims = (24, 24, 24)
      

      So, even solving the puzzle in the intended way gives 2 viable answers (a quite small tank, and a very large tank). Although one of these could have been eliminated by placing restrictions on the size or cost of the tank.

      The published answer is the larger one (which has a longer run of consecutive digits in the cost):

      Solution: The volume of the tank is 13824 cu ft.


      However, this is not how I originally read the puzzle.

      As the constraint that the dimensions of the tank are whole numbers is introduced first I considered that we are only dealing with tanks that have integer dimensions, and this means that the lowest possible cost for a given tank is not necessarily when the tank is a cube. (And also we don’t have to assume the cost of fractional areas is charged proportionally).

      For example, if we suppose Farmer Barfoot is looking for a tank to hold 1175 cu ft, then there are the following possible integer dimensioned tanks:

      1 × 1 × 1175; cost = 4702
      1 × 5 × 235; cost = 2830
      1 × 25 × 47; cost = 2494
      5 × 5 × 47; cost = 990

      So, for a tank holding exactly 1175 cu ft, the best price we can manage is £ 990.

      But an 9 ft × 11 ft × 12 ft tank would cost £ 678 (a sequence of consecutive digits) and hold 1188 cu ft, so be cheaper and slightly larger (and is a reasonable size).

      And it is not possible to build a tank costing less, with a capacity of at least 1175 cu ft.

      So this seems like 1188 cu ft would be a satisfactory solution to the puzzle.

      I checked volumes up to 500,000 cu ft, and found the following solutions where the cost is at least 2 digits and consists of consecutive digits in order:

      vol = 2: cost = 10 -> (1, 1, 2)
      vol = 12: cost = 32 -> (2, 2, 3)
      vol = 27: cost = 54 -> (3, 3, 3)
      vol = 40: cost = 76 -> (2, 4, 5)
      vol = 45: cost = 78 -> (3, 3, 5)
      vol = 200: cost = 210 -> (5, 5, 8)
      vol = 1188: cost = 678 -> (9, 11, 12)
      vol = 13824: cost = 3456 -> (24, 24, 24)
      

      So, my initial reading of the puzzle throws up even more candidate solutions. Although a restriction on the cost or size of the tank could narrow these down to a single solution.

      Like

  • Unknown's avatar

    Jim Randell 8:07 am on 19 July 2026 Permalink | Reply
    Tags:   

    Teaser 3330: Bermuda triangles 

    From The Sunday Times, 19th July 2026 [link] [link]

    My local estate agent was advertising a house named “Bermuda” which had a garden in the shape of a right-angled triangle containing a sundial. I went to view the property and it had nine straight and narrow footpaths. There was a path along each edge of the garden, one from each corner to the sundial and one from each edge to the sundial. Each of the latter paths was perpendicular to the edge it joined. Each path was a different whole number of feet in length, the longest being 500 feet and the two shortest being 36 and 77 feet. These two paths did not join the longest and were the only ones that were not a multiple of five feet.

    What was the combined length of the nine footpaths?

    [teaser3330]

     
    • Jim Randell's avatar

      Jim Randell 8:15 am on 19 July 2026 Permalink | Reply

      The paths are laid out as follows:

      The shortest paths do not touch the hypotenuse, so these must be x and y (which are 36 and 77 in some order), and we can immediately calculate u (= hypot(x, y)).

      And (a, b, c) is the right-angled triangle, and the longest path (= c) is the hypotenuse. As all sides must be multiples of 5, we can look for triangles with a hypotenuse of 100 and multiply the side lengths by 5.

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

      from enigma import (sum_of_squares, sq, permute, ihypot, triangle_iheight, item, printf)
      
      # find the sides of the triangle (a, b, c)
      h = 100  # = 500 / 5
      for (a, b) in sum_of_squares(sq(h), min_v=1):
        (a, b, c) = (a * 5, b * 5, h * 5)
      
        # the two shortest paths do not join the hypotenuse
        # so position S at (x, y)
        for (x, y) in permute([(36, 77)]):
          # we can calculate the paths to the corners (u, v, w)
          vs = (u, v, w) = (ihypot(x, y), ihypot(y, a - x), ihypot(x, b - y))
          if None in vs or u % 5 > 0 or v % 5 > 0 or w % 5 > 0: continue
          # calculate the length of the remaining perpendicular path
          z = triangle_iheight(c, v, w)
          if z is None or z % 5 > 0: continue
          # calculate the total path length
          (es, ps) = ((a, b, c), (x, y, z))
          ns = es + vs + ps
          t = sum(ns)
          # check puzzle conditions hold (different lengths, shortest 2 and longest 1)
          if not (len(set(ns)) == 9 and item(0, 1, -1)(sorted(ns)) == (36, 77, 500)): continue
      
          # output solution
          printf("t = {t} [edge = {es}; corner = {vs}; perpendicular = {ps}]")
      

      Instead of using [[ triangle_iheight() ]] we can consider the big triangle to be made up from the three triangles with altitudes x, y, z. Then we have:

      ab/2 = ay/2 + bx/2 + cz/2

      z = (ab − ay − bx)/c

      So line 15 can be:

      z = div(a * b - a * y - b * x, c)
      

      Solution: The combined length of the 9 paths is 2163 ft.

      The paths around the edge of the garden have lengths 300 ft, 400 ft, 500 ft. (And so the triangle is a scaled-up (3, 4, 5) triangle).

      The paths to the corners of the triangle have lengths 85 ft, 275 ft, 325 ft.

      And the perpendicular paths have lengths 36 ft, 77 ft, 165 ft.

      (In the diagram the assignments are: a = 300, b = 400, c = 500; u = 85, v = 275, w = 325; x = 36, y = 77, z = 165).

      Like

      • Frits's avatar

        Frits 3:05 pm on 22 July 2026 Permalink | Reply

        @Jim, min_v can be probably set to 8 to that the resulting a and b are both greater than 36.

        Like

        • Jim Randell's avatar

          Jim Randell 9:28 am on 23 July 2026 Permalink | Reply

          @Frits: When I wrote the code I was just worried about eliminating the 0² + 100² = 100² solution. But you are right, as we already know that the shortest paths are 36 and 77 we can start from min_v=16.

          Like

    • Alex T Sutherland's avatar

      Alex T Sutherland 10:34 am on 21 July 2026 Permalink | Reply

      Found that there are 3 Pythagorean triangles with a hypotenuse = 500.
      One set can be discounted,the sides not divisible by 5.(obvious)
      Another set whose vertices to sundial lengths are not divisible by 5
      can be eliminated.(requires length calc)
      The remaining set satisfies the requirements.
      The third normal (36,77,?) can be found by equating the sum of the areas
      of 3 triangles to that of the garden triangle.(both equal).
      The answer I have for the total path length is a 4 digit number with the
      following characteristic:-
      T = abcd
      3*(ab) = (cd)
      Time < 1ms ( Mostly finding the distances between known points ).

      Like

    • Ellen Napier's avatar

      Ellen Napier 2:43 pm on 23 July 2026 Permalink | Reply

      Seems like the last sentence of the puzzle statement is redundant.

      Like

  • Unknown's avatar

    Jim Randell 8:57 am on 17 July 2026 Permalink | Reply
    Tags:   

    Teaser 2423: [PIN combinations] 

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

    Joe could not remember his six-digit PIN, but he did remember that it used the digits 1 to 6 in some order. He tried the following four without success:

    6 1 2 5 3 4
    4 6 5 3 2 1
    2 3 4 1 6 5
    3 2 6 5 4 1

    Actually, more than one digit in each of these numbers was in the correct place. Had Joe known this, he would have needed to try only a few more numbers to find his PIN.

    How many more numbers did Joe need to try?

    This puzzle was originally published with no title.

    [teaser2423]

     
    • Jim Randell's avatar

      Jim Randell 8:58 am on 17 July 2026 Permalink | Reply

      Presumably the puzzle is asking how many possible combinations remain after the initial 4 attempts.

      This Python program runs in 64ms. (Internal runtime is 820µs).

      from enigma import (subsets, printf)
      
      # candidates already tried
      tries = [
        (6, 1, 2, 5, 3, 4),
        (4, 6, 5, 3, 2, 1),
        (2, 3, 4, 1, 6, 5),
        (3, 2, 6, 5, 4, 1),
      ]
      
      # start with all orderings
      ss = list(subsets((1, 2, 3, 4, 5, 6), size=6, select='P'))
      
      # count the number of correct positions
      def count(xs, ys):
        return sum(1 for (x, y) in zip(xs, ys) if x == y)
      
      # more than one of the digits in each candidate was in the correct position
      for ts in tries:
        ss = list(ns for ns in ss if 1 < count(ns, ts) < 6)
      
      # output solution
      printf("remaining candidates = {n}", n=len(ss))
      printf("-> {ss}")
      

      Solution: There are 3 combinations remaining.

      The remaining candidates are:

      2 6 4 5 3 1
      4 3 2 5 6 1
      6 3 4 5 2 1

      In each case, each of the initial 4 attempts was correct for exactly 2 digits.

      Like

    • Ruud's avatar

      Ruud 4:17 pm on 17 July 2026 Permalink | Reply

      import istr
      
      
      def matches(n0, n1):
          return sum(i0 == i1 for i0, i1 in zip(n0, n1))
      
      
      tries = istr(["612534", "465321", "234165", "326541"])
      for n0 in istr.permutations(range(1, 7), join=True):
          if all(2 <= matches(n0, n1) <= 5 for n1 in tries):
              print(n0)
      

      Like

    • Frits's avatar

      Frits 12:54 pm on 21 July 2026 Permalink | Reply

      from itertools import permutations
       
      # candidates already tried
      tries = [
        (6, 1, 2, 5, 3, 4),
        (4, 6, 5, 3, 2, 1),
        (2, 3, 4, 1, 6, 5),
        (3, 2, 6, 5, 4, 1),
      ]
      
      # count the number of correct positions (return 2 if two or more are correct)
      def count(xs, ys):
        found = 0
        for x, y in zip(xs, ys):
          if x == y:
            if found: return 2
            found = 1
        return found
      
      rng, sols = set(range(1, 7)), []
      # select values for first four positions
      for p4 in permutations(rng, 4):
        sol, hit1 = tuple(), []
        # check the four attempts
        for a in tries:
          # check if last 2 digits of attempt must be correct
          if count(p4, a[:4]) == 0: 
            # last 2 numbers of attempt must be different from the permutation
            if set(a[4:]).isdisjoint(p4):
              sol = p4 + a[4:]
            break
        else: # no break
          for n56 in permutations(rng - set(p4)):
            sol = p4 + n56
            if all(count(sol, a) > 1 and sol != a for a in tries):
              sols.append(sol)
          continue    
      
        # last 2 values are known
        if sol:
          if all(count(sol, a) > 1 and sol != a for a in tries):
            sols.append(sol)
            
      print("answer:", len(sols)) 
      

      Like

  • Unknown's avatar

    Jim Randell 4:52 pm on 15 July 2026 Permalink | Reply
    Tags: ,   

    Teaser 2425: [Boat hire] 

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

    Our club has hired a boat for a St Patrick’s Day cruise on Lough Neagh.

    It has fewer than 100 seats. When the number of passengers doesn’t exceed the number of seats, the hire charge is a whole number of pounds per head.

    For each extra passenger, the charge per head, for all passengers, is reduced by 5p. On our cruise, if the number of passengers having to stand was different, the hire charge would be less.

    All the seats have been allocated to older members; a smaller number will stand on deck, the number standing being the reverse of the number seated.

    What is the boat’s seating capacity?

    This puzzle was originally published with no title.

    [teaser2425]

     
    • Jim Randell's avatar

      Jim Randell 4:52 pm on 15 July 2026 Permalink | Reply

      If the boat has n seats, then for x passengers the hire charge H(x) is:

      H(x) = x . k [if x ≤ n]
      H(x) = x . (k − 5(x − n)) [if x > n]

      where k is the charge per seat (a multiple of 100 pence).

      For the x > n case, H(x) is a quadratic function:

      H(x) = −5x² + (k + 5n)x

      which has a stationary point at:

      x = (k + 5n) / 10

      Now, if the number seated in the party is AB (all seats are occupied, so n = AB), then the number standing is BA (with B < A). And the total number of passengers is:

      x = AB + BA = 11(A + B)

      So the stationary point is achieved when:

      11(A + B) = (k + 5AB) / 10

      k = 60A + 105B

      This Python program considers possible A and B values and looks for when the corresponding k value is a multiple of 100.

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

      from enigma import (irange, subsets, printf)
      
      # choose non-zero digits A and B (B < A)
      for (B, A) in subsets(irange(1, 9), size=2):
        # k = cost per seat
        k = 60*A + 105*B
        if k % 100 != 0: continue
        # t = total number of passengers
        t = 11*(A + B)
        # n = number of seats
        n = 10*A + B
        # h = total charge
        h = t * (k - 5 * (t - n))
        # output solution
        printf("{t} passengers: {n} seated, {r} standing; seat = {k} p -> total charge = {h} p", r=t - n)
      

      Solution: The boat has 84 seats.

      And for 84 or fewer passengers the price per passenger is £ 9.00.

      So, with all seats occupied, and no passengers standing, the hire charge is £ 756.

      The club outing has 84 seats occupied, and 48 passengers standing. The price per passenger is therefore reduced by 48 × 5 p = £ 2.40, to £6.60.

      The total charge for the 132 passengers is therefore, 132 × £6.60 = £871.20.

      And we can see this is a maximum value by checking the prices for 131 and 133 passengers:

      131 passengers: total charge = 131 × (900 − 5 × 47) = £ 871.15
      133 passengers: total charge = 133 × (900 − 5 × 49) = £ 871.15

      Like

      • Jim Randell's avatar

        Jim Randell 4:11 pm on 18 July 2026 Permalink | Reply

        If K is the prices of the seat in pounds then we have the following linear diophantine equation in 3 variables:

        60A + 105B − 100K = 0
        [0 < B < A < 10; K > 0]

        We already have a solver for linear diophantine equations in 2 variables in enigma.py, as [[ diop_linear() ]]. And we can use this to write a solver for equations with 3 variables.

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

        from enigma import (irange, inf, gcd, diop_linear, printf)
        
        # solve linear diophantine equation in 3 variables: a.X + b.Y + c.Z = v
        def diop_linear3(a, b, c, v=0, rX=(0, inf), rY=(0, inf), rZ=(0, inf)):
          ((min_X, max_X), (min_Y, max_Y), (min_Z, max_Z)) = (rX, rY, rZ)
          g = gcd(a, b)
          (a_, b_) = (a // g, b // g)
          # solve: g.U + c.Z = v
          min_U = a_ * (min_X if a_ > 0 else max_X) + b_ * (min_Y if b_ > 0 else max_Y)
          max_U = a_ * (max_X if a_ > 0 else min_X) + b_ * (max_Y if b_ > 0 else min_Y)
          f1 = diop_linear(g, c, v, mX=min_U, fn=1)
          for i in irange(0, inf):
            (U, Z) = f1(i)
            if U > max_U: break
            if Z < min_Z or Z > max_Z: continue
            # solve: a_.X + b_.Y = U
            f2 = diop_linear(a_, b_, U, mX=min_X, fn=1)
            for j in irange(0, inf):
              (X, Y) = f2(j)
              if X > max_X: break
              if Y < min_Y or Y > max_Y: continue
              yield (X, Y, Z)
        
        # solve the diophantine equation 60A + 105B - 100K = 0; 0 < B < A <= 9; K > 0
        for (A, B, K) in diop_linear3(60, 105, -100, 0, rX=(2, 9), rY=(1, 8), rZ=(1, inf)):
          if not (B < A): continue
          # k = cost per seat (in pence)
          k = 100 * K
          # t = total number of passengers
          t = 11*(A + B)
          # n = number of seats
          n = 10*A + B
          # h = total charge (in pence)
          h = t * (k - 5 * (t - n))
          # output solution
          printf("{t} passengers: {n} seated, {r} standing; seat = {k} p -> total charge = {h} p", r=t - n)
        

        Like

    • Frits's avatar

      Frits 2:23 pm on 16 July 2026 Permalink | Reply

      I got it down to one loop.

      # S = ab = number of seats, n = ba, k = whole number hire charge in pounds
      for a in [i for i in range(3, 10) if i != 5]:
        if (b := (8 * a) % 10) > a: continue
        # cost formula f(S + n) = -5.n^2 + (100.k - 5.S).n + 100.k.S is maximal 
        # if k = (S + 2.n) / 20
        # S + 2 * n = 11.(a + b) + 10.b + a = 12.a + 21.b = 20.k so b is even
        if (12 * a + 21 * b) % 20: continue
        print("answer:", 10 * a + b)
      

      Like

  • Unknown's avatar

    Jim Randell 5:36 am on 12 July 2026 Permalink | Reply
    Tags:   

    Teaser 3329: Ten cards 

    From The Sunday Times, 12th July 2026 [link] [link]

    I have ten cards on which are written 20 different positive integers, with one integer on each side of the cards. The total of the integers on the front of the ten cards equals the total of the integers on the back. The total of the integers on each card is also the same.

    The numbers on the front of the first nine cards are 2, 15, 17, 21, 24, 31, 35, 36 and 44. If I told you how many prime numbers are on the cards you should be able to tell me the two numbers on the tenth card.

    In ascending order, what are the numbers on card ten?

    [teaser3329]

     
    • Jim Randell's avatar

      Jim Randell 5:50 am on 12 July 2026 Permalink | Reply

      If the front of the tenth card has a value of X, and the sum of the numbers on each card is N, then, by considering the total of the fronts and backs, we have:

      450 + 2X = 10N

      N = 45 + X/5

      Hence X is a multiple of 5.

      The following Python program runs in 63ms. (Internal runtime is 87µs).

      from enigma import (defaultdict, irange, inf, union, is_prime, ordered, singleton, printf)
      
      # the numbers on the front of the first 9 cards
      front = [2, 15, 17, 21, 24, 31, 35, 36, 44]
      
      # collect candidate solutions (= card 10) by the total number of primes
      rs = defaultdict(set)
      
      # consider the front of card 10 (= X)
      for X in irange(5, inf, step=5):
        if X in front: continue
      
        # determine the total for each card
        N = 45 + (X // 5)
        # and the back of card 10 (= Y)
        Y = N - X
        if Y < 1: break
      
        # the numbers on the back of the first 9 cards
        back = list(N - x for x in front)
      
        # the numbers on the cards are all different
        ns = union([front, back, (X, Y)])
        if len(ns) != 20: continue
      
        # count the primes
        P = sum(1 for n in ns if is_prime(n))
      
        # collect candidate solution
        printf("[X={X} N={N} -> {front} / {back} + [{X} / {Y}] -> {P} primes]")
        rs[P].add(ordered(X, Y))
      
      # look for unique solutions
      for (k, vs) in rs.items():
        v = singleton(vs)
        if v is not None:
          printf("{k} primes -> card 10 = {v}")
      

      If you are running under Python 3 you can use the following at line 23 to collect all the numbers into a set:

        ns = { X, Y, *front, *back }
      

      Solution: The numbers on the tenth card are 9 and 45.

      The cards are ((front, back), primes underlined):

      (2, 52) (15, 39) (17, 37) (21, 33) (24, 30) (31, 23) (35, 19) (36, 18) (44, 10) (45, 9)

      The numbers on each card sum to 54, and the total of the fronts is 270 and the total of the backs is also 270.

      The are two other candidate sets of cards that can be constructed, but they each have 7 primes:

      (2, 45) (15, 32) (17, 30) (21, 26) (24, 23) (31, 16) (35, 12) (36, 11) (44, 3) (10, 37)
      (2, 47) (15, 34) (17, 32) (21, 28) (24, 25) (31, 18) (35, 14) (36, 13) (44, 5) (20, 29)

      Like

    • Frits's avatar

      Frits 11:20 am on 12 July 2026 Permalink | Reply

      from collections import defaultdict
      
      # front nine cards
      f9 = {2, 15, 17, 21, 24, 31, 35, 36, 44} # sum = 225
      sumf9 = sum(f9)
      
      # primes in range 2 to (sum(f9) - 1) // 4 - 1 
      P = {3, 5, 7}
      P |= {2} | {x for x in range(11, (sum(f9) - 1) // 4, 2) if all(x % p for p in P)}
      
      d = defaultdict(list) 
      # b = sum(f9) - 4 * t2 >= 1 or t2 <= (sum(f9) - 1) / 4
      # possible totals of the integers on a card 
      for t2 in range(max(f9) + min(set(range(1, max(f9) + 2)) - f9), 
                      (sumf9 - 1) // 4 + 1):
        # calculate front of the 10th card
        f = 5 * t2 - sumf9
        if f in f9: continue
        f10 = f9 | {f}
        b10 = {t2 - n for n in f10}
        # 20 different positive integers
        if len(fb10 := f10 | b10) != 20: continue
        # store the 20 numbers for the number of prime numbers
        d[sum(n in P for n in fb10)] += [(t2, fb10)]
      
      # look for a unique solution
      for k, vs in d.items():
        if len(vs) == 1:
          t, ns = vs[0]
          print(f"answer: {sorted(ns - f9 - {t - n for n in f9})}")
      

      Like

    • Ruud's avatar

      Ruud 5:27 pm on 12 July 2026 Permalink | Reply

      import collections
      import types
      import istr
      
      
      collect = collections.defaultdict(list)
      first_nine_cards_front = [2, 15, 17, 21, 24, 31, 35, 36, 44]
      
      for sum_on_card in range(45, 100):
          for card10_front in range(1, 100):
              if card10_front in first_nine_cards_front:
                  continue
              cards_front = first_nine_cards_front + [card10_front]
              cards_back = []
              for card_front in cards_front:
                  card_back = sum_on_card - card_front
                  if card_back < 1 or card_back in cards_front + cards_back:
                      break
                  cards_back.append(card_back)
              else:
                  if sum(cards_front) == sum(cards_back):
                      number_of_primes = sum(filter(istr.is_prime, cards_front + cards_back))
                      collect[number_of_primes].append(types.SimpleNamespace(cards_front=cards_front, cards_back=cards_back))
      
      
      for solutions in collect.values():
          if len(solutions) == 1:
              print(solutions[0])
              print("card10:", sorted([solutions[0].cards_front[-1], solutions[0].cards_back[-1]]))

      Like

  • Unknown's avatar

    Jim Randell 11:47 am on 10 July 2026 Permalink | Reply
    Tags:   

    Teaser 2420: [Extension number] 

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

    Since we last met George and Martha, George has moved departments and has a new extension number, which Martha has completely forgotten.

    When she inquired at the switchboard, the operator told her that if she wrote down the sum of the four digits of the number, then followed that with the product of the four digits, she would end up with the extension number itself. So Martha was able to work it out.

    What is George’s new extension number?

    This puzzle was originally published with no title.

    [teaser2420]

     
    • Jim Randell's avatar

      Jim Randell 11:48 am on 10 July 2026 Permalink | Reply

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

      It runs in 89ms. (Internal runtime of the generated code is 11ms).

      #! python3 -m enigma -r
      
      SubstitutedExpression
      
      --distinct=""
      --invalid=""
      
      # suppose the number is: ABCD
      "concat(A + B + C + D, A * B * C * D) == int2base(ABCD, width=4)"
      
      --answer="ABCD"
      

      Solution: The number is 1236.

      The sum of the digits is: 1 + 2 + 3 + 6 = 12.

      The product of the digits is: 1 × 2 × 3 × 6 = 36.

      Like

      • Hugo's avatar

        Hugo 3:42 pm on 19 July 2026 Permalink | Reply

        The third digit is the sum of the first two, and the fourth digit is the sum of the first three.
        But I can’t believe 1236 is hard to remember!

        Like

    • Ruud's avatar

      Ruud 8:11 pm on 10 July 2026 Permalink | Reply

      import istr
      
      print(*(number for number in istr.range(length=4) if sum(number) | number.prod() == number))
      

      Like

    • Frits's avatar

      Frits 2:24 pm on 21 July 2026 Permalink | Reply

        
      # A + B + C + D > 9 otherwise B = C = D = 0 resulting in a zero product
      # AB = A + B + C + D or 9A = C + D
      # if A = 2 then C = D = 9 but also A.C.D > 99 so A = 1
      # C + D = 9, A.C.D is always even thus D must be even and C must be odd
      
      # A.B.C.D = CD or B = (10C + (9 - C)) / C.(9 - C) = (9 + 9/C) / (9 - C)
      # two options for odd C < 9 for C/9 to be an integer:
      # C = 1: B = 18 / 8 invalid
      # C = 3: B = 12 / 6 = 2, D = 9 - C = 6 so ABCD = 1236
      

      Like

  • Unknown's avatar

    Jim Randell 9:43 am on 8 July 2026 Permalink | Reply
    Tags:   

    Teaser 2416: [Path-o-logical] 

    From The Sunday Times, 11th January 2009 [link]

    Recently, Joe repaved his metre-wide front path. He used square slabs with half-metre sides, some of the slabs being pink and the rest grey. He made the first two slabs pink, then he arranged the rest so that the pattern of the four slabs in any square metre was not repeated, as would be seen by anyone walking forward along the path towards the front door. This would not have been possible had the path been any longer.

    (a) How long is Joe’s front path?
    (b) What are the colours of the last two slabs?

    This puzzle was originally published with no title.

    [teaser2416]

     
    • Jim Randell's avatar

      Jim Randell 9:45 am on 8 July 2026 Permalink | Reply

      See also: Enigma 1511, Enigma 1520.

      From a fixed viewpoint (so we don’t have to worry about rotations) it is easy to see that there are 4 possible pairs of tiles (“pp”, “pg”, “gp”, “gg”), and these can appear at the top or bottom of a 4×4 unit, so there are 16 possible units. Which if they are all used would lead to 16 tops and 16 bottoms which overlap to fit on a 17×0.5 m = 8.5 m path.

      This Python program constructs all possible maximal paths.

      It runs in 754ms. (Internal runtime is 704ms).

      from enigma import (Accumulator, empty, printf)
      
      # possible pairs of slabs
      pairs = ["pp", "pg", "gp", "gg"]
      
      # extend the path <ps> without repeating two pairs
      def solve(ps, seen=empty):
        # look for possible next pairs
        x = ps[-2:]
        nps = list(p for p in pairs if x + p not in seen)
        # are we done?
        if not nps:
          yield ps
        else:
          for p in nps:
            yield from solve(ps + p, seen.union({x + p}))  # [Python 3]
      
      # look for maximal length paths
      r = Accumulator(fn=max, collect=1)
      for ps in solve("pp"):
        r.accumulate_data(len(ps), ps)
      
      printf("max path len = {n} m", n=r.value * 0.25)
      printf("-> {ps} [of {n}]", ps=r.data[0], n=len(r.data))
      printf("final = {ss}", ss=set(ps[-2:] for ps in r.data))
      

      Solution: (a) Joe’s front path is 8.5 m long; (b) The last two slabs are both pink.

      For example:

      There are 82944 possible maximal length arrangements, and each ends with a double-pink slab (so each arrangement will appear both forwards and backwards). Which means the path could be bent around so the start and end pairs overlapped to give a circular arrangement with all possible 2×2 arrangements. (And the loop can then be broken at an appropriate point to give a path starting with any particular pair).

      Like

  • Unknown's avatar

    Jim Randell 7:35 am on 5 July 2026 Permalink | Reply
    Tags:   

    Teaser 3328: Loose change 

    From The Sunday Times, 5th July 2026 [link] [link]

    Elaine reminded Phil to take his suit to the dry cleaners. When Phil emptied his pockets he found he had exactly £5 in coins. All denominations of coin were represented (£2, £1, 50p, 20p, 10p, 5p, 2p and 1p) but there were more of one coin than any other.

    Phil told all of this to Elaine, but she couldn’t work out how many coins Phil had. “If I told you the total two-figure number of coins you would be able to work out the numbers of each coin present”, said Phil.

    Which denomination appeared most and how many of that coin were present?

    The S2T2 site now has all puzzles from April 2009 to present (along with solutions), which is a continuous run of the most recent 900 Teaser puzzles. Additionally there are currently 460 older Teaser puzzles available too.

    [teaser3328]

     
    • Jim Randell's avatar

      Jim Randell 7:43 am on 5 July 2026 Permalink | Reply

      This Python program runs in 87ms. (Internal runtime is 13ms).

      from enigma import (express, group, singleton, printf)
      
      # denominations (in increasing order)
      ds = [1, 2, 5, 10, 20, 50, 100, 200]
      
      # generate candidate quantities
      def generate():
        # make a total amount of 500p
        for qs in express(500, ds, min_q=1):
          # only allow those with a distinct maximum quantity
          if qs.count(max(qs)) == 1:
            yield qs
      
      # collect arrangements by total number of coins
      g = group(generate(), by=sum)
      
      # look for unique 2-digit totals
      for (k, vs) in g.items():
        if k < 10 or k > 99: continue
        qs = singleton(vs)
        if qs is None: continue
      
        # output solution
        printf("{k} coins = {qs} * {ds}p")
        # find maximum occurring denomination
        i = qs.index(max(qs))
        printf("-> most common = {q} * {d}p", q=qs[i], d=ds[i])
      

      Solution: There were more 20p coins than any other denomination. There were 4 of them.

      The solution is derived from the following arrangement:

      1× £2 = 200p
      1× £1 = 100p
      2× 50p = 100p
      4× 20p = 80p
      1× 10p = 10p
      1× 5p = 5p
      2× 2p = 4p
      1× 1p = 1p
      total = 500p using 13 coins

      Like

    • Ruud's avatar

      Ruud 10:55 am on 5 July 2026 Permalink | Reply

      import collections
      
      coins = (200, 100, 50, 20, 10, 5, 2, 1)
      
      
      def allocate(amount_left, coins_left, result):
          if coins_left:
              for n in range(1, amount_left // coins_left[0] + 1):
                  if amount_left >= n * coins_left[0]:
                      yield from allocate(amount_left - n * coins_left[0], coins_left[1:], result + [n])
          else:
              if amount_left == 0 and result.count(max(result)) == 1 and len(str(sum(result))) == 2:
                  yield result
      
      
      collect = collections.defaultdict(list)
      for coin_count in allocate(500, coins, []):
          collect[sum(coin_count)].append(coin_count)
      
      for number_of_coins, coin_counts in collect.items():
          if len(coin_counts) == 1:
              print(*(f"{number} * {coin}p" for coin, number in zip(coins, coin_counts[0]) if number == max(coin_counts[0])))
              print(*(f"{number} * {coin}p" for coin, number in zip(coins, coin_counts[0])))
      

      Like

    • Frits's avatar

      Frits 8:38 pm on 5 July 2026 Permalink | Reply

      A two-stage rocket. More efficient than doing only one standard decompose. S =2 seems to be the optimum.

      from collections import defaultdict
      
      denoms = [1, 2, 5, 10, 20, 50, 100, 200]
      N = len(denoms)
      # remaining target starting with 1 coin of each
      T = 500 - sum(denoms)
      # determine smallest <S> denominations last (S > 0)
      S = 2
      
      # decompose: choose numbers from <ns> so that sum(chosen numbers) equals <t>
      #            and increment these numbers with 1
      def decompose(t, ns, m, s=[]):
        if m == 0:
          n, r = divmod(t, ns[0])
          if not r:
            yield [n + 1] + s[::-1]
        else:
          for i in range(t // ns[m] + 1):
            yield from decompose(t - i * ns[m], ns, m - 1, s + [i + 1])
      
      # decompose: choose numbers from <ns> so that sum(chosen numbers) <= t
      def decompose_upto(t, ns, m, s=[]):
        if m < 0:
          yield s[::-1]
        else:
          for i in range(t // ns[m] + 1):
            yield from decompose_upto(t - i * ns[m], ns, m - 1, s + [i + 1])      
      
      # dictionary of total number of coins still to be made
      d = defaultdict(list)
      
      denoms2 = denoms[S:]       # all except the <S> smallest denominations
      sm = sum(denoms2)
      # get coins for these denominations not exceeding T
      for p1 in decompose_upto(T, denoms2, N - S - 1):
        todo = T + sm - sum(x * y for x, y in zip(p1, denoms2))
        d[todo] += [p1]
      
      # dictionary of total number of coins occurences
      freqs = defaultdict(list)
      
      for k, vs in sorted(d.items()):
        # we still need to make <k> pennies with the <S> lowest denominations
        for p2 in decompose(k, denoms[:S], S - 1):
          for v in vs:
            if 10 <= (ncoins := sum(coins := p2 + v)) <= 99:
              # there were more of one coin than any other
              if coins.count(max(coins)) == 1:
                freqs[ncoins] += [coins]
      
      # if I told you the total two-figure number of coins you would be able to
      # work out the numbers of each coin present
      for k, vs in freqs.items():
        if len(vs) != 1: continue
        # denomination that appeared most
        ans = max([(x, y) for x, y in zip(vs[0], denoms)])
        print(f"answer: {ans[1]}p appeared most ({ans[0]} times)")
      

      Like

    • Frits's avatar

      Frits 6:08 pm on 6 July 2026 Permalink | Reply

      Another approach that is sometimes more efficient for hard cases (like denominations 1, 4, 5, 7, 14, 15, 19 and 35).
      Jim’s program ran for 80 seconds under CPython for this special case. With this program it takes 2 seconds to finish.

      denoms = [1, 2, 5, 10, 20, 50, 100, 200] 
      #denoms = [1, 4, 5, 7, 14, 15, 19, 35] 
      
      N = len(denoms)
      # remaining target starting with 1 coin of each
      T = 500 - sum(denoms)
      
      # find the factor that occurs the most
      fs = sorted([([n for n in denoms if n % i == 0], i) 
                     for i in range(2, denoms[-2] + 1)], 
                  key=lambda x: (-len(x[0]), -x[1]))
       
      # denoms with the list with common factor usage at the back
      denoms = [n for n in sorted(denoms) if n not in fs[0][0]] + fs[0][0]
      
      # decompose: choose numbers from <ns> so that sum(chosen numbers) equals <t>
      #            and increment these numbers with 1
      def decompose(t, k, ns, m, s=[]):
        if m == 0:  
          # we need <k> coins of lowest denomination to make <t>
          if k * ns[0] == t:
            coins = [k + 1] + s 
            # there were more of one coin than any other
            if coins.count(max(coins)) == 1:
              yield coins
        else:
          for i in range(min(k, t // ns[m]) + 1):
            yield from decompose(t - i * ns[m], k - i, ns, m - 1, [i + 1] + s)
      
      # process 2-digit number of coins
      for k in range(10, 100):
        sol = []
        # make amount <T> with <k - 8> coins
        cnt = 0
        for p in decompose(T, k - len(denoms), denoms, len(denoms) - 1):
          cnt += 1
          if cnt >= 2:
            break # no unique solution for <k> coins
          sol = p  
        else: # no break
          if cnt == 1:
            times = max(sol)
            print(f"answer: {denoms[sol.index(times)]}p appeared most ({times} times)")
            print(f"        {sol} * {denoms}")
      

      Like

      • Jim Randell's avatar

        Jim Randell 3:07 pm on 7 July 2026 Permalink | Reply

        Considering the possible numbers of coins is a neat idea.

        In fact I already have an [[ express_pairs() ]] function in enigma.py that supports expressing an amount using a specific number of coins. (I added this to support [[ multiset.express() ]] some time ago; see Enigma 824).

        And if we find a second arrangement for a particular number of coins we can move directly onto the next number of coins without examining any other arrangements.

        The following Python program runs in 80ms. (Internal runtime is 10ms, so it is slightly faster than my original program).

        from enigma import (irange, inf, express_pairs, multiset, unzip, singleton, printf)
        
        # denominations (in increasing order)
        ds = [1, 2, 5, 10, 20, 50, 100, 200]
        
        # target total
        T = 500
        
        # one of each coin is used, so we just need to find the remaining total
        t = T - sum(ds)
        
        # denominations we can use to make <t>
        vs = list((d, inf) for d in ds if not (d > t))
        
        # express total <t> using exactly <k> coins from <ds>
        def express(k):
          for ss in express_pairs(t, vs, inf, k):
            # check there is a single denomination with maximum quantity
            (_, qs) = unzip(ss)
            if qs.count(max(qs)) == 1:
              yield ss
        
        # look for a 2-digit number of coins with a unique arrangement
        n = len(ds)
        for k in irange(10, 99):
          # we need to find a set of (k - n) coins to make the value t
          ps = singleton(express(k - n))
          if ps is None: continue
          # construct the collection of coins
          m = multiset.from_pairs(ps).update_from_seq(ds)
          # output solution
          printf("{k} coins [{m} -> {t}]", m=m.map2str(), t=m.sum())
          for (d, q) in m.most_common(1):
            printf("-> most common = {q} * {d}p")
          printf()
        

        For your alternate set of denominations ([1, 4, 5, 7, 14, 15, 19, 35]) it runs in 1.0s (using PyPy 7.3.23), and finds two possible sets:

        % pypy teaser3328v.py
        20 coins [(1=1, 4=1, 5=1, 7=1, 14=1, 15=2, 19=1, 35=12) -> 500]
        -> most common = 12 * 35p
        
        21 coins [(1=2, 4=1, 5=1, 7=1, 14=2, 15=1, 19=1, 35=12) -> 500]
        -> most common = 12 * 35p
        
        

        But they give the same answer of 12× 35p coins.

        [Note: With the early rejection suggested by Frits below, and some other refinements, the internal runtime of this program is reduced to 1.3ms (and 2.8ms for the alternate set of denominations)].

        Like

        • Frits's avatar

          Frits 10:08 am on 9 July 2026 Permalink | Reply

          @Jim, one more improvement.

          The case with ds = [4, 5, 6, 7, 13, 16, 33, 116] still runs for 12 seconds.
          The run time can be improved by replacing line 25 with:

          for k in irange(10, min(99, n + t // ds[0])):
          

          Like

        • Frits's avatar

          Frits 10:09 pm on 9 July 2026 Permalink | Reply

          Probably my final post for this teaser.
          Calculating the number of coins for the 2 lowest denominations in one go.

          denoms = [1, 2, 5, 10, 20, 50, 100, 200] 
          
          N = len(denoms)
          # remaining target starting with 1 coin of each
          T = 500 - sum(denoms)
          
          # decompose: choose numbers from <ns> so that sum(chosen numbers) equals <t>
          #            and increment these numbers with 1
          def decompose(t, k, ns, m, s=[]):
            if m == 1:  
              # we can calculate the number of coins for the 2 lowest denominations
              k2, r = divmod(t - k * ns[0], ns[1] - ns[0])
              if not (r == 0 and 0 <= k2 <= k): return
              coins = [k - k2 + 1, k2 + 1] + s 
              # there were more of one coin than any other
              if coins.count(max(coins)) == 1:
                yield coins
            else:
              if k * ns[m] < t: return # target cannot be achieved in k coins
              for i in range(0, t // ns[m] + 1):
                yield from decompose(t - i * ns[m], k - i, ns, m - 1, [i + 1] + s)
          
          # process 2-digit number of coins
          for k in range(10, min(99, N + T // denoms[0]) + 1):
            sol = []
            # make amount <T> with <k - N> coins
            for p in decompose(T, k - N, denoms, N - 1):
              if sol and p != sol:
                break # no unique solution for <k> coins
              sol = p  
            else: # no break
              if sol:
                times = max(sol)
                print(f"answer: {denoms[sol.index(times)]}p appeared most ({times} times)")
                print(f"        {sol} * {denoms}")
          

          Like

      • Frits's avatar

        Frits 8:01 pm on 7 July 2026 Permalink | Reply

        Not restructuring “denoms” and using “if k * ns[m] < t : return" before line 27 gives run times below 20ms.

        Like

        • Frits's avatar

          Frits 8:52 pm on 7 July 2026 Permalink | Reply

          @Jim, _express_pairs also seems to benefit hugely by a similar addition (eg “if k * x < t: return" at approx. line 7567 in enigma.py).

          Like

          • Jim Randell's avatar

            Jim Randell 6:55 am on 8 July 2026 Permalink | Reply

            @Frits: Good call. This is early rejection for arrangements which cannot possible achieve the required target using the remaining k coins.

            I have added it into [[ express_pairs() ]] (enigma.py version 2026-07-08).

            Like

    • Frits's avatar

      Frits 5:40 pm on 7 July 2026 Permalink | Reply

      For the alternate set of denominations ([1, 4, 5, 7, 14, 15, 19, 35]) this program runs in 15ms under CPython 3.14.3. On my Windows computer Jim’s express_pairs() program runs in 3 seconds (using PyPy 7.3.20).

      from collections import defaultdict
      
      denoms = [1, 2, 5, 10, 20, 50, 100, 200] 
      
      N = len(denoms)
      # remaining target starting with 1 coin of each
      T = 500 - sum(denoms)
      
      # find the factor that occurs the most
      fs = sorted([([n for n in denoms if n % i == 0], i) 
                     for i in range(2, denoms[-2] + 1)], 
                  key=lambda x: (-len(x[0]), -x[1]))
       
      # denoms with the list with common factor usage at the back
      denoms = [n for n in sorted(denoms) if n not in fs[0][0]] + fs[0][0]
      
      d = defaultdict(list)
      # build dictionary of totals for one, two and three coins
      for i1, c1 in enumerate(denoms):
        d[c1] += [(i1, )]
        for i2, c2 in enumerate(denoms[i1:]):
          d[c1 + c2] += [(i1, i1 + i2)]
          for i3, c3 in enumerate(denoms[i1 + i2:]):
            d[c1 + c2 + c3] += [(i1, i1 + i2, i1 + i2 + i3)]
            
      # decompose: choose numbers from <ns> so that sum(chosen numbers) equals <t>
      #            and increment these numbers with 1
      def decompose(t, k, ns, m, s=[]):
        # we still need to use 3 or less coins
        if m == 0 or k <= 3:
          if k <= 3:  
            if t in d:
              for cs in d[t]:
                # are there <k> coins that add up to <t>
                if len(cs) == k:
                  coins = [1] * (m + 1) + s
                  for c in cs:
                    coins[c] += 1
                  # there were more of one coin than any other
                  if coins.count(max(coins)) == 1:  
                    yield coins
          else:  
            # we need <k> coins of lowest denomination to make <t>
            if k * ns[0] == t:
              coins = [k + 1] + s 
              # there were more of one coin than any other
              if coins.count(max(coins)) == 1:
                yield coins
        else:
          # m = 1 --> t - i * ns[1] must be equal to (k - i) * ns[0]
          #           i = (t - k * ns[0]) / (ns[1] - ns[0])
          if m > 1:
            mn = 0 
            # stop if k * ns[m] < t 
            mx = -1 if k * ns[m] < t else t // ns[m]
          else:
            mn, r = divmod(t - k * ns[0], ns[1] - ns[0])
            if not (r == 0 and 0 <= mn <= k): return
            mx = mn 
          
          for i in range(mn, mx + 1):
            yield from decompose(t - i * ns[m], k - i, ns, m - 1, [i + 1] + s)
      
      # process 2-digit number of coins
      for k in range(10, 100):
        sol = []
        # make amount <T> with <k - 8> coins
        for p in decompose(T, k - N, denoms, N - 1):
          if sol and p != sol:
            break # no unique solution for <k> coins
          sol = p  
        else: # no break
          if sol:
            times = max(sol)
            print(f"answer: {denoms[sol.index(times)]}p appeared most ({times} times)")
            print(f"        {sol} * {denoms}")
      

      Like

    • Alex.T.Sutherland's avatar

      Alex.T.Sutherland 12:15 pm on 8 July 2026 Permalink | Reply

      Subject regarding the alternate set of denominations.I have found
      many possible singular sets two of which (20 & 21) have already been
      found and mentioned.
      The following is a sample of my findings of the items and their number :-

      Sum = 20

      1 1 1 1 1 2 1 12 –> number of items ordered in increasing value.

      Sum = 21

      2 1 1 1 2 1 1 12

      Sum = 30

      1 1 1 1 3 7 14 2

      Sum = 45

      5 5 5 5 4 14 6 1

      Sum = 47

      5 5 5 5 14 11 1 1

      Not sure how it helps solving the original puzzle for a unique answer.

      My answer to the puzzle (T3328) comes from a group of 4 sets each with the same sum.

      Time :- ~25ms

      Like

      • Jim Randell's avatar

        Jim Randell 3:26 pm on 8 July 2026 Permalink | Reply

        @Alex:

        Using the alternate set of denominations ([1, 4, 5, 7, 14, 15, 19, 35]) I found many viable arrangements for 30, 45 and 47 coins. (In fact, 1941 for 30 coins, 103905 for 45 coins, 133002 for 47 coins). But there is only a single viable arrangement for each of 20 and 21 coins.

        For example, as well as the arrangements you give there are also the following:

        (1, 1, 1, 1, 3, 3, 19, 1) * (1, 4, 5, 7, 14, 15, 19, 35)p = 500p in 30 coins
        (1, 1, 4, 14, 22, 1, 1, 1) * (1, 4, 5, 7, 14, 15, 19, 35)p = 500p in 45 coins
        (1, 1, 4, 18, 20, 1, 1, 1) * (1, 4, 5, 7, 14, 15, 19, 35)p = 500p in 47 coins

        Like

  • Unknown's avatar

    Jim Randell 11:38 am on 3 July 2026 Permalink | Reply
    Tags:   

    Teaser 2429: [Easter alphametic] 

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

    In this subtraction sum, digits have consistently been replaced by letters, with different letters for different digits.

    EASTERBONNET = TIMES

    What number is TEASER?

    This puzzle was originally published with no title.

    [teaser2429]

     
    • Jim Randell's avatar

      Jim Randell 11:39 am on 3 July 2026 Permalink | Reply

      By rearranging the sum as an addition sum we can use the [[ SubstitutedExpression.split_sum ]] solver from the enigma.py library.

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "TIMES + BONNET = EASTER"
      
      --answer="TEASER"
      

      Solution: TEASER = 591792.

      Like

    • Ruud's avatar

      Ruud 12:56 pm on 3 July 2026 Permalink | Reply

      import peek
      import istr
      
      for e, a, s, t, r, i, m in istr.permutations(range(0, 10), 7):
          if (
              (bonnet := istr(":=easter") - istr(":=times")) > 0
              and bonnet[2] == bonnet[3]
              and (istr("=eastrim") | bonnet[:3]).all_distinct()
              and bonnet[4:] == (e | t)
          ):
              peek(easter, bonnet, times, istr("=teaser"))
      

      Like

  • Unknown's avatar

    Jim Randell 8:08 am on 1 July 2026 Permalink | Reply
    Tags:   

    Brain-Teaser 943: Order in football 

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

    There are three teams In the Midchester football league: Albion, United and Victoria. During the season they play each other twice; once at home and once away.

    A team gets two points for a win and one point tor a draw. After each match the local paper publishes the current league table. In this table the teams are listed according to points and if two or more teams have equal points then they are listed according to goal difference, (i.e. “goals for” minus “goals against”). If two or more teams have equal points and equal goal difference then they are listed alphabetically.

    At the end of last season the sports editor published the results of all the matches:

    Albion 4 – United 0
    Albion 0 – Victoria 0
    United 0 – Albion 1
    United 2 – Victoria 0
    Victoria 3 – Albion 0
    Victoria 1 – United 2

    He also said that each of the six league tables published during the season had put the teams in a different order. Thus each of the six possible orders of the teams (AUV, AVU, UAV, UVA, VAU, VUA) had occurred in one of the six league tables. Finally, he said that the third match of the season had been the draw, A vs. V.

    List the six matches in the order in which they were played during the season.

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

    [teaser943]

     
    • Jim Randell's avatar

      Jim Randell 8:09 am on 1 July 2026 Permalink | Reply

      I thought about writing a recursive solver that checks the orders of the teams are all different as each match is chosen, but it is more straightforward to just generate the possible orderings of the matches, calculate the orders of the teams after each match, and see if they are all different.

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

      from enigma import (compare, subsets, join, seq_all_different, printf)
      
      teams = "AUV"  # labels for the teams
      points = [0, 1, 2]  # point for l, d, w
      
      # produce the orders for the teams after each of the matches is played
      def orders(ms):
        # go through the matches, and accumulate points, goal difference for each team
        (pts, gd, tss) = (dict.fromkeys(teams, 0), dict.fromkeys(teams, 0), list())
        for ((X, gX), (Y, gY)) in ms:
          # assign points
          pts[X] += compare(gX, gY, vs=points)
          pts[Y] += compare(gY, gX, vs=points)
          # count goals
          gd[X] += (gX - gY)
          gd[Y] += (gY - gX)
          # calculate order
          ts = join(sorted(teams, key=(lambda k: (pts[k], gd[k], -ord(k))), reverse=1))
          tss.append(ts)
        return tss
      
      # the matches, and the outcomes:
      # this match was 3rd
      match3 = (("A", 0), ("V", 0))
      # and these are the remaining matches
      matches = [
        (("A", 4), ("U", 0)),
        (("U", 0), ("A", 1)),
        (("U", 2), ("V", 0)),
        (("V", 3), ("A", 0)),
        (("V", 1), ("U", 2)),
      ]
      
      # choose an order for the remaining matches
      for ms in subsets(matches, size=len, select='P', fn=list):
        # insert match3 into the 3rd position (= index 2)
        ms.insert(2, match3)
        # calculate the team orders after each match
        tss = orders(ms)
        # check they are all different
        if seq_all_different(tss):
          for (i, (((X, x), (Y, y)), ts)) in enumerate(zip(ms, tss), start=1):
            printf("{i}: {X} vs. {Y}  ({x} - {y});  order = {ts}")
          printf()
      

      Solution: The order of the matches was: U vs. V; V vs. A; A vs. V; U vs. A; A vs. U; V vs. U.

      Giving the following orderings of the teams after each match:

      match 1: U vs. V = 2 – 0
      order: U (2 points, +2 goal diff); A (0 points, 0 goal diff); V (0 points, −2 goal diff)

      match 2: V vs. A = 3 – 0
      order: U (2 points, +2 goal diff); V (2 points, +1 goal diff); A (0 points, −3 goal diff)

      match 3: A vs. V = 0 – 0
      order: V (3 points, +1 goal diff); U (2 points, +2 goal diff); A (1 points, −3 goal diff)

      match 4: U vs. A = 0 – 1
      order: V (3 points, +1 goal diff); A (3 points, −2 goal diff); U (2 points, +1 goal diff)

      match 5: A vs. U = 4 – 0
      order: A (5 points, +2 goal diff); V (3 points, +1 goal diff); U (2 points, −3 goal diff)

      match 6: V vs. U = 1 – 2
      order: A (5 points, +2 goal diff); U (4 points, −2 goal diff); V (3 points, 0 goal diff)

      Like

    • Ruud's avatar

      Ruud 8:18 pm on 1 July 2026 Permalink | Reply

      Correction:

      import peek
      import itertools
      
      for matches in itertools.permutations("AU40 AV00 UA01 UV20 VA30 VU12".split()):
          if matches[2] != "AV00":
              continue
          points = dict.fromkeys("AUV", 0)
          goals_difference = dict.fromkeys("AUV", 0)
          score = {"A": (0, 0, "A"), "U": (0, 0, "U"), "V": (0, 0, "V")}
          seen = set()
          for match in matches:
              team0, team1 = match[:2]
              goals0, goals1 = map(int, match[2:])
              points[team0] += (goals0 > goals1) + (goals0 >= goals1)
              points[team1] += (goals1 > goals0) + (goals1 >= goals0)
              goals_difference[team0] += goals0 - goals1
              goals_difference[team1] += goals1 - goals0
              score[team0] = (points[team0], goals_difference[team0], team0)
              score[team1] = (points[team1], goals_difference[team1], team1)
              order = tuple(x[2] for x in sorted(score.values(), key=lambda x: (-x[0], -x[1], ord(x[2]))))
              if order in seen:
                  break
              seen.add(order)
          else:
              peek(matches)
      

      Like

  • Unknown's avatar

    Jim Randell 6:46 am on 28 June 2026 Permalink | Reply
    Tags:   

    Teaser 3327: Water, water everywhere 

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

    Having moved to the southern coast of a large lake, George and Martha have acquired a speedboat, capable of doing V km/h. They are going to visit their daughter who lives 60 km due north on the opposite shore of the lake. A steady current is flowing from the West due East. George has told Martha that he intends to propel the boat at top speed at such an angle that it will complete the direct south-north journey in T minutes.

    “In that case”, replied Martha confidently, “you have allowed for the current to be flowing at C km/h, reducing your speed by exactly 1 or 2 km/h to S“.

    C, V, S and T are all two-digit whole numbers, with S+T being a perfect power.

    How fast is the current flowing?

    [teaser3327]

     
    • Jim Randell's avatar

      Jim Randell 6:59 am on 28 June 2026 Permalink | Reply

      This is a straightforward solution that starts by considering possible V values.

      The following Python program runs in 72ms. (Internal runtime is 223µs).

      from enigma import (irange, div, is_ipower, ircs, printf)
      
      # consider possible 2-digit speeds (= V)
      for V in irange(10, 99):
        # the speed is reduced by 1 or 2 to give S
        for S in (V - 1, V - 2):
          if S < 10: continue
      
          # the 60 km journey at effective speed S takes T min
          T = div(3600, S)
          if T is None or T < 10 or T > 99: continue
      
          # speed of the current (= C)
          C = ircs(V, -S)
          if C is None or C < 10 or C > 99: continue
      
          # S+T is a perfect power
          if not is_ipower(S + T): continue
      
          # output solution
          printf("C={C} [V={V} S={S} T={T}]")
      

      Solution: The current flows at 18 km/h.

      The speed of the speedboat (in still water) is V = 82 km/h.

      The current flows at a rate of C = 18 km/h, to give an effective speed of S = 80 km/h.

      So the 60 km journey takes T = 0.75 hours = 45 minutes.

      And S + T = 80 + 45 = 125 = 5³.


      Although starting from viable Pythagorean triples gives a faster program, I think manually it is easier to consider divisor pairs of 60 × 60, where each divisor has 2 digits. These form candidate S and T values, and so their sum must be a perfect power.

      There are only 5 cases to consider:

      60, 60 → sum = 120
      50, 72 → sum = 122
      48, 75 → sum = 123
      45, 80 → sum = 125 = 5^3
      40, 90 → sum = 130

      Only one pair has a sum that is a perfect power.

      So, (S, T) = (45, 80) (in some order).

      Now, V = S + {1 or 2}, and C = √(V² − S²) = √((VS)(V + S))

      So we can consider the possibilities:

      S = 45, T = 80:
      V = 46 → C = √91 (irrational)
      V = 47 → C = √184 (irrational)

      S = 80, T = 45:
      V = 81 → C = √161 (irrational)
      V = 82 → C = √324 = 18

      Hence the solution is: C = 18.

      Like

      • Jim Randell's avatar

        Jim Randell 7:50 am on 28 June 2026 Permalink | Reply

        A (slightly shorter, and faster) alternative approach (that assumes C < S).

        The internal runtime of this program is 90µs.

        from enigma import (pythagorean_triples, div, is_ipower, printf)
        
        # consider possible C, S, V speeds
        for (C, S, V) in pythagorean_triples(99):
          if C < 10 or (V - S) not in {1, 2}: continue
        
          # calculate time for the 60 km journey at effective speed S (= T min)
          T = div(3600, S)
          if T is None or T < 10 or T > 99: continue
        
          # S+T is a perfect power
          if not is_ipower(S + T): continue
        
          # output solution
          printf("C={C} [V={V} S={S} T={T}]")
        

        Liked by 1 person

      • Ruud's avatar

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

        I wonder how you know that S+T can only by a number to the power of 2..6.
        I think it would be more accurate to test for the irange of 2..int(math.log2(S + T), which happens to be 6 for all your tested values of S + T .

        Like

        • Jim Randell's avatar

          Jim Randell 11:59 am on 28 June 2026 Permalink | Reply

          @Ruud: 2^7 (= 128) is greater than 2-digits, so a 2 digit number cannot be a power greater than 6.

          But S + T is the sum of two 2-digit numbers, so we should check powers up to 2^7.

          However I already have [[ is_ipower() ]] in enigma.py to check if a number is an exact power, so I switched to using that.

          Like

          • Ruud's avatar

            Ruud 12:06 pm on 28 June 2026 Permalink | Reply

            I don’t think that’s the reason. S+T does not have to be 2 digits long (and in fact it isn’t).
            You are actually testing for 123, 120 and 125! So there must be another reason how you now that it can’t be >= 128.

            Like

        • Jim Randell's avatar

          Jim Randell 1:28 pm on 28 June 2026 Permalink | Reply

          It might be more efficient to set up a collection of possible powers, and then check if S + T is in it:

          # generate possible powers
          pows = first(ipowers(), skip=lt(20), count=lt(199), fn=set)
          
          # or use a precomputed set
          pows = {25, 27, 32, 36, 49, 64, 81, 100, 121, 125, 128, 144, 169, 196}
          

          Using the precomputed set brings my second program down to 82µs.

          Like

    • Ruud's avatar

      Ruud 8:22 am on 28 June 2026 Permalink | Reply

      import peek
      import istr
      import math
      
      for s in istr.range(length=2):
          t = istr.divided_by(3600, s, 0)
          if len(t) == 2 and any((t + s).is_power_of(n) for n in range(2, int(math.log2(t + s) + 1))):
              for v in (s + 1, min(s + 2, 100)):
                  c = istr(math.sqrt(v * v - s * s))
                  if c * c == v * v - s * s and len(c) == 2:
                      peek(c, v, s, t)
      

      Like

    • ruudvanderham's avatar

      ruudvanderham 11:11 am on 28 June 2026 Permalink | Reply

      With the latest istr, we can do:

      import peek
      import istr
      import math
      
      for s in istr.divisors(3600):
          if len(s) == 2:
              t = istr.divided_by(3600, s)
              if len(t) == 2 and (t + s).is_power_of():
                  for v in (s + 1, min(s + 2, 100)):
                      c = istr(math.sqrt(c2 := v * v - s * s))
                      if c * c == c2 and len(c) == 2:
                          peek(c, v, s, t)
          elif len(s) > 2:
              break
      

      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