Recent Updates Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 11:02 am on 6 January 2026 Permalink | Reply
    Tags:   

    Brain teaser 981: Bailing out 

    From The Sunday Times, 10th May 1981 [link]

    The pirate ship “Nancy” had one leaky hold. As long as the water was not allowed to rise above a certain level there was no danger of the ship sinking; so the practice was to allow the water to rise to a mark on the side of the hold which was just below the danger level, and then to send in a team of about 15 or 20 pirates to bale out the hold until it was empty. The water came in continuously at a constant: rate, so the process had, to be repeated regularly.

    There were two kinds of baling equipment: practical pans and capacious cans (which emptied the water at different rates), and each pirate who was assigned to baling duty picked up whichever of these was nearest to hand.

    One baling party consisting of 11 pirates with practical pans and 6 pirates with capacious cans emptied the hold in 56 minutes. When the water had risen again, the next party of 5 pirates with practical pans and 12 with capacious cans emptied it in 35 minutes. A third party of 15 with practical pans plus 4 with capacious cans took exactly an hour to do the job.

    How long would it take a party of 6 with practical pans and 10 with capacious cans?

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

    [teaser981]

     
    • Jim Randell's avatar

      Jim Randell 11:02 am on 6 January 2026 Permalink | Reply

      If we treat the volume of water at the point that baling starts as 1 unit, then we can write the following equation for a team with P pans and C cups as:

      T(P.p + C.c + f) = 1

      P.p + C.c + f = 1/T

      where:

      T is the total time taken to empty the hold (min);
      p is the rate at which a pan removes water (units/min);
      c is the rate at which a cup removes water (units/min);
      f is the rate at which the hold fills (units/min) (this will be negative as water is flowing into the hold).

      For the three teams given we get the following equations:

      11p + 6c + f = 1/56
      5p + 12c + f = 1/35
      15p + 4c + f = 1/60

      This gives us three equations in three variables, which can be solved to give the rates p, c and f.

      We can then calculate the time taken for the 4th team:

      T(6p + 10c + f) = 1

      T = 1/(6p + 10c + f)

      The following Python program runs in 74ms. (Internal runtime is 186µs).

      from enigma import (Matrix, Rational, printf)
      
      Q = Rational()
      
      # construct the equations
      eqs = [
        #  p   c   f   1/time
        ((11,  6,  1), Q(1, 56)), # team 1 (11p + 6c) takes 56 minutes
        (( 5, 12,  1), Q(1, 35)), # team 2 (5p + 12c) takes 35 minutes
        ((15,  4,  1), Q(1, 60)), # team 3 (15p + 4c) takes 60 minutes
      ]
      
      (p, c, f) = Matrix.linear(eqs, field=Q)
      printf("[p={p} c={c} f={f}]")
      
      # calculate time for team 4 (6p + 10c)
      t = Q(1, 6*p + 10*c + f)
      printf("team 4 takes {t} minutes")
      

      Solution: It would take the 4th team 42 minutes to empty the hold.

      We get:

      p = 1/840
      c = 1/336
      f = −11/840

      1/T = 6/840 + 10/336 − 11/840
      1/T = 1/42
      T = 42

      So the water is coming in at 11/840 units/min, and a pirate with a pail can empty out 1/840 units/min. So 11 pirates with pails would be able to remove the water at the rate it was coming in. A pirate with a cup can empty 1/336 units/min (so a cup can bail more than a pail), and 2.5 pirates with cups could remove water at the rate it is coming in.

      Like

  • Unknown's avatar

    Jim Randell 7:15 am on 4 January 2026 Permalink | Reply
    Tags:   

    Teaser 3302: Challenging grandson 

    From The Sunday Times, 4th January 2026 [link] [link]

    My grandson likes making puzzles; his most recent one started with a four-digit number with all the digits different. He added up all the possible two-digit combinations of these four digits (e.g., if the digits were 0, 1, 2 and 3 he would add 01 + 02 + 03 + 10 + 12 + …). This total divided exactly into his four-digit number.

    Not satisfied with this, he said he had added an extra digit (different from all the others) to the end of his number to make a five-digit number. The five-digit number was an exact multiple of the total of all the two-digit combinations of these five digits. He said that if he told me how many digits were in this multiple, I would be able to work out his five-digit number.

    What was his five-digit number?

    [teaser3302]

     
    • Jim Randell's avatar

      Jim Randell 7:35 am on 4 January 2026 Permalink | Reply

      When constructing the sum of the combinations, with 4 digits, each can be paired with 3 others, and so appears 3 times in the tens column and 3 times in the units column. The total sum is therefore:

      33 × sum(digits)

      And starting with 5 digits the total sum is:

      44 × sum(digits)

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

      from enigma import (irange, nsplit, div, ulambda, filter_unique, ndigits, printf)
      
      # generate possible scenarios
      def generate():
        digits = set(irange(0, 9))
      
        # the first number is a 4-digit multiple of 33 with distinct digits
        for n1 in irange.round(1000, 9999, step=33, rnd='I'):
          ds = nsplit(n1)
          if len(set(ds)) != 4: continue
          # calculate the sum of the 2-digit combinations
          t1 = sum(ds)
          s1 = 33 * t1
          # n1 is a multiple of s1
          k1 = div(n1, s1)
          if k1 is None: continue
      
          # add in an extra digit to form the second number
          for d in digits.difference(ds):
            n2 = 10*n1 + d
            # calculate the sum of the 2-digit combinations
            s2 = 44 * (t1 + d)
            # n2 is a multiple of s2
            k2 = div(n2, s2)
            if k2 is None: continue
      
            # return (number, sum, multiple) values for each number
            printf("[n1={n1} s1={s1} k1={k1} -> n2={n2} s2={s2} k2={k2}]")
            yield ((n1, s1, k1), (n2, s2, k2))
      
      # find solutions unique by number of digits in k2
      f = ulambda("((n1, s1, k1), (n2, s2, k2)): ndigits(k2)")
      ss = filter_unique(generate(), f).unique
      
      # output solutions
      for ((n1, s1, k1), (n2, s2, k2)) in ss:
        printf("n2={n2} [s2={s2} k2={k2}; n1={n1} s1={s1} k1={k1}]")
      

      Solution: [To Be Revealed]

      Like

    • Ruud's avatar

      Ruud 8:51 am on 4 January 2026 Permalink | Reply

      import peek
      import istr
      import collections
      
      collect = collections.defaultdict(list)
      for n4 in istr.range(1000, 10000):
          if not n4.all_distinct():
              continue
          s4 = 33 * sum(n4)
          if not n4.is_divisible_by(s4):
              continue
          for n1 in istr.range(10):
              if n1 in n4:
                  continue
              n5 = n4 | n1
              s5 = 44 * sum(n5)
              if n5.is_divisible_by(s5):
                  multiple = n5 / s5
                  collect[len(multiple)].append(n5)
      
      for n5s in collect.values():
          if len(n5s) == 1:
              peek(n5s[0])
      

      .

      Like

      • ruudvanderham's avatar

        ruudvanderham 7:15 pm on 4 January 2026 Permalink | Reply

        Slightly faster:

        import peek
        import istr
        import collections
        
        collect = collections.defaultdict(list)
        for p4 in istr.permutations(range(10), 4):
            n4 = istr.join(p4)
            s4 = 33 * sum(n4)
            if not n4.is_divisible_by(s4):
                continue
            for n1 in set(istr.range(10)) - set(p4):
                n5 = n4 | n1
                s5 = 44 * sum(n5)
                if n5.is_divisible_by(s5):
                    multiple = n5 / s5
                    collect[len(multiple)].append(n5)
        
        for n5s in collect.values():
            if len(n5s) == 1:
                peek(n5s[0])
        

        Like

    • Frits's avatar

      Frits 11:43 am on 4 January 2026 Permalink | Reply

      from itertools import permutations
      
      # the extra digit must be zero due to the alternating sum rule
      d = dict()
      for A, B, C in permutations(range(1, 10), 3):
        # divisibility of 11 rule (alternating digit sum is 0 or a multiple of 11)
        if (D := (A + C - B) % 11) in {0, A, B, C, 10}: continue
        # calculate sum of digits (must be a multiple of 3)
        if (s := A + B + C + D) % 3: continue
        # calculate first multiple  
        m1, r = divmod(ABCD := 1000 * A + 100 * B + 10 * C + D, 33 * s)
        if r or m1 % 2: continue  # as m2 = 7.5 * m1
        # number of digits in second multiple must be unique
        d[nd2] = 0 if (nd2 := len(str(m2 := int(7.5 * m1)))) in d else 10 * ABCD
        
      for k, v in d.items():
        if v:
          print("answer:", v)
      

      Like

      • Frits's avatar

        Frits 12:47 pm on 4 January 2026 Permalink | Reply

        from itertools import permutations
        
        # the extra digit must be zero due to the alternating sum rule
        d = dict()
        digits = set(range(1, 10))    
        # D must be even as 10 * ABCD must be divisible by 44
        for D in [2, 4, 6, 8]:
          for A, C in permutations(digits - {D}, 2):
            # divisibility of 11 rule (alternating digit sum is 0 or a multiple of 11)
            if (B := (A + C - D) % 11) in {0, A, C, D, 10}: continue
            # calculate sum of digits (must be a multiple of 3)
            if (s := A + B + C + D) % 3: continue
            # calculate first multiple  
            m1, r = divmod(ABCD := 1000 * A + 100 * B + 10 * C + D, 33 * s)
            if r or m1 % 2: continue  # as m2 = 7.5 * m1
            # number of digits in second multiple must be unique
            d[nd2] = 0 if (nd2 := len(str(int(7.5 * m1)))) in d else 10 * ABCD
              
        for k, v in d.items():
          if v:
            print("answer:", v)
        

        Like

  • Unknown's avatar

    Jim Randell 11:16 am on 2 January 2026 Permalink | Reply
    Tags:   

    Teaser 2463: [Single-digit multiples] 

    From The Sunday Times, 6th December 2009 [link]

    I have used each of the digits 0 to 9 once each to make a one-digit number, a two-digit number, a three-digit number and a four-digit number. The fourth number is a single-digit multiple of the third; the third number is a single-digit multiple of the second; and the second is a single-digit multiple of the first.

    What is the four-digit number?

    This puzzle was originally published with no title.

    [teaser2463]

     
    • Jim Randell's avatar

      Jim Randell 11:17 am on 2 January 2026 Permalink | Reply

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

      It runs in 85ms. (Internal runtime of the generated code is 8.8ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "div(GHIJ, DEF) < 10"
      "div(DEF, BC) < 10"
      "div(BC, A) < 10"
      
      --answer="(A, BC, DEF, GHIJ)"
      

      Solution: The 4-digit number is 3402.

      We have:

      3402 = 6 × 567
      567 = 7 × 81
      81 = 9 × 9
      9

      And between them the numbers 9, 81, 567, 3402 use each of the digits 0-9 exactly once.

      Like

    • Ruud's avatar

      Ruud 2:21 pm on 2 January 2026 Permalink | Reply

      Here is recursive solution:

      import istr
      
      def expand(n="", used=[]):
          for d in range(1, 10):
              next_n = d * istr(n if n else 1)
              next_used = used + [next_n]
              if len(next_n) == len(n) + 1 and istr("").join(next_used).all_distinct():
                  if len(next_n) == 4:
                      print(*next_used)
                  else:
                      expand(n=next_n, used=next_used)
      
      expand()
      

      Like

    • GeoffR's avatar

      GeoffR 4:54 pm on 2 January 2026 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9: a; var 0..9: b; var 0..9: c; var 0..9: d;
      var 1..9: e; var 0..9: f; var 0..9: g;
      var 1..9: h; var 0..9: i; var 1..9: j;
      
      constraint all_different ([a, b, c, d, e, f, g, h, i, j]);
      
      var 1000..9999: abcd == 1000*a + 100*b + 10*c + d;
      var 100..999: efg == 100*e + 10*f + g;
      var 10..99: hi == 10*h + i;
      var 2..9: d1; var 2..9: d2; var 2..9: d3; 
      
      % Four, three and two digut number constraints
      constraint abcd div efg == d1 /\ d1 * efg == abcd;
      constraint efg div hi == d2 /\ d2 * hi == efg;
      constraint hi div j == d3 /\ d3 * j == hi;
      
      solve satisfy;
      
      output ["The four-digit number was " ++ show(abcd)
      ++ "\n" ++ "[a, b, c, d, e, f, g, h, i, j] = "
      ++ "\n" ++ show([a, b, c, d, e, f, g, h, i, j]) ];
      
      % The four-digit number was 3402
      % [a, b, c, d, e, f, g, h, i, j] = 
      % [3, 4, 0, 2, 5, 6, 7, 8, 1, 9]
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 5:33 am on 28 December 2025 Permalink | Reply
    Tags:   

    Teaser 3301: Round the table 

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

    I recently attended a celebration with fewer than 100 family and friends. We sat round a large oval table with one seat for each person. I said to Liam that he could pick any seat, but it might be interesting to work out how many different arrangements were possible for people round the table. He replied: “I’ll leave that to you Grandad”, then (mischievously) added “but remember I always sit next to you and to Granny”.

    The restriction that I must sit next to Liam divided the number of possible arrangements by a certain prime number. The further restriction that he must also be next to Granny caused further division by a prime number with a different number of digits.

    What, in ascending order, are the number of people round the table and the two prime numbers?

    This completes the archive of puzzles from 2025. There is now a complete archive of puzzles (and solutions!) from December 2009 onwards (plus a lot of earlier puzzles).

    [teaser3301]

     
    • Jim Randell's avatar

      Jim Randell 5:41 am on 28 December 2025 Permalink | Reply

      With n people there are factorial(n) free arrangements (assignments of people to seats). (Or, if we only care about who is sitting next to who, and not about the absolute positions the number of free arrangements is factorial(n − 1) / 2 (i.e. is reduced by a factor of 2n)).

      We can generate these by seating Liam, then Grandad, then Granny and then all the remaining (n − 3) guests.

      After Liam has chosen his seat, for a free arrangement, Grandad has a choice of the remaining (n − 1) seats, but if we require Grandad to sit next to Liam, there is only a choice of 2 seats.

      So this restriction reduces Grandad’s choice from (n − 1) to 2, so the number of arrangements is reduced by a factor of:

      p1 = (n − 1) / 2

      And this must be a prime number.

      Similarly, if Granny had a free choice, she could could choose from (n − 2) seats, but if we require her to sit next to Liam, she must “choose” from only 1 seat.

      This further reduces the number of arrangements by a factor of:

      p2 = (n − 2)

      And this is also a prime number (with a different number of digits to p2).

      If n is less than 100, then p2 must have 2 digits and p1 must have 1 digit. So there are only a few cases to consider, and a manual solution is straighforward.


      This Python program starts from the total number of people n.

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

      from enigma import (irange, div, is_prime, ndigits, printf)
      
      # consider number of people around the table
      for n in irange(3, 99):
        # first restriction divides possibilities by a prime number
        p1 = div(n - 1, 2)
        if not is_prime(p1): continue
        # second restriction further divides possibilities by a prime number
        p2 = n - 2
        if not is_prime(p2): continue
        # the primes have different numbers of digits
        if not (ndigits(p1) != ndigits(p2)): continue
        # output solution
        ss = sorted([n, p1, p2])
        printf("n={n} -> p1={p1} p2={p2} {ss}")
      

      Or, shorter and faster, this Python program considers possible 1-digit prime values for p1.

      It has an internal runtime of 54µs.

      from enigma import (primes, printf)
      
      # p1 = (n - 1) / 2 is prime with fewer digits than p2 = n - 2
      for p1 in primes.between(2, 9):
        n = 2 * p1 + 1
        p2 = n - 2
        if not (p2 > 9 and primes.is_prime(p2)): continue
        # output solution
        ss = [p1, p2, n]
        printf("n={n} -> p1={p1} p2={p2} {ss}")
      

      Solution: The numbers are: 7, 13, 15.

      There are 15 people around the table.

      So the total number of possible free arrangements is factorial(15) = 1307674368000.

      But, requiring Grandad to sit next to Liam reduces Grandad’s choices from 14 to 2, so divides the number of possibilities by 7 (to give 186810624000 arrangements).

      Further requiring Granny to also sit next to Liam reduces Granny’s choices from 13 to 1, so divides the number of possibilities by 13 (to give 14370048000 arrangements).

      (If we only care about the relative seating arrangements (who sits next to who), then the number of arrangements is reduced by a factor of 2n = 30, but this does not affect the values of the divisions).


      Manually:

      p1 is a 1-digit prime, and p2 is a 2-digit prime such that:

      p1 = (n − 1) / 2
      p2 = (n − 2)

      p2 = 2 p1 − 1
      n = p2 + 2

      and:

      p2 > 9

      2 p1 − 1 > 9
      2 p1 > 10
      p1 > 5

      Considering 1-digit primes for p1 that are greater than 5:

      p1 = 7 ⇒ p2 = 13, n = 15 [solution]

      Like

    • Ruud's avatar

      Ruud 8:14 am on 28 December 2025 Permalink | Reply

      import peek
      import istr
      from math import factorial
      
      for n in range(3, 100):
          f1 = istr(factorial(n) / (n * 2 * factorial(n - 2)))
          f2 = istr(factorial(n - 2) / factorial(n - 3))
          if f1.is_prime() and f2.is_prime() and len(f1) != len(f2):
              peek(sorted((n, f1, f2)))
      
      

      Like

    • GeoffR's avatar

      GeoffR 9:01 am on 30 December 2025 Permalink | Reply

      An AI solution, nicely commented by AI.

      # ST 3301 by Perplexity AI
      
      import math
      
      def is_prime(num):
          if num < 2:
              return False
          for i in range(2, int(math.sqrt(num)) + 1):
              if num % i == 0:
                  return False
          return True
      
      def digits(num):
          return len(str(num))
      
      # Solve the teaser: n < 100 people around oval table (circular)
      # Total arrangements: (n-1)!
      # 1st restriction (Liam next to Grandad): divides by prime p1 with D1 digits
      # 2nd restriction (Liam also next to Granny): further divides
      # by prime p2 with D2 != D1 digits
      
      for n in range(3, 100):  # At least 3 people (Grandad, Liam, Granny)
          # Fix Grandad's position to handle circular symmetry
          total = math.factorial(n - 1)
          
          # 1st restriction: Liam next to Grandad (2 choices for Liam's seat)
          restr1 = 2 * math.factorial(n - 2)
          div1 = total // restr1  # Should be integer prime
          if total % restr1 != 0 or not is_prime(div1):
              continue
          
          # 2nd restriction: Liam next to Grandad AND Granny
          # The three occupy 3 consecutive seats with Grandad fixed
          # 2 possible blocks (Liam-Granny or Granny-Liam to Grandad's sides)
          restr2 = 2 * math.factorial(n - 3)
          div2 = restr1 // restr2  # Further division from restr1
          if restr1 % restr2 != 0 or not is_prime(div2):
              continue
          
          # Check different number of digits
          if digits(div1) != digits(div2):
              print(f"n={n}, First prime={div1} ({digits(div1)} digits)")
              print(f"Second prime={div2} ({digits(div2)} digits)")
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:19 am on 26 December 2025 Permalink | Reply
    Tags: ,   

    Teaser 2465 : [Christmas bonus] 

    From The Sunday Times, 20th December 2009 [link]

    Pat’s company employs six staff: they joined on six consecutive Christmases and have stayed ever since. This Christmas, he is giving them each a bonus in the form of vouchers, each worth a whole number of pounds, with one red voucher for each year of service for a man and one green voucher (worth £1 more) for each year of service for a woman. The value of all the year’s vouchers is £500. Ms Jones joined more than two years after Mr Smith, but their bonuses have the same total value.

    What is that common value?

    This puzzle was originally published with no title.

    [teaser2465]

     
    • Jim Randell's avatar

      Jim Randell 11:20 am on 26 December 2025 Permalink | Reply

      This Python program runs in 70ms. (Internal runtime is 1.7ms).

      from enigma import (irange, inf, subsets, div, printf)
      
      # choose male (= 0) and female (= 1) values (longest serving -> shortest serving)
      for vs in subsets([0, 1], size=6, select='M'):
        # find possible (i, j) values for Mr S and Ms J
        ijs = list((i, j) for (i, j) in subsets(irange(6), size=2) if j >= i + 3 and vs[i] == 0 and vs[j] == 1)
        if not ijs: continue
      
        # consider increasing values for the male voucher
        for x in irange(1, inf):
          # calculate the years of service for the longest serving employee (>= 6)
          n = 500 + sum(i * (x + v) for (i, v) in enumerate(vs))
          d = 6 * x + sum(vs)
          if n < 6 * d: break
          y = div(n, d)
          if y is None: continue
      
          # calculate actual gift amounts
          gs = list((y - i) * (x + v) for (i, v) in enumerate(vs))
      
          # find shared values for Mr S and Ms J
          for (i, j) in ijs:
            if gs[i] == gs[j]:
              # output solution
              printf("vs={vs}, y={y}, x={x}, gs={gs}, i={i}, j={j}")
      

      Solution: The common value is £80.

      The red vouchers (male) are worth £4 each. The green vouchers (female) are worth £5 each.

      The amounts given to the employees are:

      21 years, female → £105
      20 years, male → £80 (Mr Smith)
      19 years, female → £95
      18 years, male → £72
      17 years, male → £68
      16 years, female → £80 (Ms Jones)
      total = £500

      Like

    • Ruud's avatar

      Ruud 1:17 pm on 26 December 2025 Permalink | Reply

      Extreme brute force:

      import peek
      import itertools
      import istr
      
      for n in range(1, 30):
          for bonus in range(100):
              for is_womans in itertools.product((False, True), repeat=6):
                  if sum(k * (bonus + is_woman) for k, is_woman in enumerate(is_womans, n)) == 500:
                      for jones, smith in itertools.product(range(6), repeat=2):
                          if not is_womans[jones] and is_womans[smith] and jones > smith + 2 and (jones + n) * bonus == (smith + n) * (bonus + 1):
                              peek((jones + n) * bonus, (smith + n) * (bonus + 1))
      

      Like

  • Unknown's avatar

    Jim Randell 8:21 am on 24 December 2025 Permalink | Reply
    Tags:   

    Teaser 2466: [Cracker game] 

    From The Sunday Times, 27th December 2009 [link]

    In my Christmas cracker, there were four cards, each with eight numbers on:

    first: 1, 3, 5, 7, 9, 11, 13, 15;
    second: 2, 3, 6, 7, 10, 11, 14, 15;
    third: 4, 5, 6, 7, 12, 13, 14, 15;
    fourth: 8, 9, 10, 11, 12, 13, 14, 15.

    You ask someone to choose a number up to 15, then state which cards it is on; then (by a simple trick) you tell them their number.

    I decided to make a version with more numbers, using the same technique but more than twice as many cards. It turned out that the numbers asked for below had equal sums of digits:

    (a) How many numbers are on each card?
    (b) What is the largest number?

    This puzzle was originally published with no title.

    [teaser2466]

     
    • Jim Randell's avatar

      Jim Randell 8:21 am on 24 December 2025 Permalink | Reply

      If we make a grid of which cards the number appears on (0 = doesn’t appear; 1 = does appear) we get this:

      We see that the rows give a representation of the number in binary.

      So when the cards containing the number are indicated we can reconstruct the original number by adding 1 if the 1st card is indicated, 2 if the 2nd card is indicated, 4 if the 3rd card is indicated, and 8 if the 4th card is indicated.

      With n cards we can distinguish (2^n − 1) numbers (if 0 is not used). And each card has 2^(n − 1) numbers on it.

      So we can look for values where these numbers have the same digit sum.

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

      from enigma import (irange, inf, dsum, printf)
      
      # how many numbers can we distinguish with n cards?
      N = lambda n: (2**n) - 1
      
      # how many numbers appear on each card?
      C = lambda n: 2**(n - 1)
      
      # consider n >= 4
      for n in irange(4, inf):
        (Nn, Cn) = (N(n), C(n))
        f = (dsum(Nn) == dsum(Cn))
        printf("n={n}: N = {Nn}; C = {Cn}; f = {f}", f=int(f))
        # are we done?
        if f and n > 8: break
      

      Solution: (a) There are 4096 numbers of each card; (b) The largest number is 8191.

      And:

      4 + 0 + 9 + 6 = 19
      8 + 1 + 9 + 1 = 19

      There are 13 cards (each with 4096 numbers up to 8191 on them).


      However this solution is not unique.

      The next smallest solution is with 67 cards:

      There are 73786976294838206464 numbers on each card, and the largest number is 147573952589676412927.

      dsum(73786976294838206464) = 109
      dsum(147573952589676412927) = 109

      Like

    • ruudvanderham's avatar

      ruudvanderham 1:09 pm on 24 December 2025 Permalink | Reply

      import istr
      import peek
      
      for number_of_cards in istr.count(8):
          if sum(number_of_numbers := 2**number_of_cards - 1) == sum(largest_number := 2 ** (number_of_cards - 1)):
              peek(number_of_cards, number_of_numbers, largest_number)
              break
      

      Like

  • Unknown's avatar

    Jim Randell 6:39 am on 21 December 2025 Permalink | Reply
    Tags: by: Peter Rowlett   

    Teaser 3300: A faulty bet 

    From The Sunday Times, 21st December 2025 [link] [link]

    I offer my friends a bet: they pay £1 to play, then draw a card at random from a standard 52-card deck. If they draw the Queen of Hearts, I pay them a £30 prize, otherwise I keep their £1.

    Although I have many friends, I have a reasonable chance of avoiding paying a prize. But I make a mistake! As I go around the group offering the bet to each friend, I forget to take their chosen card back and shuffle it into the deck, meaning each new bet is made with fewer cards. As a result, my chances of having to pay out are nearly 50 per cent greater. In fact if I had just one more friend, then my chances of having to pay out would be more than 50 per cent greater than if I hadn’t made the mistake.

    How many friends did I have in the group?

    [teaser3300]

     
    • Jim Randell's avatar

      Jim Randell 6:52 am on 21 December 2025 Permalink | Reply

      This Python program calculates the chances of not paying out for increasing numbers of friends. If this probability is P, then the probability of paying out is (1 − P).

      It then checks the ratio of the probabilities in the variants, until two consecutive ratios bracket 1.5.

      It runs in 75ms. (Internal runtime is 188µs).

      from enigma import (Rational, irange, tuples, printf)
      
      Q = Rational()
      
      # generate probabilities of paying out for each variant
      # by considering increasing numbers of friends (N)
      def generate(T):
        # probability of _not_ paying out in each of the variants
        P1 = P2 = 1
        # number of cards in variant 2
        X = T
        for N in irange(1, 52):
          P1 *= Q(T - 1, T)  # choice from T cards
          P2 *= Q(X - 1, X)  # choice from X cards
          X -= 1  # chosen card is _not_ replaced
          # return probabilities of paying out: (<num-friends>, <p1>, <p2>, <p2/p1>)
          (p1, p2) = (1 - P1, 1 - P2)
          r = Q(p2, p1)
          printf("[N={N}: p1={p1:.3f} p2={p2:.3f} -> r={r:.3f}]", p1=float(p1), p2=float(p2), r=float(r))
          yield (N, p1, p2, r)
      
      # look for consecutive group sizes with ratios that bracket 1.5
      for ((N, _, _, r1), (_, _, _, r2)) in tuples(generate(52), 2):
        if r1 < 1.5 < r2:
          printf("group size = {N}")
          break
      

      Solution: There are 46 friends in the group.

      With 46 friends the chance of paying out in the second variant is 1.498× the chance in the first (just less than 50% more).

      With 47 friends the chance of paying out in the second variant is 1.510× the chance in the first (just over 50% more).

      Like

      • Jim Randell's avatar

        Jim Randell 6:19 pm on 21 December 2025 Permalink | Reply

        Or using the formulae for the probabilities and the numerical solver in the enigma.py library:

        In the first variant the probability of not paying out in each round is 51/52, so with n rounds it is:

        P1 = (51/52)^n

        So the probability of paying out is:

        p1 = 1 − P1
        p1 = 1 − (51/52)^n

        In the second variant the probability of not paying out in n rounds is:

        P2 = 51/52 × 50/51 × 49/50 × … × (52 − n) / (53 − n)
        P2 = (52 − n)/52 = 1 − n/52

        And so the probability of paying out is:

        p2 = 1 − P2
        p2 = n/52

        from enigma import (fdiv, find_values, intf, printf)
        
        # formulae for p1, p2 in terms of n (size of group)
        p1 = lambda n: 1.0 - fdiv(51, 52)**n
        p2 = lambda n: fdiv(n, 52)
        
        # ratio formula
        f = lambda n: fdiv(p2(n), p1(n))
        
        # find value when r = 1.5
        for r in find_values(f, 1.5, 1, 52):
          n = r.v
          printf("r({n:.3f}) = 1.5")
        
          n = intf(n)
          if f(n) < 1.5 < f(n + 1):
            printf("group size = {n}")
        

        Treating the function as continuous we find that the ratio achieves a value of 1.5 at:

        n ≈ 46.189

        and so:

        r(46) ≈ 1.498
        r(47) ≈ 1.510

        Like

  • Unknown's avatar

    Jim Randell 8:44 am on 16 December 2025 Permalink | Reply
    Tags:   

    Teaser 2464 : [Trapezium plots] 

    From The Sunday Times, 13th December 2009 [link]

    George and Martha have a trapezium-shaped garden [*]. The southern border (running east-west) is 100 metres long, as is the western border (north-south). The eastern border (also north-south) is a whole number of metres long (more than 100, but less than 200) and the fourth border is also a whole number of metres long. Coincidentally, if you replace “100” with “1000” and “200” with “2000”, exactly the same can be said of the park over the road.

    “Then the park’s sides are all 10 times those of our garden”, Martha said. “Actually, they are not”, George replied.

    How long is the park’s longest side?

    [*] Trapezium = a quadrilateral with two parallel sides.

    This puzzle was originally published with no title.

    [teaser2464]

     
    • Jim Randell's avatar

      Jim Randell 8:44 am on 16 December 2025 Permalink | Reply

      We can construct the shape of the plots by taking a Pythagorean triangle (x, y, z), and then constructing the square on the y side to give a trapezium with sides (x + y, y, y, z).

      (I believe in US terminology this shape would be referred to as a trapezoid).

      This Python program collects possible triangles with y = 100 (for the garden) or y = 1000 (for the park), and then looks for a pair where not all sides are in the ratio 10:1.

      It runs in 80ms. (Internal runtime is 150µs).

      from enigma import (cproduct, printf)
      import pells
      
      # generate candidate triangles
      def triangles(y):
        for (x, z) in pells.diop_quad(1, -1, -y*y):
          if not (x < y): break
          if x > 0:
            yield (x, y, z)
      
      # find candidate dimensions for the garden and the park
      gs = list(triangles(100))
      ps = list(triangles(1000))
      
      # find garden/park combinations that are not in 1:10 ratio
      for ((x1, y1, z1), (x2, y2, z2)) in cproduct([gs, ps]):
        if (x2 == 10 * x1 and z2 == 10 * z1): continue
        printf("garden: S={y1}, W={y1}, E={E}, Z={z1}", E=x1 + 100)
        printf("park: S={y2}, W={y2}, E={E}, Z={z2}", E=x2 + 1000)
        printf()
      

      Solution: The longest side of the park measures 1225 m.

      The garden has dimensions: 175, 100, 100, 125.

      The park has dimensions: 1225, 1000, 1000, 1025.

      Like

      • Jim Randell's avatar

        Jim Randell 2:50 pm on 16 December 2025 Permalink | Reply

        Alternatively, we can just look at possible dimensions of the park, and reject any where each side is divisible by 10.

        The following code has an internal runtime of 98µs.

        from enigma import printf
        import pells
        
        # generate candidate triangles
        def triangles(y):
          for (x, z) in pells.diop_quad(1, -1, -y*y):
            if not (x < y): break
            if x > 0:
              yield (x, y, z)
        
        # find candidate dimensions for the park
        for (x, y, z) in triangles(1000):
          # reject dimensions that are all divisible by 10
          if 0 == x % 10 == y % 10 == z % 10: continue
          # output solution
          printf("park: S={y}, W={y}, E={E}, Z={z}", E=x + 1000)
        

        Like

    • Ruud's avatar

      Ruud 1:05 pm on 16 December 2025 Permalink | Reply

      import istr
      
      s = {}
      for n in (100, 1000):
          s[n] = {(a ** (1 / 2) * 1000 / n, b * 1000 / n) for b in range(n + 1, 2 * n) if istr.is_square((a := (b - n) ** 2 + n**2))}
      
      print([max(a, b) for a, b in s[1000] - s[100]])
      

      Like

    • Ellen Napier's avatar

      Ellen Napier 4:13 pm on 26 December 2025 Permalink | Reply

      Specifically, a Right Trapezoid (two right angles), which is implied by the compass directions given.

      Like

  • Unknown's avatar

    Jim Randell 7:54 am on 14 December 2025 Permalink | Reply
    Tags:   

    Teaser 3299: Who wins mostly? 

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

    The teams in our local football league play each other once during the season, getting three points for a win and one for a draw. On the last day of the season each team played a game and then I worked out the league table with the teams in alphabetical order. Here is part of three rows of that league table in which digits have consistently been replaced by letters, with different letters used for different digits.

    How many teams are there in the league and what number is LOST?

    [teaser3299]

     
    • Jim Randell's avatar

      Jim Randell 7:56 am on 14 December 2025 Permalink | Reply

      It is the end of the season, and so each team has played each of the other teams once. So, for each team, the sum of the “won”, “drawn” and “lost” values must be one less than the total number of teams in the league (and if each team plays a match on the last day, then the number of teams must be even).

      Also in order to win a game, the winning team must score at least one goal. So the value in “goals for” must be greater than or equal to “won”. And the value in “goals against” must be greater than or equal to “lost”.

      This is sufficient analysis to provide a single candidate solution to the puzzle.

      Here is a programmed solution using the [[ SubstitutedExpression ]] solver from the enigma.py library. (Which took me less than 2 minutes to write).

      It executes in 81ms, and the internal runtime of the generated code is 439µs.

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # end of the season
      "W + H + O == W + I + N == M + O + S"
      
      # 3 points for win, 1 point for a draw
      "3 * W + I = S"
      "3 * M + O = T"
      
      # "goals for" >= "won"; "goals against" >= "lost"
      "L >= M"
      "Y >= S"
      
      --answer="(W + H + O + 1, LOST)"
      --template=""
      

      Solution: There are 12 teams in the league. LOST = 3467.

      Like

    • Frits's avatar

      Frits 5:51 pm on 14 December 2025 Permalink | Reply

      # n = number of teams - 1 = number of games per team
      # 8 <= n = 3W + M + I + O   with 0 < W < 3 and I <= 5 (3W + I < Y)
      # n != 17 otherwise 
      #   if W = 1 no correct values exist for  {H, O, I, N} 
      #   if W = 2 then {H, O, I, N} = {6, 7, 8, 9} but 3W + I < 10 --> invalid
      
      dgts = set(range(10))
      # consider odd number of games per team
      for n in range(9, 17, 2):
        for W in range(1, 3):       # W != 3 as Y > (S = 3.W + I)
          for I in range(6):        # I != 6 as S < 9
            if len(n6 := dgts - {W, I, (N := n - W - I), (S := 3 * W + I)}) != 6:
              continue
            for M in range(1, 4):
              H, O, T = M + 2 * W + I, n - M - S, n + 2 * M - S
              if len(LY := list(n6 - {M, H, O, T})) != 2: continue
              # assert L > M, Y > S
              for L, Y in [LY, LY[::-1]]:
                if L <= M or Y <= S: continue
                LOST = 1000 * L + 100 * O + 10 * S + T  
                print(f"{n + 1} teams, {LOST = }")
      

      Like

  • Unknown's avatar

    Jim Randell 8:48 am on 9 December 2025 Permalink | Reply
    Tags:   

    Teaser 2509: [Lottery numbers] 

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

    I won £10 on the lotto with three correct numbers (in the range 1 to 49).

    I then calculated the difference between two six-figure numbers: one formed by writing my three winning numbers in descending order; the other by writing them in ascending order. I multiplied this difference by three and divided the answer by the sum of my winning numbers. I then divided the new answer by one of my winning numbers. The answer was another of my three winning numbers.

    What, in ascending order, were my three winning numbers?

    This puzzle was originally published with no title.

    This completes the archive of Teaser puzzles originally published in  2010. There is now a complete archive (with solutions) of puzzles from 2010 – 2025, as well as lots of earlier puzzles going back to the start of Sunday Times Teaser puzzles. I will continue to add new puzzles as time allows.

    [teaser2509]

     
    • Jim Randell's avatar

      Jim Randell 8:49 am on 9 December 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # suppose the numbers are: AB CD EF (in order)
      --distinct=""
      --invalid="5|6|7|8|9,ACE"
      "A <= C" "C <= E"
      "0 < AB < CD < EF < 50"
      
      # the two 6-digit numbers are: ABCDEF, EFCDAB
      # and 3 times the difference divided by the sum of the original numbers
      # is the same as the product of two of the original numbers
      "div(abs(ABCDEF - EFCDAB) * 3, AB + CD + EF) in { AB * CD, AB * EF, CD * EF }"
      
      # [optional] tidy up output
      --template="(AB CD EF)"
      --solution=""
      --verbose="1-H"
      

      Solution: The three winning numbers were: 32, 33, 36.

      These are assembled into the two 6-digit numbers:

      ascending: 323336
      descending: 363332

      And the dividing 3 times the difference of these by the sum of the original numbers gives:

      3 × (363332 − 323336) / (32 + 33 + 36) = 1188

      And 1188 is the product of two of the original numbers:

      1188 = 33 × 36

      Like

    • Ruud's avatar

      Ruud 10:33 am on 9 December 2025 Permalink | Reply

      import istr
      
      for numbers in istr.combinations(range(1, 50), 3):
          n_descending = istr("").join(sorted(numbers, reverse=True))
          n_ascending = istr("").join(numbers)
          n_difference = n_descending - n_ascending
          if (n_difference * 3).is_divisible_by(sum(numbers)) and any(n0 * n1 == n_difference * 3 / sum(numbers) for n0, n1 in istr.combinations(numbers, 2)):
              print(numbers)
      

      Like

    • Frits's avatar

      Frits 7:58 pm on 10 December 2025 Permalink | Reply

      The sum of the winning numbers must be 101.

      See Teaser 2509

      Like

      • Jim Randell's avatar

        Jim Randell 10:24 am on 11 December 2025 Permalink | Reply

        This analysis allows a nice compact program:

        # for numbers (x, y, z) we have:
        #
        #   3.9999.(z - x) = (x + y + z).{ x.y | x.z | y.z }
        #
        # 101 is a prime factor of 9999 that can only appear in (x + y + z)
        # and since (x + y + z) in [6, 144] it follows:
        #
        #   (x + y + z) = 101
        #
        # leaving:
        #
        #   3.99.(z - x) = { x.y | x.z | y.z }
        
        from enigma import (decompose, printf)
        
        for (x, y, z) in decompose(101, 3, increasing=1, sep=1, min_v=1, max_v=49):
          if 297 * (z - x) in { x * y, x * z, y * z }:
            printf("({x}, {y}, {z})")
        

        Like

  • Unknown's avatar

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

    Teaser 3298: Bletchley Park 

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

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

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

    DKCCVTCSZQRZYTAZXTTX

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

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

    What was the decoded message?

    [teaser3298]

     
    • Jim Randell's avatar

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

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

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


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

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

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

      Like

      • Frits's avatar

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

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

        Like

      • Jim Randell's avatar

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

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

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

        It runs in 5.03s (using PyPy).

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

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

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

        Like

  • Unknown's avatar

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

    Teaser 2517: [Christmas alphametic] 

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

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

    XMAS + TREE = FAIRY

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

    What number is XMAS?

    This puzzle was originally published with no title.

    [teaser2517]

     
    • Jim Randell's avatar

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

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

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

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

      Solution: XMAS = 7209.

      The sum is: 7209 + 3655 = 10864.

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

      Like

    • Ruud's avatar

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

      To run this code, istr 1.1.12 is required.

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

      More compact, but much less efficient:

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

      Like

    • CB Root's avatar

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

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

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

      Like

      • Jim Randell's avatar

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

        Hi, thanks for leaving a comment.

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

        If you leave a comment with your code between:

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

        I will fix it up in the original comment.

        Like

    • CB Root's avatar

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

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

      [see code above]

      Like

    • GeoffR's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 2469: [Football league] 

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

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

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

    This puzzle was originally published with no title.

    [teaser2469]

     
    • Jim Randell's avatar

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

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

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

      My program adopts the following strategy:

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

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

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

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

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

      It runs in 5.3s (using PyPy).

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

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

      The full set of results is:

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

      The final order was:

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

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

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

      At the intermediate stage the following games were unplayed:

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

      Giving the following orders:

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

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

      or (if E v F is unplayed):

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

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

      Like

  • Unknown's avatar

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

    Teaser 3297: 6oker 

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

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

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

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

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

    [teaser3297]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

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

      Like

      • Frits's avatar

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

        @Jim,

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

        Like

        • Jim Randell's avatar

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

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

          Like

    • Pete Good's avatar

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

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

      Like

      • Jim Randell's avatar

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

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

        Like

    • Pete Good's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 2474: [Flipping counters] 

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

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

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

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

    This puzzle was originally published with no title.

    [teaser2474]

     
    • Jim Randell's avatar

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

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

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

      Solution: 112 counters are turned over.

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

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

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

      Like

  • Unknown's avatar

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

    Teaser 2516: [Football league] 

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

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

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

    This puzzle was originally published with no title.

    [teaser2516]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

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

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

      The full list of match outcomes and scores is:

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

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

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

      Like

    • Frits's avatar

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

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

      Like

      • Jim Randell's avatar

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

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

        Like

        • Frits's avatar

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

          @Jim,

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

          Please use:

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

          Like

  • Unknown's avatar

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

    Teaser 3296: Woolly jumpers 

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

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

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

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

    [teaser3296]

     
    • Jim Randell's avatar

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

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

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

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

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

      Solution: The number of the fifth sheep was 606.

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

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

      Liked by 1 person

      • Tony Smith's avatar

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

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

        Like

    • Frits's avatar

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

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

      Like

      • Jim Randell's avatar

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

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

        Like

        • Frits's avatar

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

          @Jim, you are right.

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

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

          Like

    • Ruud's avatar

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

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

      Like

    • Ruud's avatar

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

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

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

      Like

  • Unknown's avatar

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

    Teaser 2498: [Party trick] 

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

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

    What is the letter on the last card?

    This puzzle was originally published with no title.

    [teaser2498]

     
    • Jim Randell's avatar

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

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

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

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

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

      Like

  • Unknown's avatar

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

    Teaser 2483: [Cricket scores] 

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

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

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

    This puzzle was originally published with no title.

    [teaser2483]

     
    • Jim Randell's avatar

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

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

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

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

      The visitor’s scoresheet is one of the following:

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

      or:

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

      then:

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

      And the home team’s scoresheet is:

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

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

      Like

  • Unknown's avatar

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

    Teaser 3295: Always as the crow flies 

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

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

    Again, as the crow flies:

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

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

    [teaser3295]

     
    • Jim Randell's avatar

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

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

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

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

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

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

      Solution: The round trip is 234 miles long.


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

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

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

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

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

      The other is:

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

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

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


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

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

      This arrangement is not composed of Pythagorean triangles.


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

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

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


      The published answer is just: “234 miles”.

      Like

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