Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 10:05 am on 27 January 2026 Permalink | Reply
    Tags: by: Derek Emley   

    Teaser 2415: [Double shift] 

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

    I have written down a large number in which no digit occurs more than twice. If I were to move the last digit of the number from the end to the front of the number, then the effect would be to double the number. If I repeated the process with the new number, then the effect would be to double it again. And if I repeated the process again, the effect would be to double the number yet again.

    What is the number that I have written down?

    This puzzle was originally published with no title.

    [teaser2415]

     
    • Jim Randell's avatar

      Jim Randell 10:06 am on 27 January 2026 Permalink | Reply

      If we suppose the original number n is of the form:

      n = 10a + z
      where z is the final digit, and a is a k-digit number

      Then moving the final digit to the front gives us:

      2n = (10^k)z + a

      From which we derive that a is a k-digit number, such that:

      19a = (10^k − 2)z

      If the number contains no digit more than twice, then the longest the number can be is 20 digits long, and so we can consider k up to 19.

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

      from enigma import (irange, div, nsplit, rotate, zip_eq, seq2str, printf)
      
      # check a candidate number <n> (with digits <ds>)
      def check(n, ds):
        # check no digit occurs more than twice
        if any(ds.count(x) > 2 for x in set(ds)): return
        # record shifts that result in doublings
        ns = [n]
        # collect doubling sequences
        while True:
          n *= 2
          ds = rotate(ds, -1)
          if not zip_eq(ds, nsplit(n)): break
          ns.append(n)
        return ns
      
      # look for up to 19 digits prefixes
      for k in irange(1, 19):
        x = 10**k - 2
        # consider possible final digits
        for z in irange(1, 9):
          a = div(x * z, 19)
          if a is None: continue
          n = 10*a + z
          ds = nsplit(n)
          if len(ds) != k + 1: continue
          ns = check(n, ds)
          if ns and len(ns) >= 4:
            printf("{ns}", ns=seq2str(ns, sep=" -> ", enc=""))
      

      Solution: The number is: 105263157894736842.

      The number is 18 digits long, and uses each digit twice except 0 and 9 (which are used once each).

      Shifting the final digit to the front multiple times we get:

      105263157894736842 = n
      210526315789473684 = n × 2
      421052631578947368 = n × 4
      842105263157894736 = n × 8

      A run of 4 is the longest run we can get in base 10 (as n × 16 will not have the same number of digits as n), but there is another run of 3 we can make:

      157894736842105263 = n
      315789473684210526 = n × 2
      631578947368421052 = n × 4

      These numbers are the repetends of fractions of the form k / 19.

      Like

  • Unknown's avatar

    Jim Randell 8:11 am on 25 January 2026 Permalink | Reply
    Tags:   

    Teaser 3305: Things with scales and things to scale with 

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

    Rummaging through a cupboard I came across an old Snakes and Ladders set. The ladders (label them A to E) went from square 4 to 26, 18 to 28, 19 to 73, 22 to 60, and 41 to 98. The snakes (label them W to Z) went from square 53 to 24, 58 to 5, 72 to 33, and 90 to 66. For old times’ sake I couldn’t resist playing a game on my own. Starting from square 1, rolling the die and moving appropriately, travelling travelling up ladders and down snakes when I chanced to land on their starting squares, I happened to finish exactly on square 100. The total of all the numbers I had thrown on the die was 51.

    What was the order of all my up and down jumps? (e.g. A, Y, D).

    [teaser3305]

     
    • Jim Randell's avatar

      Jim Randell 8:56 am on 25 January 2026 Permalink | Reply

      Here is a program that attempts to constructively play games that finish exactly on square 100 and hve a throw total of 51.

      We assume that once a possible game is found, all other possible games have the same sequence of jumps. (I’m thinking about better ways to show the answer to the puzzle is unique).

      It runs in 77ms. (Internal runtime is 1.1ms).

      from enigma import (tuples, seq2str, printf)
      
      # snakes and ladders
      jumps = {
        # ladders (lower -> higher)
        (4, 26): 'A', (18, 28): 'B', (19, 73): 'C', (22, 60): 'D', (41, 98): 'E',
        # snakes (higher -> lower)
        (53, 24): 'W', (58, 5): 'X', (72, 33): 'Y', (90, 66): 'Z',
      }
      
      # linked squares
      link = dict(jumps.keys())
      
      # possible throws
      throws = [6, 5, 4, 3, 2, 1]
      
      # play the game
      # p = current position
      # t = target position
      # b = throw budget (must not be exceeded)
      # ds = dice throws
      # ps = positions
      def play(p, t, b, ds=[], ps=[]):
        ps = ps + [p]
        # are we done
        if p == t or p >= 100 or b <= 0:
          yield (ds, ps, b)
        elif b > 0:
          # make the next roll
          for d in throws:
            if not (d > b):
              # move the player
              p1 = p + d
              # perform any jump
              p2 = link.get(p1)
              # continue play
              if p2 is None:
                yield from play(p1, t, b - d, ds + [d], ps)
              else:
                yield from play(p2, t, b - d, ds + [d], ps + [p1])
      
      # find a possible game
      for (ds, ps, r) in play(1, 100, 51):
        if r == 0 and ps[-1] == 100:
          printf("throws = {ds} (sum = {t}) -> positions = {ps}", t=sum(ds))
          # find the jumps involved
          js = tuple(filter(None, (jumps.get(k) for k in tuples(ps, 2))))
          printf("jumps = {js}", js=seq2str(js, enc="[]"))
          break
      

      Solution: [To Be Revealed]

      Like

      • Jim Randell's avatar

        Jim Randell 1:28 pm on 25 January 2026 Permalink | Reply

        Here is a better solution. It is shorter, faster, can be used to experiment with different throw budgets, and finds all solutions (thereby showing the answer to the original puzzle is unique):

        We don’t care about the actual dice throws, a game is made up of a sequence of contiguous blocks (that are moved with throws of the dice) with jumps (i.e. a snake or a ladder) between the blocks. The blocks themselves can usually be made by lots of dice throws (and the starts of the jumps are sufficiently fragmented that we can always avoid any unwanted jumps).

        This Python program makes a collection of possible contiguous segments, and then joins a sequence of them together with jumps inbetween to make a game that starts at 1, ends at 100, and requires a total of 51 to be scored on the dice throws during the segments.

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

        from enigma import (defaultdict, tuples, seq2str, printf)
        
        # snakes and ladders
        jumps = {
          # ladders (lower -> higher)
          (4, 26): 'A', (18, 28): 'B', (19, 73): 'C', (22, 60): 'D', (41, 98): 'E',
          # snakes (higher -> lower)
          (53, 24): 'W', (58, 5): 'X', (72, 33): 'Y', (90, 66): 'Z',
        }
        
        # linked squares
        link = dict(jumps.keys())
        
        # possible contiguous segments; segs: start -> ends
        segs = defaultdict(list)
        seg = lambda x, y: segs[x].append(y)
        seg(1, 100)  # no jumps
        for (a, b) in jumps.keys():
          # add in 1 to the start of a jump
          seg(1, a)
          # add in end of a jump to 100
          seg(b, 100)
          # add in end of a jump to start of a jump
          for (x, y) in jumps.keys():
            if b < x: seg(b, x)
        
        # find contiguous segments from <s> to <t> with throw budget <b>
        def solve(s, t, b, ss=[]):
          # are we done?
          if s == t and b == 0:
            yield ss
          elif s < 100 and b > 0:
            # add in the next segment
            for x in segs[s]:
              d = x - s
              if not (d > b):
                yield from solve(link.get(x, x), t, b - d, ss + [(s, x)])
        
        # find games from 1 to 100 with a throw budget of B
        B = 51
        for ss in solve(1, 100, B):
          # find jumps used
          js = tuple(jumps[(b, x)] for ((a, b), (x, y)) in tuples(ss, 2))
          printf("{B}: {ss} -> {js}", js=seq2str(js, enc="[]"))
        

        Like

  • Unknown's avatar

    Jim Randell 9:48 am on 23 January 2026 Permalink | Reply
    Tags:   

    Teaser 2462: [What NOW?] 

    From The Sunday Times, 29th November 2009 [link]

    Puzzlers are often confused about whether the number 1 is prime or square, so perhaps this Teaser will clarify the issue!

    • ONE is not prime, but it is an odd perfect square;
    TWO is divisible by 2 but not 4;
    • The number of odd primes less than SIX is TWO.

    In those statements digits have consistently been replaced by capital letters, with different letters being used for different digits.

    What is the number NOW?

    This puzzle was originally published with no title.

    [teaser2462]

     
    • Jim Randell's avatar

      Jim Randell 9:49 am on 23 January 2026 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 14.4ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # no leading zeros (ONE, TWO, SIX, NOW)
      --invalid="0,OTSN"
      
      # collect primes < 1000
      --code="primes.expand(999)"
      
      # ONE is not prime, but it is an odd perfect square
      "ONE not in primes"
      "E % 2 = 1"
      "is_square(ONE)"
      
      # TWO is divisible by 2, but not 4
      "O % 2 = 0"
      "WO % 4 != 0"
      
      # the number of odd primes less than SIX is TWO
      "icount(primes.between(3, SIX - 1)) = TWO"
      
      --template="(ONE) (TWO) (SIX)"
      --answer="NOW"
      

      Solution: NOW = 820.

      There are two possible assignments of digits to letters:

      ONE = 289, TWO = 102, SIX = 564
      ONE = 289, TWO = 102, SIX = 567

      The next prime after 563 is 569, so we count 102 odd primes if X is 4 or 7.

      Like

      • Frits's avatar

        Frits 9:09 am on 24 January 2026 Permalink | Reply

        @Jim, your code allows N to be zero. Is this what you wanted?

        Like

    • ruudvanderham's avatar

      ruudvanderham 11:24 am on 23 January 2026 Permalink | Reply

      import peek
      import istr
      
      for o, n, e, t, w, s, i, x in istr.permutations(range(10), 8):
          if o * t * s != 0:
              if not (one := istr("=one")).is_prime() and one.is_odd() and one.is_square():
                  if (two := istr("=two")).is_divisible_by(2) and not two.is_divisible_by(4):
                      if len(istr.primes(3, (six := istr("=six")))) == two:
                          peek(one, two, six)
      

      Like

  • Unknown's avatar

    Jim Randell 9:16 am on 20 January 2026 Permalink | Reply
    Tags:   

    Teaser 2363: [A round number] 

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

    We are used to thinking of 1,000 as a “round number”, but this is very much based on 10. One definition of a round number is that it should have lots of small factors, and certainly that it should be divisible by every number up to and including sixteen. There is one such round number that, when written in a certain base (of less than 10,000), looks like 1111. What is more, its base can be found very neatly.

    What is that base?

    This puzzle was originally published with no title.

    [teaser2363]

     
    • Jim Randell's avatar

      Jim Randell 9:17 am on 20 January 2026 Permalink | Reply

      Programatically, it is straightforward to just try all possible bases.

      This Python program runs in 75ms. (Internal runtime is 4.2ms).

      from enigma import (irange, mlcm, call, nconcat, printf)
      
      # M = LCM(2..16)
      M = call(mlcm, irange(2, 16))
      
      # consider possible bases
      for b in irange(2, 9999):
        # n = 1111 [base b]
        n = nconcat([1, 1, 1, 1], base=b)
        # check for divisibility by 2..16
        if n % M == 0:
          # output solution
          printf("base={b} n={n}")
      

      But here is a more sophisticated approach:

      Another way to write 1111 [base b] is: b^3 + b^2 + b + 1, which is an integer valued polynomial function.

      And this means we can use the [[ poly_roots_mod() ]] solver from the enigma.py library (described here: [ link ]) to solve the puzzle.

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

      from enigma import (Polynomial, rev, irange, mlcm, call, poly_roots_mod, printf)
      
      # calculate 1111 [base b]
      p = Polynomial(rev([1, 1, 1, 1]))
      
      # M = LCM(2..16)
      M = call(mlcm, irange(2, 16))
      
      # find roots of p(b) = 0 [mod M] that give a valid base
      roots = poly_roots_mod(p, 0)
      for b in roots(M):
        if 1 < b < 10000:
          # calculate the corresponding n value
          n = p(b)
          # output solution
          printf("base={b} n={n}")
      

      Solution: The base is: 5543.

      And the round number is: 170338568400 = 720720 × 236345.

      Like

    • Ruud's avatar

      Ruud 10:52 am on 20 January 2026 Permalink | Reply

      import istr
      
      print(*(base for base in istr.range(2, 10000) if all(sum(base**i for i in range(4)).is_divisible_by(i) for i in range(2, 17))))
      

      Like

    • Frits's avatar

      Frits 8:33 am on 21 January 2026 Permalink | Reply

      # 1111 [base b] is: b^3 + b^2 + b + 1 or (b + 1)(b^2 + 1)
      # I am not good at modular arithmetic but b^2 + 1 is never a multiple of
      # numbers 4.k + 3. So b + 1 must be divisible by 3, 7 and 11 and thus by 231.
      
      for b1 in range(231, 10001, 231):
        # calculate n = 1111 [base b]
        b = b1 - 1
        n = b1 * (b**2 + 1)
        # n must be divisible by numbers 2-16
        if any(n % i for i in [9, 11, 13, 14, 15, 16]): continue
        print("answer:", b)
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:11 am on 21 January 2026 Permalink | Reply

        @Frits: A neat bit of analysis. That is probably the kind of approach the setter had in mind.

        In fact as (b^2 + 1) is not divisible by 3, then it is also not divisible by 9, so we can proceed in steps of: 9 × 7 × 11 = 693, which only needs 14 iterations.

        This Python program has an internal runtime of 34µs:

        from enigma import (irange, printf)
        
        S = 9 * 7 * 11
        R = 720720 // S
        for i in irange(S, 10000, step=S):
          b = i - 1
          n = i * (b*b + 1)
          if n % R == 0:
            printf("base={b} n={n}")
        

        Like

        • Jim Randell's avatar

          Jim Randell 9:40 am on 21 January 2026 Permalink | Reply

          In fact although (b^2 + 1) may be divisible by 2, it cannot be divisible by 4, so we can move in steps of:

          S = 8 × 9 × 7 × 11 = 5544

          And, as the base is limited to be less than 10000, there is only one value to check (or if we accept that there is a solution to the puzzle, then there are no values to check).

          Like

          • Frits's avatar

            Frits 1:10 pm on 21 January 2026 Permalink | Reply

            @Jim, good work.

            if b is even then b=2k and b^2 + 1 = 4k^2 + 1
            if b is odd then b = 2k+1 and b^2 + 1 = 4k^2 + 4k + 2

            so indeed b^2 + 1 cannot be divisible by 4.

            Like

  • Unknown's avatar

    Jim Randell 6:38 am on 18 January 2026 Permalink | Reply
    Tags:   

    Teaser 3304: Roaming bull 

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

    Farmer Quentin keeps his prize bull in a circular field of diameter 50m. To improve the layout, he decides to switch to a square field with side length equal to the old diameter. He builds the new fencing around the old, with fence posts every metre starting at the corner. After completing the new fencing, he tethers the bull outside the fence to one of these fence posts with a rope of integer metre length, to allow the removal of the inner circular fence. As a result, the bull’s temporary roaming area is now four times what it was inside the circle.

    What is the length of the rope?

    [teaser3304]

     
    • Jim Randell's avatar

      Jim Randell 7:14 am on 18 January 2026 Permalink | Reply

      If the bull were tethered to a single post with a 50 m rope in an otherwise unobstructed flat plain, then it would be able to roam a region that is 4 times the area of the original (25 m radius) circular field. So the rope must be at least this long.

      When tethered to a post that makes up the boundary of the square field, then bull can roam over a collection of quadrants of a circle are centred on the tether point, and the corners of the square field, as the rope wraps around the boundary of the field.

      I considered rope lengths allowing roaming areas of between 2 and 6 non-overlapping quadrants (of various sizes) around the outside of the field. (Which assumes the bull has no other obstructions). If the rope is longer than 100 m then the regions will overlap, as the bull can reach the opposite point on the perimeter of the square from either direction.

      For each possible tethering point a corresponding equation is generated and then solved to find possible integer rope lengths.

      The following Python code runs in 79ms. (Internal runtime is 2.1ms).

      from enigma import (Rational, Polynomial as Poly, irange, sq, divc, sprintf as f, printf)
      
      Q = Rational()
      q = Q(1, 4)
      
      d = 50  # size of the square
      A = sq(d)  # target area (= 4 * r^2 where r = d/2)
      
      # find roots of polynomial <p> = <v> between <lo> and <hi>
      def roots(p, v, lo, hi, s=''):
        xs = p.roots(v, domain='Z', include='+', F=Q)  # integer roots
        for x in xs:
          if lo <= x <= hi:
            printf("{s}x={x}")
      
      # suppose the bull is tethered at post a = 0 (corner) to 25 (middle)
      # with a rope of length x
      for a in irange(0, divc(d, 2)):
      
        # 2 quadrants: 0 <= x <= a
        p = 2*q * sq(Poly([0, 1]))  # x^2
        roots(p, A, 0, a, f("[2 quads] a={a} -> "))
      
        # 3 quadrants: a <= x <= d - a
        p += q * sq(Poly([-a, 1]))  # (x - a)^2
        roots(p, A, a, d - a, f("[3 quads] a={a} -> "))
      
        # 4 quadrants: d - a <= x <= d + a
        p += q * sq(Poly([a - d, 1]))  # (x + (a - d))^2
        roots(p, A, d - a, d + a, f("[4 quads] a={a} -> "))
      
        # 5 quadrants: d + a <= x <= 2d - a
        p += q * sq(Poly([-(a + d), 1]))  # (x - (a + d))^2
        roots(p, A, d + a, 2*d - a, f("[5 quads] a={a} -> "))
      
        # 6 quadrants (no overlap): 2d - a <= x <= 2d
        p += q * sq(Poly([a - 2*d, 1]))  # (x + (a - 2d))^2
        roots(p, A, 2*d - a, 2*d, f("[6 quads] a={a} -> "))
      

      Solution: The length of the rope is 58 m.

      And the tether is at post 2 m from a corner.

      The bull can roam in two quadrants with radius 58 m, one quadrant with radius 56 m, one quadrant with radius 10 m, and one quadrant with radius 6 m.

      Here is a diagram of the situation – the light green area (outside the square field) is 4 times the size of the dark green area (inside the square field):


      If Farmer Quentin only measures the rope to approximately a whole number of metres, then there are two more solutions where the rope is within 4 cm of a whole number of metres:

      rope = 58.99 m; tether = 6 m from a corner
      rope = 60.03 m; tether = 12 m from a corner

      We can see these non-integer solutions by setting [[ domain='F', F='float' ]] at line 11 in the program above.

      Like

      • Jim Randell's avatar

        Jim Randell 11:00 am on 18 January 2026 Permalink | Reply

        Alternatively, just considering all possible rope lengths and tether points (although this doesn’t let you find non-integer rope lengths).

        This Python program runs in 392µs.

        from enigma import (irange, sq, divc, printf)
        
        d = 50  # size of the square
        A = 4 * sq(d)  # target area (in quarter units)
        
        # consider possible tether points
        for a in irange(0, divc(d, 2)):
        
          # consider increasing rope length
          for x in irange(d, 2*d):
        
            # calculate total area of possible quadrants
            rs = [x, x, x - a, x + a - d, x - a - d, x + a - 2*d]
            v = sum(sq(r) for r in rs if r > 0)
        
            # output solution
            if v == A: printf("a={a} x={x} [quads={q}]", q=sum(r > 0 for r in rs))
            if v >= A: break
        

        Like

    • Frits's avatar

      Frits 1:43 pm on 18 January 2026 Permalink | Reply

      3 areas (or 4 quadrants) will definitely be roamed as I don’t let the rope length start from 1.
      A 6th quadrant is not possible for diameter 50 as I have calculated a basic upper limit for the rope length.

      d = 50 # diameter in meters
      
      # old roaming area = 25^2.pi so new roaming area must be 50^2.pi
      # southern area S = r^2 / 2 < 2500, r^2 < 5000
      mx_rope = int((2 * d**2)**.5)
      
      # the rope length must be larger than the diameter
      for r in range(d + 1, mx_rope + 1):
        # tether the bull to the southern fence (on the west side)
        
        for y in range(26):   # distance from SW corner
          # quadruple areas divided by pi
          S4 = 2 * r**2        # 4 x area below the southern fence
          NW4 = (r - y)**2 
          N4 = (r - d - y)**2 if r - d - y > 0 else 0
          E4 = (r - d + y)**2 
          
          # all areas should sum to 100^2
          if S4 + NW4 + N4 + E4 == 4 * d**2:
            print("answer:", r, "meters")
      

      Like

  • Unknown's avatar

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

    Teaser 2312: Backward step 

    From The Sunday Times, 14th January 2007 [link]

    Seb had just been given his four-digit PIN, and he was afraid that he would not be able to remember it. So he broke all the rules and wrote it in his pocket diary. But to make it “more secure” he did not actually write the PIN; instead, he wrote the digits in reverse order. He was surprised to discover that the new number was a multiple of the correct PIN.

    What is his PIN?

    Note that without additional requirements there are many solutions to this puzzle. The intended solution seems to come from the requirement that all the digits of the PIN are non-zero.

    [teaser2312]

     
    • Jim Randell's avatar

      Jim Randell 8:12 am on 16 January 2026 Permalink | Reply

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

      It runs in 82ms. (Internal runtime of the generated code is 1.7ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # suppose the real PIN is ABCD
      "ediv(DCBA, ABCD) > 1"
      "D >= A" # [optional]
      
      --distinct=""
      --invalid=""
      --digits="1-9"
      --answer="ABCD"
      

      Solution: The PIN is: 2178.

      And so the reversed PIN is: 8712.

      And: 8712 = 4 × 2178.

      (The multiple solutions to the puzzle as published can be seen by removing the digit restriction on line 11).


      Without the restriction to non-zero digits there are 131 solutions, for example (reversed-PIN, PIN):

      1000 & 0001
      1010 & 0101

      9801 & 1089

      9990 & 0999

      (Although not all these would be surprising).

      But only one of these solutions remains if the digits of the PIN must all be non-zero.


      The published solution was: “1089 or 2178”.

      Like

    • Ruud's avatar

      Ruud 10:33 am on 16 January 2026 Permalink | Reply

      import istr
      print(*(pin for digits in istr.product(range(1, 10), repeat=4) if (pin:=istr.join(digits))[::-1].divided_by(pin) not in (1, None)))
      

      Like

    • GeoffR's avatar

      GeoffR 7:43 pm on 16 January 2026 Permalink | Reply

      from enigma import nsplit, is_pairwise_distinct
      
      for pin in range(1234, 9876):
          a, b, c, d = nsplit(pin)
          if 0 in (a, b, c, d): continue
          if is_pairwise_distinct(a, b, c, d):
              rev_pin = 1000*d + 100*c + 10*b + a
              q, r = divmod(rev_pin, pin)
              if r == 0 and q > 1:
                 print(f"Pin = {pin}.")
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 3:34 pm on 14 January 2026 Permalink | Reply
    Tags:   

    Brain teaser 983: Superchamp 

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

    The five champion athletes competed against one another in their five special events to decide who was the supreme champion (the one with the highest number of points). Three points were awarded for the winner in each event. two points for second place, and one for third.

    The Superchamp won more events than anyone else. All scored a different number of points, and all scored in a different number of events.

    Ben and Colin together scored twice as many points as Fred. Eddie was the only one to win his own special event, and in it the Superchamp came second. In another event Eddie gained one point less than the hurdler. The hurdler only came third in the hurdles, which was won by Denis, who was last in the long jump but scored in the sprint.
    Fred won the long jump and was second in the walk, but came nowhere in the hurdles.

    Ben scored in the long jump and Colin scored in the sprint. The thousand-metre man was third in two events. The long jumper scored more points than the sprinter.

    Who was the [long] jumper, and what was his score?

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

    [teaser983]

     
    • Jim Randell's avatar

      Jim Randell 3:35 pm on 14 January 2026 Permalink | Reply

      There are a lot of conditions to this puzzle, so I used the minizinc.py library to allow me to construct a declarative model for it in MiniZinc, which is then solved, and the Python program used to format the solutions.

      It runs in 545ms.

      from enigma import (require, printf)
      from minizinc import MiniZinc
      
      require("sys.version", "3.6") # needed to support [ use_embed=1 ]
      
      # indices for names and events (1..5)
      (B, C, D, E, F) = (1, 2, 3, 4, 5)
      (hurdles, long_jump, sprint, m1000, walk) = (1, 2, 3, 4, 5)
      
      # construct the MiniZinc model
      m = MiniZinc([
        'include "globals.mzn"',
      
        # scores for the competitors in each event: pts[event, competitor] = score
        "array [1..5, 1..5] of var 0..3: pts",
      
        # scores in each event are: 3, 2, 1, 0, 0
        "constraint forall (i in 1..5) (sum (j in 1..5) (pts[i, j] = 3) = 1)", # 1st = 3pts
        "constraint forall (i in 1..5) (sum (j in 1..5) (pts[i, j] = 2) = 1)", # 2nd = 2pts
        "constraint forall (i in 1..5) (sum (j in 1..5) (pts[i, j] = 1) = 1)", # 3rd = 1pts
        "constraint forall (i in 1..5) (sum (j in 1..5) (pts[i, j] = 0) = 2)", # unplaced = 0pts
      
        # functions:
        "function var int: points(var int: j) = sum (i in 1..5) (pts[i, j])",
        "function var int: wins(var int: j) = sum (i in 1..5) (pts[i, j] = 3)",
        "function var int: scored(var int: j) = sum (i in 1..5) (pts[i, j] > 0)",
      
        # "the superchamp scored more points than anyone else"
        "var 1..5: S",
        "constraint forall (j in 1..5 where j != S) (points(j) < points(S))",
      
        # "the superchamp won more events than anyone else"
        "constraint forall (j in 1..5 where j != S) (wins(j) < wins(S))",
      
        # each competitor scored a different number of points
        "constraint all_different (j in 1..5) (points(j))",
      
        # each competitor scored in a different number of events
        "constraint all_different (j in 1..5) (scored(j))",
      
        # "B and C together scored twice as many points as F"
        "constraint points({B}) + points({C}) = 2 * points({F})",
      
        # "E was the only one to win his own event"
        # "and in that event the superchamp came second"
        "array [1..5] of var 1..5: spe",
        "constraint all_different(spe)",
        "constraint forall (j in 1..5) ((j == {E}) <-> (pts[spe[j], j] = 3))",
        "constraint pts[spe[{E}], S] = 2",
      
        # "in another event E gained 1 point less than the hurdler"
        "var 1..5: hurdler",
        "constraint spe[hurdler] = {hurdles}",
        "constraint exists (i in 1..5) (pts[i, {E}] + 1 = pts[i, hurdler])",
      
        # "the hurdler only came third in hurdles"
        "constraint pts[{hurdles}, hurdler] = 1",
      
        # "hurdles was won by D"
        "constraint pts[{hurdles}, {D}] = 3",
      
        # "D came last in long jump"
        "constraint pts[{long_jump}, {D}] = 0",
      
        # "D scored in the sprint"
        "constraint pts[{sprint}, {D}] > 0",
      
        # "F won the long jump"
        "constraint pts[{long_jump}, {F}] = 3",
      
        # "F was 2nd in the walk"
        "constraint pts[{walk}, {F}] = 2",
      
        # "F did not score in hurdles"
        "constraint pts[{hurdles}, {F}] = 0",
      
        # "B scored in the long jump"
        "constraint pts[{long_jump}, {B}] > 0",
      
        # "C scored in the sprint"
        "constraint pts[{sprint}, {C}] > 0",
      
        # "the 1000m man was 3rd in 2 events"
        "var 1..5: m1000er",
        "constraint spe[m1000er] = {m1000}",
        "constraint sum (i in 1..5) (pts[i, m1000er] = 1) = 2",
      
        # "the long jumper scored more points than the sprinter"
        "var 1..5: long_jumper",
        "constraint spe[long_jumper] = {long_jump}",
        "var 1..5: sprinter",
        "constraint spe[sprinter] = {sprint}",
        "constraint points(long_jumper) > points(sprinter)",
      
      ], use_embed=1)
      
      
      # map indices back to names / events (for output)
      name = {1: "Ben", 2: "Colin", 3: "Denis", 4: "Eddie", 5: "Fred"}
      event = {1: "hurdles", 2: "long jump", 3: "sprint", 4: "1000m", 5: "walk"}
      
      # solve the model and find:
      # pts = results in each event
      # spe = special event for each competitor
      for (pts, spe) in m.solve(solver="minizinc -a", result="pts spe"):
        # collect points
        t = dict((k, 0) for k in name.keys())
      
        # output the results for each event
        for (i, rs) in enumerate(pts, start=1):
          (p1, p2, p3) = (rs.index(p) + 1 for p in (3, 2, 1))
          printf("{e}: 1st = {p1}, 2nd = {p2}, 3rd = {p3}", e=event[i], p1=name[p1], p2=name[p2], p3=name[p3])
          # collect points
          t[p1] += 3
          t[p2] += 2
          t[p3] += 1
        printf()
      
        # determine superchamp
        S = max(t.keys(), key=t.get)
        assert all(k == S or v < t[S] for (k, v) in t.items())
      
        # output the results for each competitor
        for j in sorted(event.keys()):
          printf("{j} [{e}]: {t} points{S}", j=name[j], e=event[spe[j - 1]], t=t[j], S=(' [SUPERCHAMP]' if j == S else ''))
        printf("--")
        printf()
      

      Solution: Denis is the long jumper. He scored 5 points.

      The program finds 2 possible scenarios:

      hurdles: 1st = Denis, 2nd = Eddie, 3rd = Ben
      long jump: 1st = Fred, 2nd = Ben, 3rd = Eddie
      sprint: 1st = Ben, 2nd = Denis, 3rd = Colin
      1000m: 1st = Eddie, 2nd = Ben, 3rd = Fred
      walk: 1st = Ben, 2nd = Fred, 3rd = Eddie

      Ben [hurdles]: 11 points [SUPERCHAMP]
      Colin [sprint]: 1 points
      Denis [long jump]: 5 points
      Eddie [1000m]: 7 points
      Fred [walk]: 6 points

      hurdles: 1st = Denis, 2nd = Eddie, 3rd = Colin
      long jump: 1st = Fred, 2nd = Ben, 3rd = Colin
      sprint: 1st = Colin, 2nd = Denis, 3rd = Eddie
      1000m: 1st = Eddie, 2nd = Colin, 3rd = Fred
      walk: 1st = Colin, 2nd = Fred, 3rd = Eddie

      Ben [sprint]: 2 points
      Colin [hurdles]: 10 points [SUPERCHAMP]
      Denis [long jump]: 5 points
      Eddie [1000m]: 7 points
      Fred [walk]: 6 points

      The solution published in the book (which runs to 3 pages) only gives the first of these solutions.

      I think this is due to the interpretation of “In another event Eddie gained one point less than the hurdler”. The book solution seems to infer (hurdler = 3pts, Eddie = 2pts) or (hurdler = 2pts, Eddie = 1pt), and concludes it is not possible for C to be the champ.

      And in the first scenario, in the long jump, we have: the hurdler (Ben) scores 2pts (2nd place), and Eddie scores 1pt (3rd place).

      However, I think the wording also allows (hurdler = 1pt, Eddie = 0pts), and in the second scenario, in the long jump, we have: the hurdler (Colin) scores 1pt (3rd place), and Eddie scores 0pts (unplaced).

      Like

    • Frits's avatar

      Frits 3:06 pm on 17 January 2026 Permalink | Reply

      from itertools import permutations
      
      '''        3   2   1       
      hurdles    A   F   K        
      long J     B   G   L        
      sprint     C   H   M              super=Superchamp
      1000m      D   I   N        
      Walk       E   J   O        
      '''
      
      sports = [-1, -1, -1, -1, -1]
      (ben, colin, denis, eddie, fred) = range(5)
      
      dgts, sols = set(range(5)), set()
      
      A = denis # Denis won hurdles
      B = fred  # Fred won the long jump
      J = fred  # Fred was 2nd in the walk
      
      # Fred came nowhere in the hurdles
      for F, K in permutations(dgts - {A, fred}, 2):    # hurdles
        # hurdler (K) came 3rd in the hurdles, Eddie is no hurdler
        if eddie == K: continue
        for G, L in permutations(dgts - {B, denis}, 2): # Denis last in long jump
          if 0 not in {G, L} : continue                 # Ben scored in long jump
          for C, H, M in permutations(dgts, 3):         # sprint
            # Colin and Denis scored in the sprint
            if not {C, H, M}.issuperset({colin, denis}): continue 
            for D, I, N in permutations(dgts, 3):       # 1000m
              for E, O in permutations(dgts - {J}, 2):  # walk
                gold   = [A, B, C, D, E]
                silver = [F, G, H, I, J]
                bronze = [K, L, M, N, O]
                # Eddie was the only one to win his own special event
                if eddie not in gold: continue
                scores = [A, F, K, B, G, L, C, H, M, D, I, N, E, J, O]
                scored = [scores.count(i) for i in range(5)]
                # all scored in a different number of events.
                if len(set(scored)) != 5: continue
                
                pts = [sum([3 - (i % 3) for i, s in enumerate(scores) if s == j]) 
                       for j in range(5)]
                # all scored a different number of points
                if len(set(pts)) != 5: continue
                # Ben and Colin together scored twice as many points as Fred
                if pts[ben] + pts[colin] != 2 * pts[fred]: continue
                # the Superchamp won more events than anyone else ( >= 2)
                sups = sorted((f, i) for i in range(5) if (f := gold.count(i)) > 1)
                if not sups or (len(sups) == 2 and len(set(gold)) == 3): continue
                
                super = sups[-1][1]
                # once the Superchamp came 2nd after Eddie
                if super == eddie or super not in silver: continue
                # the Superchamp had the highest number of points
                if pts[super] != max(pts): continue
                
                sports[0] = K  # hurdler only came third in the hurdles
                      
                # check sports where Eddie and Superchamp ended 1 and 2
                for e, (g, s) in enumerate(zip(gold, silver)):
                  if (g, s) != (eddie, super): continue
                  
                  if e == 0: continue # Eddie is not the hurdler
                  # in another event Eddie gained one point less than the hurdler
                  for i, gsb in enumerate(zip(gold, silver, bronze)):
                    if ((K, eddie) in zip(gsb, gsb[1:]) or 
                        (eddie not in gsb and gsb[-1] == K)): break
                  else: # no break
                    break    
                  
                  sports_ = sports.copy()
                  sports_[e] = eddie  # Eddie performs sport <e>
                  # assign people to remaining sports
                  for p in permutations(dgts - {eddie, K}):
                    sports__ = sports_.copy()
                    j = 0
                    for i in range(5):
                      if sports_[i] != -1: continue
                      sports__[i] = p[j]
                      j += 1
                    
                    # Eddie was the only one to win his own special event
                    for i, s in enumerate(sports__):
                      if gold[i] == s and s != eddie: break
                    else: # no break  
                      # the long jumper scored more points than the sprinter
                      if not (pts[sports__[1]] > pts[sports__[2]]): continue
                      # the thousand-metre man was third in two events
                      if bronze.count(sports__[3]) != 2: continue
                      # store solution
                      sols.add((sports__[1], pts[sports__[1]]))
      
      names = "Ben Colin Denis Eddie Fred".split()                
      print("answer:", ' or '.join(names[a] + ' with score ' + str(s) 
                                    for a, s in sols))         
      

      Like

  • Unknown's avatar

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

    Teaser 3303: Knights and knaves 

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

    On my travels, I came across a group of some knights (always truthful) and some knaves (always lying). At first, I couldn’t tell which were knights and which were knaves. However, six individuals then gave the following statements about their group:

    “The number of knights is prime.”
    “The number of knights is odd.”
    “The number of knaves is square.”
    “The number of knaves is even.”
    “The total of knights and knaves is an odd number.”
    “There are more knaves than knights.”

    These statements enabled me to deduce the composition of the group.

    How many (a) knights and (b) knaves were in the group?

    [teaser3303]

     
    • Jim Randell's avatar

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

      The setter has an additional piece of information – i.e. they can observe the total number of people in the group. If it is possible to break this total down in into knights and knaves in only one way that is consistent with the statements made, then the composition of the group can be determined with certainty.

      The following Python program runs in 71ms. (Internal runtime is 158µs).

      from enigma import (irange, inf, decompose, filter2, is_prime, is_square_p, printf)
      
      # consider increasing group sizes (at least 6)
      for t in irange(6, inf):
        # collect candidate solutions for this total size
        sols = list()
        # decompose into (a) knights and (b) knaves
        for (a, b) in decompose(t, 2, min_v=1, increasing=0, sep=0):
      
          # evaluate the statements
          ss = filter2(bool, [
            # 1. "a is prime"
            is_prime(a),
            # 2. "a is odd"
            (a % 2 == 1),
            # 3. "b is square"
            is_square_p(b),
            # 4. "b is even"
            (b % 2 == 0),
            # 5. "a + b is odd"
            (t % 2 == 1),
            # 6. "b > a"
            (b > a),
          ])
      
          # check minimum group sizes
          if len(ss.true) > a or len(ss.false) > b: continue
          # record this candidate
          sols.append((a, b))
      
        printf("[{t} -> {sols}]")
        # check for unique solutions
        if len(sols) == 1:
          (a, b) = sols[0]
          printf("a = {a}; b = {b}")
          break
      

      Solution: (a) There are 5 knights; (b) There are are 2 knaves.

      The total size of the group is 7, and the statements made are:

      1. True (“the number of knights is prime” = 7)
      2. True (“the number of knights is odd” = 5)
      3. False (“the number of knaves is square” = 2)
      4. True (“the number of knaves is even” = 2)
      5. True (“the total number of knights and knaves is odd” = 7)
      6. False (“there are more knaves than knights” 5 > 2)

      There are 4 true statements, and 2 false statements, so both knaves made a false statement, and 4 of the 5 knights made a true statement.

      And this is the only way a group of 7 can be broken down consistent with the statements.

      If we allow the program to continue to larger group sizes, we see that the number of candidate breakdowns grows, so it would not be possible to determine the composition of a larger group.

      Like

  • Unknown's avatar

    Jim Randell 9:58 pm on 9 January 2026 Permalink | Reply
    Tags:   

    Teaser 2360: [Flower arranging] 

    From The Sunday Times, 16th December 2007 [link]

    In the following statements and instruction, digits have been consistently replaced by letters, with different letters for different digits.

    F + L + O + W + E + R raised to the power P is equal to the six-digit number FLOWER.

    FACTOR is divisible by A, R and T.

    What is the value of FLOWER ART?

    This puzzle was originally published with no title.

    [teaser2360]

     
    • Jim Randell's avatar

      Jim Randell 10:03 pm on 9 January 2026 Permalink | Reply

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

      It runs in 226ms. (Internal runtime of the generated code is 126ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "pow(F + L + O + W + E + R, P) = FLOWER"
      
      "FACTOR % A = 0"
      "FACTOR % R = 0"
      "FACTOR % T = 0"
      
      --answer="(FLOWER, ART)"
      

      Solution: FLOWER ART = 390625 751.

      And:

      FLOWER = 390625 = 25^4 = (3 + 9 + 0 + 6 + 2 + 5) ^ 4.
      FACTOR = 378105 is divisible by 7, 5, 1.

      Like

    • Ruud's avatar

      Ruud 6:11 am on 10 January 2026 Permalink | Reply

      import istr
      
      for f, l, o, w, e, r, p in istr.permutations(range(10), 7):
          if sum(istr("=flower")) ** p == istr("=flower"):
              for a, c, t in istr.permutations(range(10), 3):
                  if istr("=flowerpact").all_distinct() and all(istr("=factor").is_divisible_by(x) for x in (a, r, t)):
                      print(istr("=flower"), istr("=art"))
      

      Like

      • ruudvanderham's avatar

        ruudvanderham 11:32 am on 10 January 2026 Permalink | Reply

        This version is *much* faster. Note that it required the latest version of istr (1.1.16):

        import istr
        
        for p in range(2, 10):
            for flower in istr.power_ofs(p, 100000, 1000000):
                if sum(flower) ** p == flower:
                    istr.decompose(flower, "flower")
                    for a, c, t in istr.permutations(set(istr.range(10))-set(flower)-{p}, 3):
                        if istr("=flowerpact").all_distinct() and all(istr("=factor").is_divisible_by(x) for x in (a, r, t)):
                            print(istr("=flower"), istr("=art"))
        

        Like

    • GeoffR's avatar

      GeoffR 2:48 pm on 10 January 2026 Permalink | Reply

      Note: P = 5 is max value for int_pow() can accomodate without an error, but OK for a solution.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:F; var 1..9:A; var 1..9:R; var 2..5:P;  
      var 0..9:L; var 0..9:O; var 0..9:W; var 0..9:E;
      var 1..9:T; var 0..9:C;
      
      constraint all_different ([F, A, R, P, L, O, W, E, T, C]);
      
      var 100000..999999:FLOWER == 100000*F + 10000*L + 1000*O
      + 100*W + 10*E + R;
      
      var 100000..999999:FACTOR = 100000*F + 10000*A + 1000*C
      + 100*T + 10*O + R;
      
      var 100..999:ART = 100*A + 10*R + T;
      
      var 2..54:SUM = F + L + O + W + E + R;
      
      constraint int_pow(SUM, P, FLOWER);
      
      constraint FACTOR mod A == 0 /\ FACTOR mod R == 0 /\ FACTOR mod T == 0;
      
      solve satisfy;
      output["FLOWER = " ++ show(FLOWER) ++ ", ART = " ++ show(ART)];
      
      % FLOWER = 390625, ART = 751
      % ----------
      % ==========
      
      
      

      Like

    • Brian Gladman's avatar

      Brian Gladman 7:40 pm on 10 January 2026 Permalink | Reply

      from itertools import permutations
      
      # convert digits to numbers and vice versa
      dgts_to_nbr = lambda dgts: int(''.join(map(str, dgts)))
      nbr_to_dgts = lambda nbr: tuple(int(d) for d in str(nbr))
      
      # the digit sum of FLOWER is between the minimum 
      # and maximum sums of six digits from 0..9
      for sm in range(15, 40):
        # the possible powers to give sm ** P six digits
        for P in (4, 5):
          if 100_000 <= (FLOWER := sm ** P) < 1_000_000:
            t = (F, L, O, W, E, R) = nbr_to_dgts(FLOWER)
            # check that three digits remain unallocated
            if len(r3 := set(range(10)).difference(t + (P,))) == 3:
              # consider the order of the remaining digits for A, C and T
              for A, C, T in permutations(r3):
                FACTOR = dgts_to_nbr((F, A, C, T, O, R))
                # FACTOR must be divisible by A, R and T
                if FACTOR % (A * R * T) == 0:
                  ART = dgts_to_nbr((A, R, T))
                  print(f"FLOWER ART = {FLOWER} {ART} [{FACTOR = }, {P = }]")
      

      Like

  • 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: The 5-digit number was 83160.

      There are 6 candidate scenarios (number = sum × multiple):

      2376 = 594 × 4 → 23760 = 792 × 30
      3564 = 594 × 6 → 35640 = 792 × 45
      4752 = 594 × 8 → 47520 = 792 × 60
      7128 = 594 × 12 → 71280 = 792 × 90
      8316 = 594 × 14 → 83160 = 792 × 105

      In each case the sum of the digits in the 4-digit and 5-digit numbers is 18, and so the sum of the 2-digit combinations from the 4-digit number is 33 × 18 = 594, and the sum of the 2-digit combinations from the 5-digit number is 44 × 18 = 792.

      Of the 5-digit numbers only the last one is a 3-digit multiple of the sum, and this provides the answer to the puzzle.

      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

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