Updates from June, 2021 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:31 pm on 13 June 2021 Permalink | Reply
    Tags: ,   

    Brain-Teaser 35: After-lunch golf 

    From The Sunday Times, 19th November 1961 [link]

    Gee and Jay play — for £1 the match, 5s. a hole, and 1s. a stroke — a golf match of nine holes which is decided on the ninth green. Neither takes less than four or more than nine strokes at any hole and the
    results vary for every hole.

    Gee wins five holes and the match, and takes 22s. off his opponent, but had the result at the last hole been reversed he would have lost 10s.

    At the eighth hole all the strokes travel an exact number of yards straight towards the pin, but whilst each of Gee’s travels one half the distance of his previous stroke, each of Jay’s goes one quarter the distance of its predecessor.

    (i) In how many strokes did Gee complete the round?
    (ii) What is the length of the eighth hole?

    [teaser35]

     
    • Jim Randell's avatar

      Jim Randell 4:31 pm on 13 June 2021 Permalink | Reply

      I struggled to find a unique solution for this puzzle, and had to make several assumptions along the way. Maybe I just don’t know enough about golf.

      Let’s start by looking at the 8th hole:

      If G makes n strokes, and the distance of the final one is x, then the total distance is:

      x + 2x + 4x + … + (2^n)x = (2^n − 1)x

      And if J makes m strokes, and the distance of the final one is y, then the total distance is:

      y + 4y + 16y + … + (4^m)y = (4^m − 1)y / 3

      So the overall distance of the hole must be some multiple of: lcm(2^n − 1, (4^m − 1) / 3)

      The following values work (for holes with a distance of less than 1000 yards):

      n=4 m=4: dist = 255k; Gs=(136, 68, 34, 17)k, Js=(192, 48, 12, 3)k
      n=5 m=5; dist = 341k; Gs=(176, 88, 44, 22, 11)k, Js=(256, 64, 16, 4, 1)k
      n=8 m=4; dist = 255k; Gs=(128, 64, 32, 16, 8, 4, 2, 1)k, Js=(192, 48, 12, 3)k

      If we limit the length of a hole we can reduce the possibilities, but even the smallest possible distance (255 yards) has 2 options for the score.

      As far as the actual match goes I’m assuming the winner of the match is the player who won the greatest number of individual holes, and the loser pays the winner £1 (= 20s), and for each individual hole the loser of each hole pays the winner 5s, plus 1s per stroke for the difference in strokes.

      I further assumed that “the results vary for each hole” mean that no score for any hole is repeated.

      We know that G wins 5 holes (including the final hole), and hole 8 is either drawn, or G loses it by 4 strokes.

      If the 8th hole is drawn there are 2 possibilities (scores from G’s perspective):

      8th hole = 4-4; Gs wins = 4-5, 5-6, 6-7, 7-8, 8-9; other holes = 8-4, 9-5, 9-4
      8th hole = 5-5; Gs wins = 4-5, 5-6, 6-7, 7-8, 8-9; other holes = 8-4, 9-5, 9-4

      The first gives G a total of 60 strokes, and J a total of 52.

      G wins 20 + (5 − 3)×5 + (52 − 60) = 22 shillings.

      And if the result of one of G’s wins (on the last hole) was reversed, there would be no winner on holes (each won 4, and 1 was drawn), so the match is drawn, and only money for the number of strokes changes hands. G has a total of 61 strokes, and J a total of 51 strokes, so G loses 10s. (Although I think J could argue that in this case he won the match).

      The second gives G a total of 61 strokes, and J a total of 53.

      Again, if the result of one of G’s wins is reversed, G’s total goes down by 1 stroke and J’s goes up by 1, so G loses 10s.

      So, if we limit the maximum possible distance of a hole to 340 yards, then only the first of these options remains, and gives the solution:

      Solution: (i) Gee completed the round in 60 strokes. (ii) The 8th hole was 255 yards long.

      Which is the published solution.

      Like

      • Jim Randell's avatar

        Jim Randell 10:35 am on 15 June 2021 Permalink | Reply

        Here is a Python program that solves the puzzles according to the assumptions described in my previous comment.

        It runs in 86ms.

        Run: [ @replit ]

        from enigma import irange, subsets, update, lcm, group, unpack, multiset, join, printf
        
        # possible strokes per hole
        strokes = irange(4, 9)
        
        # consider the number of strokes on the 8th hole (G=x, J=y)
        def hole8():
          for (x, y) in subsets(strokes, size=2, select="M"):
            # calculate minimal distance
            tx = sum(pow(2, n) for n in irange(0, x - 1))
            ty = sum(pow(4, n) for n in irange(0, y - 1))
            d = lcm(tx, ty)
            if d < 1000:
              printf("[hole8: G={x} J={y}; dist = {d}k; Gs={Gs}k Js={Js}k]",
                Gs=tuple(pow(2, n) * d // tx for n in irange(0, x - 1))[::-1],
                Js=tuple(pow(4, n) * d // ty for n in irange(0, y - 1))[::-1],
              )
              yield (x, y)
        # collect hole 8 scores by score
        score = unpack(lambda x, y: y - x)
        h8s = group(hole8(), by=score)
        
        # collect possible wins/losses
        scores = group(subsets(strokes, size=2, select="M"), by=score)
        
        # calculate the gain for a sequence of holes
        def gain(hs):
          # total gain (shillings), difference (in holes)
          T = d = 0
          for x in hs:
            if x < 0:
              # G wins the hole
              T += 5 - x
              d += 1
            elif x > 0:
              # G loses the hole
              T -= 5 + x
              d -= 1
          if d > 0:
            # G wins the match (on holes, not strokes)
            T += 20
          elif d < 0:
            # G loses the match
            T -= 20
          return T
        
        # find scores with stroke differences in ds
        def complete(ds, ss):
          if not ds:
            if len(set(ss)) == len(ss):
              yield ss
          else:
            (k, v) = ds[0]
            for xs in subsets(scores[k], size=v, fn=list):
              yield from complete(ds[1:], ss + xs)
        
        # output holes and total strokes
        def output(hs, ss):
          holes = list()
          G = J = 0
          for (h, (x, y)) in zip(hs, ss):
            if h > 0: (x, y) = (y, x)
            holes.append((x, y))
            G += x
            J += y
          printf("{holes} -> G={G} J={J}", holes=join((f"{x}-{y}" for (x, y) in holes), sep=", ", enc="()"))
        
        # G wins 5 holes (including the final one)
        for Gw in subsets([-1, -2, -3, -4, -5], size=5, select="R"):
          # G does not win any of the other 4 holes
          for Gl in subsets([0, 1, 2, 3, 4, 5], size=4, select="R"):
            # calculate G's winnings
            hs = list(Gw + Gl)
            # hole 8 is 0 or -4
            if not any(k in hs for k in h8s.keys()): continue
            w = gain(hs)
            if w != 22: continue
        
            # look for a 9th hole, which if swapped would give -10s
            for x in set(Gw):
              hs_ = update(hs, [hs.index(x)], [-x])
              w_ = gain(hs_)
              if w_ != -10: continue
        
              # count the scores, and reject any collection that would require a duplicate score
              s = multiset.from_seq((abs(x) for x in hs))
              if any(s.count(k) > len(scores[k]) for k in s.keys()): continue
        
              # choose a hole 8
              for (i, h8) in enumerate(hs):
                for s8 in h8s.get(h8, []):
                  # count the remaining scores
                  for ss in complete(list(s.difference([abs(h8)]).items()), [s8]):
                    # output solution (hole 8 first)
                    output([h8] + hs[:i] + hs[i + 1:], ss)
        

        The distance for the 8th hole is limited to below 1000 yards, so the program produces two possible answers.

        In the output the holes are not listed in play order, rather hole 8 comes first, then the remaining holes, starting with those that G won.

        Like

      • Jim Randell's avatar

        Jim Randell 5:49 pm on 19 June 2021 Permalink | Reply

        With Teaser 36 the following was published:

        In Brain-Teaser No. 35 (Sunday, November 19) owing to the omission of the result of the eighth hole (halved in four) several alternative solutions became possible. No reader who submitted any of these was excluded from the prize.

        Which I think means we are take it that the result of the eighth hole was a draw, each player taking 4 strokes.

        With this additional information we can reduce the number of possible solutions (although we still need to limit the distance of the eighth hole, or the individual strokes, to deduce a unique distance).

        Like

    • Jim Olson's avatar

      Jim Olson 9:14 pm on 13 June 2021 Permalink | Reply

      I’m not following the total score for each. If Gee wins the match then he has the lowest total strokes. In the analysis presented Jay should have won the match. It is not determined by the number of holes won. I understand the assumption you made but that is not golf rules.

      Like

      • Jim Randell's avatar

        Jim Randell 11:02 pm on 13 June 2021 Permalink | Reply

        I tried various things to try and get a unique solution, and this was the only way I found. Looking on Wikipedia [link] it seemed to be allowed (“match play” vs “stroke play” – I did start off using the latter, but didn’t get anywhere).

        I don’t think a modern Teaser puzzle would be published that didn’t have at least a brief description of the scoring system that is to be used. But this one is from 60 years ago.

        Like

    • Jim Olson's avatar

      Jim Olson 2:18 am on 14 June 2021 Permalink | Reply

      I agree your interpretation is what the setter had in mind. However, after many years of playing golf In tournaments and at golf clubs the tournament or match was won by the golfer with the lowest number of total strokes. The setter should have made it clear how the scoring for the match winner was to be computed. It would have saved quite a bit of time.

      Like

  • Unknown's avatar

    Jim Randell 4:36 pm on 11 June 2021 Permalink | Reply
    Tags:   

    Teaser 3064: Turnip prize 

    From The Sunday Times, 13th June 2021 [link] [link]

    The Turnip Prize is awarded to the best piece of work by an artist under fifty. This year’s winning entry consisted of a mobile made up of many different plain white rectangular or square tiles hanging from the ceiling. The sides of the tiles were all whole numbers of centimetres up to and including the artist’s age, and there was precisely one tile of each such possible size (where, for example, a 3-by-2 rectangle would be the same as a 2-by-3 rectangle). Last week one of the tiles fell and smashed and then yesterday another tile fell and smashed. However, the average area of the hanging tiles remained the same throughout.

    How old is the artist?

    There are now 500 Teaser puzzles available on the site (approximately 10 years worth).

    And I still have 52 puzzles from 2016-2017 that I solved at the time to post, which at the current rate will take me another year.

    [teaser3064]

     
    • Jim Randell's avatar

      Jim Randell 4:50 pm on 11 June 2021 Permalink | Reply

      The following Python program runs in 52ms.

      Run: [ @replit ]

      from enigma import irange, tri, div, printf
      
      # collect rectangles with sides up to x, and total area
      (rs, t) = (list(), 0)
      for x in irange(1, 49):
        rs.extend((a, x) for a in irange(1, x))
        t += x * tri(x)
      
        # calculate the number of tiles and the total area
        n = len(rs)
        # look for an integer mean
        m = div(t, n)
        if m is None: continue
      
        # look for tiles with the mean area
        ts = list((a, b) for (a, b) in rs if a * b == m)
        if len(ts) > 1:
          printf("x={x} [m={m}; ts={ts}]")
      

      or (shorter, but doesn’t track tile dimensions):

      from enigma import multiset, irange, div, printf
      
      # collect rectangles with sides up to x
      rs = multiset()
      for x in irange(1, 49):
        rs.update(a * x for a in irange(1, x))
        # calculate integer mean tile area
        m = div(rs.sum(), rs.size())
        # look for tiles with the mean area
        if rs.count(m) > 1:
          printf("x={x} [m={m}]")
      

      Solution: The artist is 37.

      So, there were originally 703 tiles with a total area of 255892 cm², given a mean area of 364 cm².

      There are 2 tiles with this area (= 14×26, 13×28), and the removal of either (or both) will leave the mean area unchanged.

      Like

      • Jim Randell's avatar

        Jim Randell 8:47 am on 12 June 2021 Permalink | Reply

        And here’s a version that doesn’t collect the rectangles at all (and does report tile dimensions):

        The number of tiles is given by the triangular numbers:

        T(n) = n (n + 1) / 2

        And the total area is given by the 4-dimensional pyramidal numbers (see: OEIS A001296):

        A(n) = n (n + 1) (n + 2) (3n + 1) / 24

        And hence the mean area of a tile is:

        M(n) = A(n) / T(n) = (n + 2) (3n + 1) / 12

        So we can look for an integer mean, that has (at least) 2 divisor pairs in the required range:

        from enigma import irange, div, divisors_pairs, printf
        
        # consider ages
        for x in irange(1, 49):
          # calculate integer mean
          m = div((x + 2) * (3 * x + 1), 12)
          if m is None: continue
          # find tiles with mean area
          ts = list((a, b) for (a, b) in divisors_pairs(m) if b <= x)
          if len(ts) > 1:
            printf("x={x} [m={m}; ts={ts}]")
        

        Like

        • Frits's avatar

          Frits 10:05 am on 12 June 2021 Permalink | Reply

          Nice.

          Proof:

          see [ https://www.wolframalpha.com/input/?i=%28sum+x*x*%28x%2B1%29%2F2%2C+x%3D1+to+n%29+ ]

          total area = sum(y=1..x) 1/2 y y (y + 1) = 1/24 x (x + 1) (x + 2) (3x + 1)
          number of tiles = x (x+1) / 2
          integer mean = (x + 2) * (3 * x + 1) / 12

          Like

        • Tony Brooke-Taylor's avatar

          Tony Brooke-Taylor 8:07 am on 14 June 2021 Permalink | Reply

          To get an integer mean, x must satisfy the requirement that (x+2).(3x+1) mod 12 = 0. This requires that x be equal to either 1+12i or 10+12i (i=0,1,…).

          This plot gives a visual representation of that requirement.

          
          import numpy as np
          import matplotlib.pyplot as plt
          
          x = np.asarray([a for a in range(1,51)])
          y = np.asarray([(a+2)*(3*a+1)%12 for a in range(1,51)])
          
          plt.scatter(x, y)
          plt.title('y=(x + 2).(3x + 1) mod 12')
          plt.xlabel('x')
          plt.ylabel('y')
          plt.show()
          
          

          Like

  • Unknown's avatar

    Jim Randell 8:00 am on 10 June 2021 Permalink | Reply
    Tags:   

    Teaser 2794: D-day 

    From The Sunday Times, 10th April 2016 [link] [link]

    I have a particular digit in mind and I shall call it D. I have written down a number consisting of D digits in which the penultimate digit is D itself. If I move that penultimate digit to the left so that it becomes the first digit, then I get a new D-digit number that turns out to be D times the number I started with.

    What is the D-digit number I started with?

    [teaser2794]

     
    • Jim Randell's avatar

      Jim Randell 8:00 am on 10 June 2021 Permalink | Reply

      Using : for concatenation, we have:

      D:abc:z = D × abc:D:z
      ⇒ 10^(D − 1) D + 10 abc + z = D (100 abc + 10 D + z)
      ⇒ abc = (10 D (10^(D − 2) − D) − (D − 1) z) / (100 D − 10)

      where abc represents a (D − 2) digit number).

      This Python program runs in 50ms.

      Run: [ @replit ]

      from enigma import irange, div, ndigits, printf
      
      # choose D
      for D in irange(3, 9):
        x = 10 * D * (10 ** (D - 2) - D)
        y = 100 * D - 10
        # possible final digits
        for z in irange(0, 9):
          # note: D * z = z [mod 10]
          r = (D - 1) * z
          if not(r % 10 == 0): continue
          # calculate abc...
          abc = div(x - r, y)
          if abc is None or ndigits(abc) != D - 2: continue
          # output solution
          printf("number={abc}{D}{z} [D={D}]")
      

      Solution: The number you started with is 101123595.

      So, D = 9, and: 9 × 101123595 = 910112355.

      Like

      • Frits's avatar

        Frits 12:15 pm on 10 June 2021 Permalink | Reply

        @Jim, you also could have used (regarding the r % 10 == 0) :

        for z in [0, 2, 4, 5, 6, 8]:
        

        Like

      • Jim Randell's avatar

        Jim Randell 2:57 pm on 10 June 2021 Permalink | Reply

        If we construct numbers by taking successive digits of the larger number (D:abc:z), and divide these by D, then we get the corresponding digits of the number we started with, which can then be used in the next division to determine the following digit:

        D / D = a (so: a = 1)
        Da / D = ab (so: b = 0)
        Dab / D = abc

        So, for a given value of D, we can compute successive digits of abc. And after the (D − 2) digits are calculated, z is chosen to complete the division, and then we check that it is a viable digit:

        from enigma import irange, div, printf
        
        # choose D
        for D in irange(3, 9):
          n = D
          x = 0
          # perform (D - 2) divisions
          for _ in irange(1, D - 2):
            d = (n // D) % 10
            n = 10 * n + d
            x = 10 * x + d
        
          # the final digit is z
          n = 10 * n
          x = 100 * x + 10 * D
          z = div(n - x * D, D - 1)
          if z is None or z < 0 or z > 10: continue
        
          # output solution
          n += z
          x += z
          printf("[D={D}] {x} * {D} = {n}")
        

        This appears to be slightly faster than my original code (internal runtime is reduced from 60μs to 42μs).

        Like

    • Frits's avatar

      Frits 11:57 am on 10 June 2021 Permalink | Reply

      We can also continue the analysis, so C must be 1, etc …

      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [# consider number ABCDEFGHI
         
         # (9 * H + carry digit) modulo 10 must be equal to G
         "(81 + 0.8 * I) % 10 = G",
      
         # we have established H must be 9   
         "HABCDEFGI == 9 * ABCDEFGHI",
         ],
      
        answer="ABCDEFGHI",
        verbose=0,
        # (9 * I) % 10 = I so I is 0 or 5
        # Consider 9 * FGHI = HFGI (F > 0), this can only happen when H is 9
        # so H is 9, A must be 1 and B must be 0
        # G is 1 or 5 (see G formula)
        d2i=dict([(0, "AH")] + [(1, "BHI")] + [(2, "ABHI")] + [(5, "ABH")] + 
                 [(k, "ABHI") for k in [3, 4, 6, 7, 8, 9]] +
                 [(9, "ABI")]),
        distinct="",   # allow variables with same values
        reorder=0,
      )
      
      # Print answer
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

    • GeoffR's avatar

      GeoffR 1:53 pm on 10 June 2021 Permalink | Reply

      A simple solution in MiniZinc, using Frits analysis i.e. D-digit number = 9

      
      % A Solution in MiniZinc
      
      var 0..9: A; var 0..9: B; var 0..9: C; var 0..9: D; var 0..9: E;
      var 0..9: F; var 0..9: G; var 0..9: H; var 0..9: I;
      
      constraint H > 0 /\ A > 0;
      
      var 100000000..999999999: ABCDEFGHI = I + 10*H + 100*G + 1000*F + 10000*E
      + 100000*D + 1000000*C + 10000000*B + 100000000*A;
      
      var 100000000..999999999: HABCDEFGI = I + 10*G + 100*F + 1000*E + 10000*D
      + 100000*C + 1000000*B + 10000000*A + 100000000*H;
      
      constraint HABCDEFGI == 9 * ABCDEFGHI;
      
      solve satisfy;
      
      output["The number you started with was " ++ show(ABCDEFGHI) ];
      
      % The number you started with was 101123595
      % time elapsed: 0.20 s
      % ----------
      % ==========
      
      

      Like

      • Frits's avatar

        Frits 7:20 pm on 10 June 2021 Permalink | Reply

        Unfortunately my analysis was incorrect (I mixed things up).
        Following program works well with Geocode but gives no answer with the Chuffed solver.

        Using “var 0..9: A;” gives an out of range error.

        % A Solution in MiniZinc
         
        var 0..1: A; var 0..9: B; var 0..9: C; var 0..9: D; var 0..9: E;
        var 0..9: F; var 0..9: G; var 0..9: H; var 0..9: I;
        
         % moving the penultimate digit to the left so that it becomes
         % the first digit
        constraint H > 2;
         
        var 100000000..999999999: ABCDEFGHI = I + 10*H + 100*G + 1000*F
        + 10000*E + 100000*D + 1000000*C + 10000000*B + 100000000*A;
         
        var 10000000..99999999: ABCDEFGI = I + 10*G + 100*F + 1000*E
        + 10000*D + 100000*C + 1000000*B + 10000000*A;
         
        constraint H * pow(10, H - 1) + ABCDEFGI == H * ABCDEFGHI;
         
        solve satisfy;
         
        output["The number you started with was " ++ show(ABCDEFGHI)];
        

        Like

  • Unknown's avatar

    Jim Randell 10:30 am on 8 June 2021 Permalink | Reply
    Tags: by: R A England   

    Brain-Teaser 774: Postage stamps 

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

    I went to the Post Office to buy three 5½p stamps (those were the days!) for my entries for the Brain-teaser, Crossword and Mephisto; but finding a long queue there I bought a 10p book of stamps from a machine, which contained two stamps at each of the following denominations: 2p, 1½p, 1p, ½p. Since I already had four stamps of total value 6½p left over from a similar 10p book I now had just enough stamps for the three entries.

    I stuck four of the stamps totalling 5½p on my Brain-teaser entry, but then found that there was room for only three stamps on the Crossword entry (because entrants have to write “Crossword” on the top left-hand corner of the  envelope) and the stamps I had left could not be arranged to give me three stamps totalling 5½p for the Crossword entry and five for the Mephisto entry.

    What were the denominations of stamps on the Brain-teaser entry?

    Current stamp prices in the UK are 85p (1st class) or 66p (2nd class).

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 1 (1980). The puzzle text above is taken from the book.

    [teaser774]

     
    • Jim Randell's avatar

      Jim Randell 10:31 am on 8 June 2021 Permalink | Reply

      The following Python program runs in 51ms.

      Run: [ @replit ]

      # working in half-pence units the denominations avaliable are:
      #  1 (= .5p), 2 (= 1p), 3 (= 1.5p), 4 (= 2p)
      
      from enigma import (multiset, subsets, join, sprintf, printf)
      
      # one book of stamps (2 of each denomination)
      book = multiset.from_seq((1, 2, 3, 4), count=2)
      
      # make an amount <t> using <k> stamps from <ss>
      make = lambda ss, t, k: (s for s in subsets(ss, size=k, select="mC") if sum(s) == t)
      
      # there are 4 stamps making 13 (= 6.5p) left over from a book
      for rs in make(book, 13, 4):
        # combine these with a new book
        ss = book.combine(rs)
      
        # choose 4 stamps making 11 (= 5.5p) for the Teaser entry
        for ts in make(ss, 11, 4):
          # but you can't make 11 (= 5.5p) using 3 stamps from what is left over
          if any(make(ss.difference(ts), 11, 3)): continue
      
          # output solution
          fmt = lambda xs: join((sprintf("{x:g}p", x=0.5 * x) for x in xs), sep=", ")
          printf("Teaser = {ts}", ts=fmt(ts))
      

      Solution: The stamps used for the Brain-teaser entry were: 1× 1p stamp; 3× 1½p stamps.

      The 8 remaining stamps are: 2× ½p; 2× 1p; 4× 2p.

      So the correct postage can be made using 4 stamps: 2p + 2p + 1p + ½p.

      Like

    • Frits's avatar

      Frits 12:51 pm on 11 June 2021 Permalink | Reply

      Using the same approach as Jim.

      2 technical issues:

      the decompose function generates duplicates
      is there an easier way to determine “remaining” (like a list comprehension)?

      # working in half-pence units 
      
      # decompose <t> into <k> increasing numbers from <ns>, count number <= c
      # so that sum(<k> numbers) equals <t>
      def decompose(t, k, ns, s=[], m=1, c=99):
        if k == 1:
          if not(t < m) and t in ns:
            res = s + [t]
            for x in set(res):
              if res.count(x) > c:
                return
            yield tuple(res)
        else:
          for (i, n) in enumerate(ns):
            if not(n < t): break
            if not(n < m):
              yield from decompose(t - n, k - 1, ns[i:], s + [n], n, c)
      
      # there are 4 stamps making 13 (= 6.5p) left over from a book
      leftovers = set(decompose(13, 4, [1, 2, 3, 4] * 2, c=2))
      
      for lo in leftovers:
       # leftover + 10p book
       ss = lo + (1, 2, 3, 4) * 2
       # choose 4 stamps making 11 (= 5.5p) for the teaser entry
       for t in set(decompose(11, 4, ss)):
         remaining = list(ss)
         for x in t:
           if x in remaining: remaining.remove(x)
         
         # but you can't make 11 (= 5.5p) using 3 stamps from what is left over
         if any(decompose(11, 3, remaining)): continue
         print(f"teaser =",
               f"{', '.join(str(x / 2).rstrip('0').rstrip('.') + 'p' for x in t)}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:28 am on 6 June 2021 Permalink | Reply
    Tags:   

    Teaser 2000: 2000 down 

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

    Many years ago. I took a thin strip of wood and cut it into four pieces, each a different whole number of centimetres long. I could use those pieces to form the sides of many different quadrilaterals, and of all the choices I made one of the largest area possible. I then mounted the quadrilateral on my study wall. When the magazine started numbering its Brainteasers (now known as Teasers), I did them each week, and on finishing the puzzle I shaded a new patch of area [exactly] one square centimetre within the quadrilateral.

    After today’s auspicious Teaser, I will have shaded the entire quadrilateral [exactly]. So next week I shall make another quadrilateral to cover precisely the next 2000 Teasers. Again its sides will be four different whole numbers of centimetres and its area will be the largest possible with those particular lengths. I shall need a strip of wood at least as long as the one I used last time.

    What are the lengths of the sides of my first quadrilateral?

    [teaser2000]

     
    • Jim Randell's avatar

      Jim Randell 9:29 am on 6 June 2021 Permalink | Reply

      For a quadrilateral with given sides, the maximal area occurs when the quadrilateral is cyclic, and is given by the formula:

      A = (1/4)√((−a + b + c + d)(a − b + c + d)(a + b − c + d)(a + b + c − d))

      (Which is a generalisation of Heron’s Formula, known as Brahmagupta’s formula).

      We are interested in cases where: A = 2000.

      Which means were are interested in when:

      (−a + b + c + d)(a − b + c + d)(a + b − c + d)(a + b + c − d) = 64000000

      And as each of a, b, c, d are integers, so are the terms on the LHS, so we can consider factorisations of the RHS into 4 integer factors, and then solve the resulting 4 equations to give us candidate side lengths.

      The [[ divisor_tuples() ]] function is borrowed from Enigma 1062.

      This Python program runs in 162ms.

      Run: [ @replit ]

      from enigma import divisors_pairs, matrix, as_int, seq_all_different, printf
      
      # find ordered k-tuples that multiply to give n
      def divisor_tuples(n, k, s=[]):
        if k == 1:
          if not(s and n < s[-1]):
            yield s + [n]
        else:
          for (a, b) in divisors_pairs(n):
            if not(s and a < s[-1]):
              yield from divisor_tuples(b, k - 1, s + [a])
      
      # find maximal quadrilateral with area A
      def solve(A):
        # coefficients of a, b, c, d in the factors
        eqs = [
          (-1, +1, +1, +1),
          (+1, -1, +1, +1),
          (+1, +1, -1, +1),
          (+1, +1, +1, -1),
        ]
        # factorise (4A)^2
        for fs in divisor_tuples(16 * A * A, 4):
          # and solve the equations (for positive integers)
          try:
            ns = list(as_int(x, '+') for x in matrix.linear(eqs, fs))
          except ValueError:
            continue
          else:
            yield sorted(ns)
      
      # collect solutions consisting of different integers
      ss = sorted((ns for ns in solve(2000) if seq_all_different(ns)), key=sum)
      
      # output solutions
      for ns in ss:
        printf("{ns} -> {t}", t=sum(ns))
        break  # we only need the smallest solution
      

      Solution: The first quadrilateral has sides of length: 5 cm, 55 cm, 65 cm, 85 cm.

      The minimal perimeter is 210 cm, and the sides are all multiples of 5cm. One possible quadrilateral with these side lengths looks like this:

      This is the collection of side lengths with the smallest perimeter. But the program finds 8 possible collections (ordered by perimeter):

      210: (5, 55, 65, 85)
      240: (20, 40, 70, 110)
      288: (16, 19, 119, 134)
      294: (22, 47, 83, 142)
      308: (26, 29, 104, 149)
      330: (5, 40, 125, 160)
      486: (43, 83, 118, 242)
      504: (2, 124, 127, 251)

      If the setter choose the next smallest perimeter for Teasers 2001 – 4000, the quadrilateral will have sides: 20 cm, 40 cm, 70 cm, 110 cm (perimeter = 240 cm), which are all multiples of 10 cm.

      Like

    • Hugh Casement's avatar

      Hugh Casement 10:01 am on 6 June 2021 Permalink | Reply

      I reckon to have found a near-solution with a shorter perimeter.
      If a = 39, b = 42, c = 47, d = 52, then the perimeter is 180 and the area is about 2000.008 units.
      It takes only a very slight deviation from cyclic to reduce that to 2000.

      Like

      • Jim Randell's avatar

        Jim Randell 10:32 am on 6 June 2021 Permalink | Reply

        For all practical purposes this is a viable answer (and a nicer looking quadrilateral).

        If I’ve calculated correctly you would have to be able to measure and cut the wooden strip to an accuracy of about 2e-14 cm to disallow this. Which I think is about 1/1000000 th the width of an atom.

        Like

    • GeoffR's avatar

      GeoffR 5:38 pm on 6 June 2021 Permalink | Reply

      I tried a brute force solution in MiniZinc, experimenting with the upper bound value of the four sides.

      I found setting an upper bound of 150 produced four out of five of Jim’s smallest perimeter solutions, but missed the perimeter of 294. It did, however, find the same two lowest perimeters of 210 and 240.

      I found the Chuffed Solver was necessary to run it satisfactorily, and it took several seconds to run.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..150:a; var 1..150:b; var 1..150:c; var 1..150:d; 
      constraint all_different([a, b, c, d]);
      
      % Using Jim's version of the Brahmagupta formula for maximising the area
      constraint (-a + b + c + d) * (a - b + c + d) * (a + b - c + d) 
      * (a + b + c - d) == 64000000;
      
      % Minimising the perimeter finds different solutions
      solve minimize(a + b + c + d);
      
      output [" Four sides = " ++ show([a, b, c, d]) ++ 
      ", Perimeter = " ++ show(a + b + c + d)  ];
      
      %  Four sides = [104, 29, 26, 149], Perimeter = 308
      %  ----------
      %  Four sides = [16, 119, 19, 134], Perimeter = 288
      % ----------
      %  Four sides = [40, 110, 20, 70], Perimeter = 240
      % ----------
      %  Four sides = [55, 5, 85, 65], Perimeter = 210  <<< first quadrilateral
      % ----------
      % ==========
      
      
      

      Like

      • Frits's avatar

        Frits 3:57 pm on 7 June 2021 Permalink | Reply

        Adding the following is a bit faster

        set of int: forbidden = {3, 7, 9};
        
        var 1..999: p1 = -a + b + c + d;
        var 1..999: p2 = a - b + c + d;
        var 1..999: p3 = a + b - c + d;
        var 1..999: p4 = a + b + c - d;
        
        % numbers may not end on 3, 7 or 9
        constraint forall( [p1 mod 10 != i | i in forbidden] );
        constraint forall( [p2 mod 10 != i | i in forbidden] );
        constraint forall( [p3 mod 10 != i | i in forbidden] );
        constraint forall( [p4 mod 10 != i | i in forbidden] );
        

        Also adding the following slows it down again but it does report all 8 solutions (with 1..300 for a,b,c,d) with the Chuffed Solver.

        set of int: forbidden2 = {3, 7};
        
        % numbers may not be multiples of 3 or 7
        constraint forall( [p1 mod i != 0 | i in forbidden2] );
        constraint forall( [p2 mod i != 0 | i in forbidden2] );
        constraint forall( [p3 mod i != 0 | i in forbidden2] );
        constraint forall( [p4 mod i != 0 | i in forbidden2] );
        

        Like

      • Frits's avatar

        Frits 7:38 pm on 9 June 2021 Permalink | Reply

         
        constraint a < b /\ b < c /\ c < d; 
        
        is a lot faster than 
        
        constraint all_different([a, b, c, d]);
        

        Like

    • GeoffR's avatar

      GeoffR 7:15 am on 8 June 2021 Permalink | Reply

      @Frits: Yes, interesting code enhancements.
      If the UB of a,b,c,d is set to 100, it produces the single smallest answer.

      Like

    • Frits's avatar

      Frits 5:14 pm on 9 June 2021 Permalink | Reply

      Runs a lot faster with PyPy than with Python 3.9.

      T = 64000000
      
      # loop over perimeter (sum of the side lengths)
      # as from (4 * sqrt(2000))
      for P in range(179, 999999):
        for a in range(1, P // 4 + 1):
          for b in range(a + 1, (P - a) // 3 + 1):
            for c in range(b + 1, (P - a - b) // 2 + 1):
              d = P - (a + b + c)
              p1 = -a + b + c + d
              p2 = a - b + c + d
              p3 = a + b - c + d
              p4 = a + b + c - d
            
              if p1 * p2 * p3 * p4 != T: continue
      
              print(f"side lengths {a}, {b}, {c}, {d} with perimeter {P}")  
              exit()
      

      (Modified as per comment below).

      Like

      • Jim Randell's avatar

        Jim Randell 5:22 pm on 9 June 2021 Permalink | Reply

        @Frits: You can start looking for perimeters at [[ intc(4 * sqrt(2000)) ]] = 179, as a square has the minimal perimeter for a given area.

        Like

    • GeoffR's avatar

      GeoffR 8:54 pm on 9 June 2021 Permalink | Reply

      A straight translation of my MiniZinc programme for the single solution.

      
      from enigma import Timer
      timer = Timer('ST2000', timer='E')
      timer.start()
      
      for a in range(1,100):
        for b in range(a+1,100):
          for c in range(b+1,100):
            for d in range(c+1, 100):
              if (-a + b + c + d) * (a - b + c + d) \
                 * (a + b - c + d) * (a + b + c - d) == 64000000:
                print(f"4 Sides/Perimeter: {a}, {b}, {c}, {d}, {a+b+c+d}")
                timer.stop()      
                timer.report()
                exit()  
      
      # 4 Sides/Perimeter: 5, 55, 65, 85, 210
      # ST2000] total time: 0.4669819s (466.98ms)
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:56 pm on 4 June 2021 Permalink | Reply
    Tags:   

    Teaser 3063: Pie by five 

    From The Sunday Times, 6th June 2021 [link] [link]

    We had a delicious pie, rectangular and measuring 20 centimetres along the top and 13 centimetres in the other direction. We divided it into five pieces of equal area, using five straight cuts radiating from one internal point. This internal point was rather off centre, in the top left-hand quarter, although the distances from the left and top sides were both a whole number of centimetres. The points where the cuts met the edges were also whole numbers of centimetres along the edges; one edge had two cuts meeting it, and the other three edges had one each.

    How far was the internal point from the left and top sides, and how far along the four sides (starting at the top) did the cuts reach the edges (measured clockwise along the edges)?

    [teaser3063]

     
    • Jim Randell's avatar

      Jim Randell 9:59 pm on 4 June 2021 Permalink | Reply

      Each portion has an area equal to one fifth of the whole pie.

      The portion between the two cuts on the same side is composed of a single triangle (with, as it turns out, its base along the bottom edge). The remaining portions are composed of two adjacent triangles, with their bases on adjacent edges.

      Using the top left corner of the pie as the origin, we can consider integer (x, y) co-ordinates for the starting point of the cuts (in the top left quadrant of the pie). We can then consider 2 integer marks on the bottom that give a triangular slice with one fifth the area of the pie, and the marks on the other edges can then be determined.

      This Python program runs in 54ms.

      Run: [ @replit ]

      from itertools import product
      from enigma import (Rational, irange, divf, printf)
      
      Q = Rational()
      
      # is rational q an integer?
      as_int = lambda q: (q.numerator if q.denominator == 1 else None)
      
      # dimensions of the rectangle
      (X, Y) = (20, 13)
      
      # area of each portion
      A = Q(X * Y, 5)
      printf("[X={X} Y={Y}; A={A}]")
      
      # calculate area of a triangle with base b and height h
      area = lambda b, h: Q(b * h, 2)
      
      # calculate base of triangle with area A and height h
      base = lambda A, h: Q(2 * A, h)
      
      # find solutions with cuts starting at (x, y)
      def solve(x, y):
        # look for the portion composed of 1 triangle:
        # bottom marks, (a, Y), (b, Y); distance z (= b - a) apart
        z = as_int(base(A, Y - y))
        if z is None: return
        for a in irange(1, X - z - 1):
          b = a + z
          # remaining bottom triangles left and right
          (B1, C1) = (area(a, Y - y), area(X - b, Y - y))
          # left mark, (0, c)
          c = as_int(Y - base(A - B1, x))
          if c is None or not (0 < c < Y): continue
          # right mark (X, d)
          d = as_int(Y - base(A - C1, X - x))
          if d is None or not (0 < d < Y): continue
          # top mark (e, 0)
          e = as_int(base(A - area(c, x), y))
          if e is None or not (0 < e < X): continue
          printf("x={x} y={y} -> bottom: {a}, {b}; left: {c}; right: {d}; top: {e}")
      
      # choose a position for the start of the cuts
      for (x, y) in product(irange(1, divf(X - 1, 2)), irange(1, divf(Y - 1, 2))):
        solve(x, y)
      

      Solution: The cuts started from a point 8 cm in from the left edge and 5 cm in from the top edge. The end of cuts were: top = 16 cm from the left; right = 7 cm from the top; bottom = 4 cm and 17 cm from the right; left = 10 cm from the bottom.

      A similar approach can be used to look for situations with 2 cuts on other edges, but these yield no viable solutions.

      The idea of splitting each portion into triangles can be used to show that for any regular polygon, portions (cut from the centre) with equal amounts of the perimeter have equal size.

      Like

      • Frits's avatar

        Frits 10:18 am on 5 June 2021 Permalink | Reply

        @Jim, very clever. I am looking forward to next week (or earlier) when you explain the method you have used.

        Like

    • Frits's avatar

      Frits 9:48 am on 5 June 2021 Permalink | Reply

      Having done most of the work myself.

      # x = distance internal point from left edge
      # y = distance internal point from top edge
      # a = distance top left-hand corner to meeting point on left edge
      # b = distance top left-hand corner to meeting point on top edge
      # c = distance top right-hand corner to meeting point on right edge
      # d = distance bottom left-hand corner to 2nd meeting point on bottom edge
      # e = distance bottom left-hand corner to 1st meeting point on bottom edge
      
      # if we make a triangle with the bottom edge (points e and d) 
      # then the following formula can be derived: (d - e)(13 - y) = 104 = 8 * 13
      #
      # only one value for y is possible.
      
      # with this derived value of y we calculate the areas of the 5 slices
      # and end up with 5 equations and 6 variables
      #
      #  (13 - a)x = 8(13 - e)       (A)
      #  5b + ax = 104               (B)
      #  (20 - x)c = 4 + 5b          (C)
      #  13x + 8d + 20c - cx = 316   (D)
      #  d = 13 + e                  (E)
      
      for a in range(1, 7):                  # (not disclosing y)
        for x in range(1, 10):
          # consider equation B
          ax = a * x
          if ax % 10 not in {4, 9}: continue
          b = (104 - ax) // 5 
          # meeting point with edge is not in the corner
          if b > 19: continue 
         
          # consider equation A
          y = 13 * x - ax
          if y % 8: continue
          
          e = 13 - y // 8
          d = 13 + e
          
          # consider equation C
          (c, r) = divmod(4 + 5 * b, 20 - x)
          if r: continue
          
          # check equation D
          if 13 * x + 8 * d + 20 * c - c * x != 316: continue
          
          # use bogus formula for derived value y (not disclosing y)
          print(f"{x=} y={x-a}, top: {b} right: {c} bottom: {e=} {d=} left: {a}")  
      

      Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 7:53 am on 7 June 2021 Permalink | Reply

        My approach was similar to yours Frits, but I could not satisfy myself by general reasoning that the two cuts had to be on the bottom edge as opposed to the right-hand edge. I therefore tested for the triangle to be on the right-hand edge but this did not produce a valid solution. Therefore the triangle must be on the bottom edge and there is only one solution.

        I got formulae similar to yours by considering parallelograms with missing triangles. Jim’s solution is much more elegant because it divides all the areas into triangles.

        Like

        • Jim Randell's avatar

          Jim Randell 9:31 am on 7 June 2021 Permalink | Reply

          @Tony: I did start by considering the two cuts on different edges, but it turned out they had to be on the bottom.

          As my program didn’t have a neat way to try the alternates, I just removed the dead paths from my program.

          I’ve put a more complete program up as teaser3063x.py on replit.

          from itertools import product
          from enigma import (Rational, irange, divf, printf)
          
          Q = Rational()
          
          # is rational q an integer?
          as_int = lambda q: (q.numerator if q.denominator == 1 else None)
          
          # dimensions of the rectangle
          (X, Y) = (20, 13)
          
          # area of each portion
          A = Q(X * Y, 5)
          printf("[X={X} Y={Y}; A={A}]")
          
          # calculate area of a triangle with base b and height h
          area = lambda b, h: Q(b * h, 2)
          
          # calculate base of triangle with area A and height h
          base = lambda A, h: Q(2 * A, h)
          
          # find solutions with cuts starting at (x, y)
          def solve(x, y):
            # look for the portion composed of 1 triangle:
          
            # top marks, distance z apart
            z = as_int(base(A, y))
            if z is not None:
              for a in irange(1, X - z - 1):
                b = a + z
                printf("x={x} y={y} -> top: {a}, {b}; ...")
          
            # bottom marks, distance z apart
            z = as_int(base(A, Y - y))
            if z is not None:
              for a in irange(1, X - z - 1):
                b = a + z
                # remaining bottom sections of slices left and right
                (B1, C1) = (area(a, Y - y), area(X - b, Y - y))
                # left mark
                c = as_int(Y - base(A - B1, x))
                if c is None or not (0 < c < Y): continue
                # right mark
                d = as_int(Y - base(A - C1, X - x))
                if d is None or not (0 < d < Y): continue
                # top mark
                e = as_int(base(A - area(c, x), y))
                if e is None or not (0 < e < X): continue
                printf("x={x} y={y} -> bottom: {a}, {b}; left: {c}; right: {d}; top: {e}")
          
            # left marks, distance z apart
            z = as_int(base(A, x))
            if z is not None:
              for a in irange(1, Y - z - 1):
                b = a + z
                printf("x={x} y={y} -> left: {a}, {b}; ...")
          
            # right marks, distance z apart
            z = as_int(base(A, X - x))
            if z is not None:
              for a in irange(1, Y - z - 1):
                b = a + z
                # remaining sections of right slices above and below
                (B1, C1) = (area(a, X - x), area(Y - b, X - x))
                # top mark
                c = as_int(X - base(A - B1, Y - y))
                if c is None or not (0 < c < X): continue
                # bottom mark
                d = as_int(X - base(A - C1, Y - y))
                if d is None or not (0 < d < X): continue
                printf("x={x} y={y} -> right: {a}, {b}; top={c}; bottom={d}; ...")
          
          # choose a position for the start of the cuts
          for (x, y) in product(irange(1, divf(X - 1, 2)), irange(1, divf(Y - 1, 2))):
            solve(x, y)
          

          Like

  • Unknown's avatar

    Jim Randell 8:38 am on 3 June 2021 Permalink | Reply
    Tags:   

    Teaser 2793: Sum triangles 

    From The Sunday Times, 3rd April 2016 [link] [link]

    In this picture:

    there are ten upwardly-pointing triangles of all sizes.

    In a similar picture with many more rows, the total number of upward-pointing triangles is divisible by all the digits from 1 to 9.

    In this larger picture the number of upward-pointing triangles in the bottom row is equal to Trix’s age.

    How old is Trix?

    [teaser2793]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 3 June 2021 Permalink | Reply

      (See also: Enigma 1296).

      The number of upward pointing triangles on a triangular grid of size n is the sum of the triangular numbers from 1 to n. These are the tetrahedral (or triangular pyramidal) numbers (see: OEIS A000292).

      The following Python program runs in 50ms.

      Run: [ @replit ]

      from enigma import (irange, inf, tri, mlcm, call, printf)
      
      # generate increasing (grid size, upward triangles) pairs
      def generate(m=inf):
        t = 0
        for n in irange(1, m):
          t += tri(n)
          yield (n, t)
      
      # find a solution divisible by d
      d = call(mlcm, irange(1, 9))
      for (n, t) in generate():
        if t % d == 0:
          printf("n={n} t={t}")
          break  # we only need the first solution
      

      Solution: Trix is 54.


      Using the formula:

      ups(n) = n(n + 1)(n + 2) / 6

      We are looking for solutions where ups(n) is a multiple of 2520:

      n(n + 1)(n + 2) / 6 = 2520k
      n(n + 1)(n + 2) = 15120k

      Which gives the following program:

      from enigma import (irange, inf, tuples, multiply, printf)
      
      for ns in tuples(irange(1, inf), 3):
        t = multiply(ns)
        if t % 15120 == 0:
          printf("n={n} t={t}", n=ns[0])
          break
      

      Like

  • Unknown's avatar

    Jim Randell 8:48 am on 1 June 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 771: Doctor Bungle bungles 

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

    The doctors in the country town of Keepwell are keen soccer fans and with the assistance of their staff they have formed themselves into 3 teams who are all to play each other once.

    I asked my friend, Dr Bungle, who is secretary of one of the teams, if he could let me know the current situation and he produced the following table:

    I knew that not more than 3 goals had been scored in any match and it did not take me long to see that there was something wrong with the table. I discovered subsequently that the doctor had been taking a non-truth drug of his own prescription. The drug was not 100% effective and so all but one of the figures were incorrect, and the remaining figure was correct.

    What were the scores in the matches played so far?

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 1 (1980). The puzzle text above is taken from the book.

    [teaser771]

     
    • Jim Randell's avatar

      Jim Randell 8:49 am on 1 June 2021 Permalink | Reply

      This Python program uses the [[ Football() ]] helper class from the enigma.py library.

      It runs in 49ms.

      Run: [ @replit ]

      from itertools import product
      from enigma import Football, printf
      
      # possible scores (not more than 3 goals scored)
      score = dict()
      score['w'] = { (1, 0), (2, 0), (3, 0), (2, 1) }
      score['d'] = { (0, 0), (1, 1) }
      score['l'] = set((y, x) for (x, y) in score['w'])
      score['x'] = [None]
      
      def check(xs, ys):
        return sum(x == y for (x, y) in zip(xs, ys))
      
      # scoring system (matches may not be played)
      football = Football(games='wdlx')
      
      # there are 3 matches
      for (AB, AC, BC) in football.games(repeat=3):
        # table rows for A, B, C
        A = football.table([AB, AC], [0, 0])
        B = football.table([AB, BC], [1, 0])
        C = football.table([AC, BC], [1, 1])
      
        # check for correct digits (the can be at most one)
        d1 = check(
          [A.played, A.w, A.l, A.d, B.played, B.w, B.l, B.d, C.played, C.w, C.l, C.d],
          [1, 0, 1, 0, 2, 2, 0, 0, 1, 0, 2, 1],
        )
        if d1 > 1: continue
      
        # consider possible scores
        for (sAB, sAC, sBC) in product(score[AB], score[AC], score[BC]):
          # goals for/against
          (fA, aA) = football.goals([sAB, sAC], [0, 0])
          (fB, aB) = football.goals([sAB, sBC], [1, 0])
          (fC, aC) = football.goals([sAC, sBC], [1, 1])
      
          # check for correct digits
          d2 = check([fA, aA, fB, aB, fC, aC], [2, 0, 1, 2, 0, 3])
          if d1 + d2 != 1: continue
      
          printf("AB={AB}:{sAB} AC={AC}:{sAC} BC={BC}:{sBC}; d1={d1} d2={d2}")
          printf("A={A}; f={fA} a={aA}")
          printf("B={B}; f={fB} a={aB}")
          printf("C={C}; f={fC} a={aC}")
          printf()
      

      Solution: The scores were: A vs B = 1-1; A vs C = 3-0; B vs C = 1-2.

      The only correct figure is B’s played value (= 2).

      Like

  • Unknown's avatar

    Jim Randell 2:36 pm on 30 May 2021 Permalink | Reply
    Tags:   

    Brain teaser 1000: One thousand down 

    From The Sunday Times, 20th September 1981 [link]

    This auspiciously-numbered Brain-teaser has given my wife a chance to take stock of the teasers which she has tried over the years. When the Brain-teasers started to be numbered she did one of the early ones and took most of that Sunday over it. So she decided not to spend every Sunday on them, but only to do those teasers whose numbers were multiples of that first one she had tried.

    This worked very well for a long time until one Sunday there was an exceptional teaser which she couldn’t resist doing, even though she had done the previous week’s. She so enjoyed doing that extra puzzle that she decided she would in future try precisely those teasers whose numbers were the sum of two numbers (possibly the same) of teasers which she had tried previously. She didn’t try last week’s, but she’ll be busy today trying this one. But the rest of us have suddenly realised with horror that we’re never going to get a decent Sunday lunch again, because my wife is going to have to do every Brain-teaser from now on.

    (1) What was the first teaser which she tried?
    (2) What was the number of that “exceptional teaser which she couldn’t resist”?
    (3) How many of the first 1,000 Brain-teasers will she have tried?

    [teaser1000]

     
    • Jim Randell's avatar

      Jim Randell 2:37 pm on 30 May 2021 Permalink | Reply

      (See also: Enigma 1154, Enigma 1194, Enigma 228, Enigma 122).

      If the first Teaser is number x, then she will try 2x, 3x, 4x, …. Until one day, having done kx she can’t resist also doing (kx + 1).

      So we can consider this to be a “money changing” problem, using denominations of x and y = (kx + 1).

      And the largest amount that cannot be expressed using these denominations is 999.

      This is the Frobenius Number of the denominations:

      F(x, y) = x⋅y − x − y

      And the number of amounts that are not expressible using the denominations is:

      N(x, y) = (x − 1)(y − 1) / 2

      The following Python program runs in 53ms.

      Run: [ @replit ]

      from enigma import irange, div, printf
      
      F = lambda x, y: x * y - x - y
      N = lambda x, y: div((x - 1) * (y - 1), 2)
      
      # consider the first teaser x (< 100)
      for x in irange(2, 99):
        # and consider multiples of x < 1000
        for kx in irange(2 * x, 997, step=x):
          y = kx + 1
          if F(x, y) == 999:
            # output solutions
            n = 1000 - N(x, y)
            printf("(1) {x}; (2) {y}; (3) {n}") 
      

      Solution: (1) The first Teaser she tried was number 5; (2) The Teaser she couldn’t resist was number 251; (3) She will have tried 500 of the first 1000 Teasers.

      Like

      • Jim Randell's avatar

        Jim Randell 4:59 pm on 30 May 2021 Permalink | Reply

        And the following program shows the numbers of the Teasers that were attempted:

        from enigma import irange, printf
        
        (x, y) = (5, 251)
        
        # collect attempted teasers
        ns = set()
        
        # attempt teaser n
        def attempt(n):
          ns.add(n)
          printf("{n}" + ("" if len(ns) % 15 == 0 else " \\"))
        
        # consider increasing teaser numbers
        for n in irange(1, 1000):
          # start by doing multiples of x
          if n < y:
            if n % x == 0:
              attempt(n)
          elif n == y:
            # and then do y
            attempt(n)
          else:
            # and then any number that is the sum of two previous numbers
            if any(n - a in ns for a in ns):
              attempt(n)
        
        printf("\nattempted = {n}", n=len(ns))
        

        Output:

        5 10 15 20 25 30 35 40 45 50 55 60 65 70 75
        80 85 90 95 100 105 110 115 120 125 130 135 140 145 150
        155 160 165 170 175 180 185 190 195 200 205 210 215 220 225
        230 235 240 245 250 251 255 256 260 261 265 266 270 271 275
        276 280 281 285 286 290 291 295 296 300 301 305 306 310 311
        315 316 320 321 325 326 330 331 335 336 340 341 345 346 350
        351 355 356 360 361 365 366 370 371 375 376 380 381 385 386
        390 391 395 396 400 401 405 406 410 411 415 416 420 421 425
        426 430 431 435 436 440 441 445 446 450 451 455 456 460 461
        465 466 470 471 475 476 480 481 485 486 490 491 495 496 500
        501 502 505 506 507 510 511 512 515 516 517 520 521 522 525
        526 527 530 531 532 535 536 537 540 541 542 545 546 547 550
        551 552 555 556 557 560 561 562 565 566 567 570 571 572 575
        576 577 580 581 582 585 586 587 590 591 592 595 596 597 600
        601 602 605 606 607 610 611 612 615 616 617 620 621 622 625
        626 627 630 631 632 635 636 637 640 641 642 645 646 647 650
        651 652 655 656 657 660 661 662 665 666 667 670 671 672 675
        676 677 680 681 682 685 686 687 690 691 692 695 696 697 700
        701 702 705 706 707 710 711 712 715 716 717 720 721 722 725
        726 727 730 731 732 735 736 737 740 741 742 745 746 747 750
        751 752 753 755 756 757 758 760 761 762 763 765 766 767 768
        770 771 772 773 775 776 777 778 780 781 782 783 785 786 787
        788 790 791 792 793 795 796 797 798 800 801 802 803 805 806
        807 808 810 811 812 813 815 816 817 818 820 821 822 823 825
        826 827 828 830 831 832 833 835 836 837 838 840 841 842 843
        845 846 847 848 850 851 852 853 855 856 857 858 860 861 862
        863 865 866 867 868 870 871 872 873 875 876 877 878 880 881
        882 883 885 886 887 888 890 891 892 893 895 896 897 898 900
        901 902 903 905 906 907 908 910 911 912 913 915 916 917 918
        920 921 922 923 925 926 927 928 930 931 932 933 935 936 937
        938 940 941 942 943 945 946 947 948 950 951 952 953 955 956
        957 958 960 961 962 963 965 966 967 968 970 971 972 973 975
        976 977 978 980 981 982 983 985 986 987 988 990 991 992 993
        995 996 997 998 1000 
        attempted = 500
        

        Like

    • John Crabtree's avatar

      John Crabtree 9:54 pm on 31 May 2021 Permalink | Reply

      If y = n * x + 1, this leads to (x – 1) * n * x = 1000 = 2^3 * 5^3
      And so x = 5, n = 50, y = 251, etc

      Like

      • Jim Randell's avatar

        Jim Randell 8:39 am on 1 June 2021 Permalink | Reply

        @John: A neat bit of analysis. It also provides the answer to (3) directly:

        y = kx + 1
        F(x, y) = xy − x − y = 999
        ⇒ kx(x − 1) = 1000

        N(x, y) = (x − 1)(y − 1) / 2 = (x − 1)kx / 2 = 1000/2

        Like

  • Unknown's avatar

    Jim Randell 5:10 pm on 28 May 2021 Permalink | Reply
    Tags:   

    Teaser 3062: Family stock 

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

    I have been transferring shares in the family business to my grandchildren, which I’ve done this as part of their birthday presents. On their first birthday I transferred one share, on their second birthday three shares, on their third birthday five shares etc. I have now four grandchildren and at the most recent birthday they were all of different ages. From my spreadsheet I noticed that the number of shares most recently transferred to each grandchild were all exact percentages of the cumulative total number of shares transferred to all of them so far.

    In increasing order, what are the ages of my grandchildren?

    [teaser3062]

     
    • Jim Randell's avatar

      Jim Randell 5:27 pm on 28 May 2021 Permalink | Reply

      I’m assuming the amounts transferred to each child at age k is (2k − 1) shares (so the cumulative total for this child will be k²). And the total amount transferred is the sum of all the shares transferred so far.

      The following Python program runs in 50ms.

      Run: [ @replit ]

      from enigma import (irange, inf, subsets, sq, div, printf)
      
      def solve():
        # consider the age of the eldest child
        for d in irange(4, inf):
          # and the younger children
          for (a, b, c) in subsets(irange(1, d - 1), size=3):
            T = sum(sq(k) for k in (a, b, c, d))
            if all(div(100 * (2 * k - 1), T) for k in (a, b, c, d)):
              yield (a, b, c, d)      
      
      for ages in solve():
        printf("ages = {ages}")
        break
      

      Solution: The children’s ages are: 1, 2, 3, 6.

      The total number of shares allocated so far is: (1) + (1 + 3) + (1 + 3 + 5) + (1 + 3 + 5 + 7 + 9 + 11) = 50.

      So any whole number of shares will be an exact percentage of the total, including the most recent allocations:

      1: 1; 1/50 = 2%
      2: 3; 3/50 = 6%
      3: 5; 5/50 = 10%
      6: 11; 11/50 = 22%

      The program stops after it finds the first solution, but I let it look for more, and there are no further solutions where the ages of the grandchildren are less than 200. (In fact it is sufficient to check d up to 49, beyond which the eldest child cannot achieve 4% of the total).


      There are also only 4 possible ages when the number of shares received by each child is required to be a whole number percentage of the total number of shares received by that child so far. (Which is how I originally read the puzzle — so I have modified the text slightly to clarify the intended interpretation).

      In this case the ages would be: 1, 2, 5, 10:

      1: 1; 1/1 = 100%
      2: 3; 3/4 = 75%
      5: 9; 9/25 = 36%
      10: 19; 19/100 = 19%

      Like

    • Brian Gladman's avatar

      Brian Gladman 6:58 am on 29 May 2021 Permalink | Reply

      Jim, shouldn’t this be that at age k a grandchild receives 2.k – 1 shares?

      Like

      • Jim Randell's avatar

        Jim Randell 8:49 am on 29 May 2021 Permalink | Reply

        @Brian: Yes, it should be. Fixed up now. Thanks. I’m so used to writing odd numbers as (2k + 1).

        Like

    • GeoffR's avatar

      GeoffR 7:31 am on 30 May 2021 Permalink | Reply

      from enigma import Timer
      timer = Timer('ST3062')
      timer.start()
           
      # Four grandchildren's ages are a, b, c, d
      for a in range(1, 19):  # child adult at age 18
        for b in range(a+1, 19):
          for c in range(b+1, 19):
            for d in range(c+1, 19):
              # Total number of shares issued
              T = a**2 + b**2 + c**2 + d**2
              # Test percentages are integers
              d1, r1 = divmod(((2 * a - 1) * 100), T)
              if r1 != 0:continue
              d2, r2 = divmod(((2 * b - 1) * 100), T)
              if r2 != 0:continue
              d3, r3 = divmod(((2 * c - 1) * 100), T)
              if r3 != 0:continue
              d4, r4 = divmod(((2 * d - 1) * 100), T)
              if r4 == 0 and a < b < c < d:
                print(f"Grandchildren's ages: {a}, {b}, {c}, {d}")
      
      timer.stop()
      timer.report()
      # [ST3062] total time: 0.0156250s (15.62ms)
      
      
      

      I tried the Timer from Jim’s enigma.py library as I could notget a time with the standard Python time module. The programme also runs satisfactorily by limiting the grandchildren’s ages to pre-teenage years (i.e. pre age 13), hence reducing the looping.
      @Jim:
      I got: [ST3062] total time: 0.0000000s (0.00us) for an upper bound of 13 for the loops.
      Am I using your Timer correctly?

      Like

      • Jim Randell's avatar

        Jim Randell 8:54 am on 30 May 2021 Permalink | Reply

        @geoff: Yes it looks like you are calling [[ Timer() ]] correctly.

        This what I get when I run your code:

        % python3.9 teaser3062geoff.py
        Grandchildren's ages: 1, 2, 3, 6
        [ST3062] total time: 0.0050900s (5.09ms)
        

        Could you tell me if you get more reasonable looking output with:

        timer = Timer('ST3062', timer='E')
        

        Like

    • GeoffR's avatar

      GeoffR 9:05 am on 30 May 2021 Permalink | Reply

      @Jim:
      Yes, looks better – I get the following output:

      [ST3062] total time: 0.0129913s (12.99ms)

      Like

      • Jim Randell's avatar

        Jim Randell 9:27 am on 30 May 2021 Permalink | Reply

        @Geoff: Thanks.

        It means your system is claiming that it has a function for reporting process time, but that function doesn’t seem to work.

        Using the ‘E’ parameter measures elapsed time instead (sometimes referred to as “wall clock” time), and that does seem to be working.

        Let me know the value of [[ sys.platform ]] and I’ll update the code to use elapsed time as the default for that system.

        Like

    • GeoffR's avatar

      GeoffR 9:36 am on 30 May 2021 Permalink | Reply

      @Jim:
      I get [win32] when I import sys and print sys.platform

      Like

    • Frits's avatar

      Frits 2:55 pm on 30 May 2021 Permalink | Reply

      Shows the first solution (it is getting slow quite quickly).

      from enigma import inf, irange, div
      
      # decompose a number into 4 different positive square numbers
      def decompose(t, m=0, ss=[]):
        if t == 0 and len(ss) == 4:
          yield ss
        else:
          for i in range(m + 1, t):
            s = i * i
            if s > t: break
            yield from decompose(t - s, i, ss + [i])
      
      found = 0
      # check different cumulative total number of shares
      for T in irange(30, inf):
        s = list(decompose(T))
        if s:
          for x in s:
            # check for exact percentages
            if all(div(100 * (2 * k - 1), T) for k in x):
              print(f"Grandchildren's ages: {x},  cumulative total={T}")
              found = 1
          if found:   
            break # only show first solution
      

      Like

    • Frits's avatar

      Frits 1:14 pm on 31 May 2021 Permalink | Reply

      Reporting all reasonable solutions.

      from enigma import is_square
      from math import ceil
      
      # decompose a number into 4 different positive square numbers
      def decompose(t, m=0, k=0, ss=[]):
        if k == 3:
          # check if last number is higher and a square  
          if t > ss[-1] ** 2:
            sr = is_square(t)
            if sr:
              yield ss + [sr]
        else:
          for i in range(m + 1, t):
            s = i * i
            # k = 1: 3*i^2 + 6*(i+1) - 1 < t        (i^2 + (i+1)^2 + (i+2)^2)
            # k = 2: 2*i^2 + 2*(i+1) - 1 < t        (i^2 + (i+1)^2) 
            if k > 0 and (4 - k) * s + (10 - 4 * k) * (i + 1) - 1 > t: break
            yield from decompose(t - s, i, k + 1, ss + [i])
      
      # only consider totals divisible by 5 and 10    
      # assume maximum age grand children to be 99
      for T in range(30, 38031, 5):
        # check which 2a - 1 parts have a whole number percentage compared to T
        sol = [2 * a - 1 for a in range(1, ceil(T ** 0.5)) 
               if (200 * a - 100) % T == 0]    
        if len(sol) > 3:    # at least 4 whole number percentages
          for s in list(decompose(T)):
            if all((2 * x - 1) in sol for x in s):
              print(f"Grandchildren's ages: {s},  cumulative total={T}")
      

      Shorter

      from math import ceil
      from itertools import combinations
      
      # only consider totals divisible by 5 and 10    
      # assume maximum age grand children to be 99
      for T in range(30, 38031, 5):
        # check which 2a - 1 parts have a whole number percentage compared to T
        sol = [2 * a - 1 for a in range(1, ceil(T ** 0.5)) 
               if (200 * a - 100) % T == 0]    
        if len(sol) > 3:    # at least 4 whole number percentages
          for c in combinations(sol, 4):
            if sum(((x + 1) // 2) ** 2 for x in c) == T:
              print(f"Grandchildren's ages: {[((x + 1) // 2) for x in c]}, "
                    f"cumulative total={T}")
      

      Like

      • Brian Gladman's avatar

        Brian Gladman 5:47 pm on 2 June 2021 Permalink | Reply

        If the age of the oldest grandchild is M, their percentage of the shares is less than 100.(2.M – 1) / M^2 and this must be at least 4. Hence M is at most 49 and T is at most 9030.

        Like

        • Brian Gladman's avatar

          Brian Gladman 10:31 pm on 2 June 2021 Permalink | Reply

          I believe this analysis can be improved to show that M < 18 (see here)

          Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 10:16 am on 1 June 2021 Permalink | Reply

      Obsessed as I am with minimising the number of wasted trials, I ended up simplifying it down to an essentially manual solution. I have coded this up. I believe the solution is unique.

      
      #Find all sets of ages for a given number of granchildren, whose squares sum to a chosen value
      
      def find_ages(S=100,gc=4,found=[]):
        if gc<=1: 
          if (S**(1/2))%1==0: yield found + [int(S**(1/2))]
        else: 
          A_min=tuple([i+1 for i in range(gc)])
          eldest_max=int((S-sum([a**2 for a in A_min[:-1]]))**(1/2))
          if eldest_max>=A_min[-1]:
            eldest_min=max(A_min[-1]-1,int((S/2)**(1/2)))  
            for eldest in range(eldest_max,max(A_min[-1]-1,int((S/2)**(1/2))),-1):
              fnd = found + [eldest]
              yield from find_ages(S-eldest**2,gc-1,fnd)
      
        
      Grandchildren=4    
      min_ages=(i+1 for i in range(Grandchildren))
      min_shares=sum(a**2 for a in min_ages)
      
      #Consider cases in which total number of shares S is less than 100
      #If S<100 and S is an integer, k.S =100 where k is an integer factor of 100
      
      k=2
      S_cand=100
      S_vec=[]
      while S_cand > min_shares:
        S_cand=100/k
        if S_cand%1==0 and S_cand>min_shares:
          S_vec.append(S_cand)
        k+=1
      
      #Now consider cases in which total number of shares S is greater than 100
      #If S>100 and S is an integer, S =100k where k is an integer
      #For all ages a, 2a-1 = k.c(a), where all c(a) are integers
      #Further, for all positive integer a, k and c(a) must both be odd
      #Using these results, we can work out the lowest possible ages corresponding to each value of k, and thus the lowest possible value of the sum of the squares. This number exceeds 100k for all values of k above 3.
      
      S_vec+=[100*k for k in (1,3)]
      
      #Find all combinations of ages that meet the requirement for the total S
      for Shares in S_vec:
        test=find_ages(Shares,Grandchildren)  
        for t in test:
          if False not in [((a*2-1)*100/Shares)%1==0 for a in t]:
            print("If the total number of shares is", Shares,"then the grandchildren's ages are",t,"and they received", [(a*2-1)*100/Shares for a in t],"shares respectively on their last birthday.")
      
      
      

      This seems to be no quicker than an iterative solution, but it was fun dredging my memory for some O-level maths.

      Like

      • Jim Randell's avatar

        Jim Randell 3:23 pm on 1 June 2021 Permalink | Reply

        I used my program to look for additional solutions and there are no others where the ages of the grandchildren are less than 200. (Beyond which none of the amounts can be whole number percentages of the total).

        So I’m happy that the puzzle has a unique solution.

        Like

    • GeoffR's avatar

      GeoffR 2:46 pm on 10 June 2021 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Upper bound as Brian/Jim's postings
      var 1..49:A; var 1..49:B; var 1..49:C; var 1..49:D;
      
      % Total cumulative shares of all grandchildren = T
      % Lower Bound of T = 1**2 + 2**2 + 3**2 + 4**2 = 30
      % Upper Bound of T = 46**2 + 47**2 + 48**2 + 49**2 = 9030
      
      var 30..9030: T = A*A + B*B + C*C + D*D;
      
      % All shares transferred are exact percentages
      constraint (2*A - 1) * 100 div T > 0 /\ (2*A - 1) * 100 mod T = 0 
      /\  (2*B - 1) * 100 div T > 0 /\ (2*B - 1) * 100 mod T = 0
      /\  (2*C - 1) * 100 div T > 0 /\ (2*C - 1) * 100 mod T = 0
      /\  (2*D - 1) * 100 div T > 0 /\ (2*D - 1) * 100 mod T = 0;
      
      % Ages are different and in increasing order
      constraint A < B /\ B < C /\ C < D;
      
      solve satisfy;
      
      output ["Grandchildrens' ages are " ++ show(A) ++ ", " ++ show(B)
      ++ ", " ++ show(C) ++ ", " ++ show(D) ];
      
      % Grandchildrens' ages are 1, 2, 3, 6
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 12:21 pm on 27 May 2021 Permalink | Reply
    Tags:   

    Teaser 2797: Sunday Times table 

    From The Sunday Times, 1st May 2016 [link] [link]

    Do you remember reciting your “times tables” — for example, one seven is 7, two sevens are 14, three sevens are 21, continuing 28, 35, … ” and going on for ever?

    I have consistently replaced some digits by letters and in this way the five-figure number TIMES can be found in the times table of each of its digits but not in the times table of any other digit. On the other hand, TABLE can be found in the times table of seven different digits, each of which is a digit of TIMES or TABLE.

    What number would be BEST?

    [teaser2797]

     
    • Jim Randell's avatar

      Jim Randell 12:21 pm on 27 May 2021 Permalink | Reply

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

      It runs in 130ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # collect digits that divide x
      --code="divs = lambda x: set(d for d in irange(1, 9) if x % d == 0)"
      
      # TIMES is divisible by each of its digits, but no other digits
      "divs(TIMES) == {E, I, M, S, T}"
      
      # TABLE is divisible by 7 different digits, each of which is in TIMES or TABLE
      "(lambda ds=divs(TABLE): len(ds) == 7 and ds.issubset({A, B, E, I, L, M, S, T}))()"
      
      --answer="BEST"
      

      Solution: BEST = 3842.

      Like

    • Frits's avatar

      Frits 10:39 am on 30 May 2021 Permalink | Reply

      from itertools import permutations
      
      # return 1-digit divisors of n
      divs = lambda n: [i for i in range(1, 10) if n % i == 0]
      # are all elements of sequence s2 part of sequence s1?
      issubset = lambda s1, s2: all(str(y) in s1 for y in s2)
      
      for T, I, M, E, S in permutations("123456789", 5):
       TIMES = T + I + M + E + S
       ds = divs(int(TIMES))
       # TIMES is divisible by each of its digits, but no other digits
       if len(ds) != 5: continue
       if not issubset(TIMES, ds): continue
       
       remaining = set('123456789').difference([T, I, M, E, S])
      
       for A, B, L in permutations(remaining, 3):
         TABLE = TIMES[0] + A + B + L + TIMES[3]
         
         ds = divs(int(TABLE))
         # TABLE is divisible by 7 different digits, 
         # each of which is in TIMES or TABLE
         if len(ds) == 7: 
           if issubset(TIMES + TABLE, ds):
             print("BEST =", B + E + S + T)
      

      Like

  • Unknown's avatar

    Jim Randell 8:47 am on 25 May 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 766: Engagement party 

    From The Sunday Times, 21st March 1976 [link]

    Alf Walker and Jill Jones celebrated their engagement at a dinner party with younger members of their families and some friends. Of the men, two were named Smith, two Jones, and two Walker; these were similarly the maiden names of the women.

    The party of six couples sat at a rectangular table, one pair at each end, and two pairs along each side. Engaged and married couples, also men and women, were arranged alternately around the table. Nobody was engaged or married to, or sat opposite to, anyone of the same original surname.

    Alf, with Jill on his left, sat at the table head, and Don and Greta sat at the foot. Two sisters, Ivy and Lena, were on Alf’s side of the table, while Jill’s brother Eddy sat next to Jill on her side. Fred and Lena each sat between a Smith and a Jones.

    Others present were Jill’s school friend Kate, Alf’s friend Bill and his sister Hilda, and Cyril and his sister Greta.

    Name the other engaged couples.

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 1 (1980). The puzzle text above is taken from the book.

    [teaser766]

     
    • Jim Randell's avatar

      Jim Randell 8:48 am on 25 May 2021 Permalink | Reply

      A bit convoluted, but not that tricky.

      The following Python program runs in 49ms.

      Run: [ @replit ]

      from enigma import subsets, update, chunk, printf
      
      # paired and opposite positions
      pairs = { (0, 1), (2, 3), (4, 5), (6, 7), (8, 9), (10, 11) }
      opps  = { (0, 7), (1, 6), (2, 11), (3, 10), (4, 9), (5, 8) }
      
      # positions we know
      first = list('AJE...DG.L.I')
      last  = list('WJJ.........')
      
      # allocate the remaining male last names
      for ns in subsets('SSJW', size=len, select="mP"):
        ls = update(last, [4, 6, 8, 10], ns)
      
        # L (pos 9) is between S and J
        if {ls[8], ls[10]} != {'S', 'J'}: continue
      
        # allocate the remaining female last names
        for ns in subsets('SSJWW', size=len, select="mP"):
          ls = update(ls, [3, 5, 7, 9, 11], ns)
      
          # I (pos 11) and L (pos 9) are siblings
          if ls[11] != ls[9]: continue
      
          # no opposite last names are the same
          if any(ls[x] == ls[y] for (x, y) in opps): continue
      
          # no pairs of last names are the same
          if any(ls[x] == ls[y] for (x, y) in pairs): continue
      
          # allocate the remaining male first names
          for ns in subsets('BCF', size=len, select="P"):
            fs = update(first, [4, 8, 10], ns)
      
            # F is between S and J
            f = fs.index('F')
            if {ls[f - 1], ls[f + 1]} != {'S', 'J'}: continue
      
            # C and G are siblings
            if ls[fs.index('C')] != ls[fs.index('G')]: continue
      
            # allocate the remaining female first names
            for ns in subsets('HK', size=len, select="P"):
              fs = update(fs, [3, 5], ns)
      
              # B and H are siblings
              if ls[fs.index('B')] != ls[fs.index('H')]: continue
      
              # collect the names
              ns = list(f + l for (f, l) in zip(fs, ls))
      
              # output the layout in pairs
              for (i, (x, y)) in enumerate(chunk(ns, 2)):
                printf("{x} + {y}; {z}", z=['engaged', 'married'][i % 2])
              printf()
      

      Solution: The other two engaged couples are: Fred Smith & Hilda Jones; Cyril Smith & Lena Walker.

      Going round the table the couples are:

      Alf Walker & Jill Jones (engaged)
      Eddy Jones & Kate Smith (married)
      Fred Smith & Hilda Jones (engaged)
      Don Walker & Greta Smith (married)
      Cyril Smith & Lena Walker (engaged)
      Bill Jones & Ivy Walker (married)

      Like

  • Unknown's avatar

    Jim Randell 11:36 am on 24 May 2021 Permalink | Reply
    Tags:   

    Teaser 2527: [Number prefixes] 

    From The Sunday Times, 27th February 2011 [link] [link]

    Without repeating a digit I wrote down two five-figure numbers. For the first of these, its first two digits form a number divisible by two, its first three digits form a number divisible by three, and likewise for four and five. For the second number, looking again at the numbers formed by the first few digits, those of prime length are prime and the one of length four is a square. Furthermore the sum of the digits of the difference between the two numbers is itself a digit. Without doing much division you should now be able to answer the question:

    What are the two five-figure numbers?

    This puzzle was originally published with no title.

    [teaser2527]

     
    • Jim Randell's avatar

      Jim Randell 11:37 am on 24 May 2021 Permalink | Reply

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

      The following run file executes in 122ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # suppose the numbers are: ABCDE and FGHIJ
      
      SubstitutedExpression
      
      # first 2, 3, 4, 5 digits of ABCDE are divisible by 2, 3, 4, 5
      "AB % 2 = 0"  # or: "B % 2 = 0"
      "ABC % 3 = 0"
      "ABCD % 4 = 0"  # or: "CD % 4 = 0"
      "ABCDE % 5 = 0"  # or: "E % 5 = 0"
      
      # first 2, 3, 5 digits of FGHIJ are prime
      "is_prime(FG)"
      "is_prime(FGH)"
      "is_prime(FGHIJ)"
      
      # FGHI is a perfect square
      "is_square(FGHI)"
      
      # the sum of the digits of the difference is a single digit
      "dsum(ABCDE - FGHIJ) < 10"
      
      --answer="(ABCDE, FGHIJ)"
      

      Solution: The numbers are: 52840 and 73961.

      Like

    • Frits's avatar

      Frits 12:07 pm on 26 May 2021 Permalink | Reply

      from itertools import compress
      from math import isqrt
      
      # return true if an integer <n> is a perfect square
      def is_square(n):
        if n % 144 not in {0, 1, 4, 9, 16, 25, 36, 49, 52,
                           64, 73, 81, 97, 100, 112, 121}:
          return False
        return isqrt(n) ** 2 == n
      
      def primesbelow(n):
        # rwh_primes1v2(n):
        """ Returns a list of primes < n for n > 2 """
        sieve = bytearray([True]) * (n // 2 + 1)
        for i in range(1, int(n ** 0.5) // 2 + 1):
          if sieve[i]:
            sieve[2*i*(i+1)::2*i+1] = \
            bytearray((n // 2 - 2 * i * (i + 1)) // (2 * i + 1) + 1)
        return [2, *compress(range(3, n, 2), sieve[1:])]
        
      digits = "0123456789"
      P = primesbelow(100000)
      odds = ["1", "3", "5", "7", "9"]
      
      second = []
      # determine second number FGHIJ
      for FG in [str(x) for x in P if 11 < x < 100]:
        (F, G) = (FG[0], FG[1])
        for H in [x for x in odds if x not in {F, G}]:
          if int(FG + H) not in P: continue
          for I in [x for x in digits if x not in {F, G, H}]:
            if not is_square(int(FG + H + I)): continue
            for J in [x for x in odds if x not in {F, G, H, I}]:
              if int(FG + H + I + J) not in P: continue
              second.append(FG + H + I + J)
      
      for FGHIJ in second:
        remaining = "".join(x for x in digits if x not in FGHIJ)
       
        # determine first number ABCDE
        for E in [x for x in remaining if x in "05"]:
          for B in [x for x in remaining if x not in {E} and x in "02468"]:
            for A in [x for x in remaining if x not in {B, E} and x != "0"]:
              for C in [x for x in remaining if x not in {A, B, E}]:
                if int(A + B + C) % 3: continue
                for D in [x for x in remaining if x not in {A, B, C, E}]:
                  if int(C + D) % 4: continue
                  
                  ABCDE = int(A + B + C + D + E)
                  # the sum of the digits of the difference between the two numbers
                  # is itself a digit
                  if sum(int(x) for x in str(abs(int(FGHIJ) - ABCDE))) > 9: continue
                  print(f"the two five-figure numbers are: {ABCDE} and {FGHIJ}")
      

      Like

    • GeoffR's avatar

      GeoffR 4:20 pm on 26 May 2021 Permalink | Reply

      
      from itertools import permutations
      from enigma import is_prime
      
      sq4 = [x ** 2 for x in range(32, 100)]
      
      # Find digits for first 5-digit number
      for P1 in permutations('1234567890', 5):
          A, B, C, D, E = P1
          if A == '0':
              continue
      
          # two,three, four and five digit numbers
          # are divisible by two, three, four and five
          AB = int(A + B)
          if AB % 2 != 0:
              continue
          ABC = int(A + B + C)
          if ABC % 3 != 0:
              continue
          ABCD = int(A + B + C + D)
          if ABCD % 4 != 0:
              continue
          ABCDE = int(A + B + C + D + E)
          if ABCDE % 5 != 0:
              continue
      
          # Find digits for second 5-digit number
          Q1 = set('1234567890').difference([A, B, C, D, E])
          for P2 in permutations(Q1):
              F, G, H, I, J = P2
              if F == '0':
                  continue
      
              # two, three and five digit numbers are prime
              FG = int(F + G)
              if not is_prime(FG):
                  continue
              FGH = int(F + G + H)
              if not is_prime(FGH):
                  continue
              # four digit number is square
              FGHI = int(F + G + H + I)
              if FGHI not in sq4:
                  continue
              FGHIJ = int(F + G + H + I + J)
              if not is_prime(FGHIJ):
                  continue
              
              # the sum of the digits in the difference between
              # the two numbers is a digit
              dd = sum(int(x) for x in str(abs(ABCDE - FGHIJ)))
              if not dd < 10:
                  continue
              print(f"Two five-figure numbers are {ABCDE} and {FGHIJ}.")
      
      # Two five-figure numbers are 52840 and 73961.
      
      

      Like

    • John Crabtree's avatar

      John Crabtree 6:46 pm on 26 May 2021 Permalink | Reply

      As the 3rd digit of N2 is odd, the 4th digit is 6.
      (10a + 4)^2 = 100a(a + 1) – 20a + 16, with 2nd digit odd for 0 < a < 6
      (10a + 6)^2 = 100a(a + 1) + 20a + 36, with 2nd digit odd for 3 < a N2 but the sum of the digits of the difference (SDD) cannot be 2
      86^2 = 7396, N2 = 73961, N2 > N1, SDD = 7, N1 = 52840 (not 42850)

      Using a prime factor calculator, 739 and 73961 are all prime.

      Like

  • Unknown's avatar

    Jim Randell 4:46 pm on 21 May 2021 Permalink | Reply
    Tags:   

    Teaser 3061: Ratio 

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

    Liam has split a standard pack of 52 cards into three piles; black cards predominate only in the second pile. In the first pile the ratio of red to black cards is 3 to 1. He transfers a black card from this pile to the second pile; the ratio of black to red cards in the second pile is now 2 to 1. He transfers a red card from the first pile to the third pile; the ratio of red to black cards in this pile is now a whole number to one.

    Liam told me how many cards (a prime number) were initially in one of the piles; if I told you which pile you should be able to solve this teaser.

    How many cards were initially in the third pile?

    [teaser3061]

     
    • Jim Randell's avatar

      Jim Randell 5:27 pm on 21 May 2021 Permalink | Reply

      I assume that Liam told the setter a specific pile number, and the total number of cards that was initially in that pile and this number of cards was a prime number.

      So knowing the number of the pile, and the fact that the total number of cards in that pile was prime (but not knowing the number itself), is sufficient for us to determine the number of cards that were in the third pile.

      This Python program runs in 48ms.

      Run: [ @replit ]

      from enigma import (irange, div, is_prime, printf)
      
      # generate possible piles
      # return (piles = ((red, black), ...), {indices of prime piles})
      def piles():
        # choose the number of red cards in pile 1 (a multiple of 3)
        for r1 in irange(3, 26, step=3):
          # there are 3 times as many reds as blacks
          b1 = div(r1, 3)
      
          # choose the number of black cards in pile 2 (+1 = a multiple of 2)
          for b2 in irange(3, 26 - b1, step=2):
            # with 1 extra black there are twice as many blacks as reds
            r2 = div(b2 + 1, 2)
            if r1 + r2 > 26: break
      
            # pile 3 has the remaining cards
            r3 = 26 - r1 - r2
            b3 = 26 - b1 - b2
            if b3 > r3: continue
            # with 1 extra red there are k times as many reds as blacks
            k = div(r3 + 1, b3)
            if k is None: continue
      
            printf("[{r1}R+{b1}B; {r2}R+{b2}B; {r3}R+{b3}B; k={k}]")
            ps = ((r1, b1), (r2, b2), (r3, b3))
            pr = set(i for (i, (r, b)) in enumerate(ps) if is_prime(r + b))
            yield (ps, pr)
      
      # collect piles, that have a prime number of cards in one of the piles
      ss = list((ps, pr) for (ps, pr) in piles() if pr)
      
      # look for unique solutions, identified by pile i being prime
      for i in (0, 1, 2):
        rs = list(ps for (ps, pr) in ss if i in pr)
        if len(rs) == 1:
          ((r1, b1), (r2, b2), (r3, b3)) = rs[0]
          printf("{r1}R+{b1}B; {r2}R+{b2}B; {r3}R+{b3}B [pile {i} is prime]", i=i + 1)
      

      Solution: There were initially 11 cards in the third pile.

      There are only 4 ways to construct the piles as described:

      [1] 3R+1B; 12R+23B; 11R+3B (4 + 35 + 13)
      [2] 6R+2B; 12R+23B; 8R+1B (8 + 35 + 9)
      [3] 9R+3B; 10R+19B; 7R+4B (12 + 29 + 11)
      [4] 12R+4B; 11R+21B; 3R+1B (16 + 32 + 4)

      The only collections with a prime number of cards in at least one pile are [1] (pile 3) and [3] (pile 2, pile 3).

      So Liam must have revealed there were 29 cards in pile 2, which means he has constructed collection [3] (as that is the only collection with a prime number of cards in pile 2).

      Although if Liam had revealed just the prime numbers of cards in the pile (without giving the pile number); 11, 13, or 29, that would have been sufficient to determine which collection he had constructed.

      So the second paragraph could be:

      “Liam told me the total number of cards that was initially in one of the piles (but not the number of the pile). This was a prime number, and that enabled me to work out the composition (number of red and black cards) of each of the piles. If I now told you the number of the pile whose total Liam had revealed to me, you could also work out the composition of each of the piles.”

      Like

      • Frits's avatar

        Frits 9:02 pm on 22 May 2021 Permalink | Reply

        from enigma import SubstitutedExpression, is_prime, div
        
        # the alphametic puzzle
        p = SubstitutedExpression(
          [
          # (r1   , b1), (r2, b2), (r3, b3) = 
          # (3 * C, C),  (EF, GH), (IJ, KL)
          
          # both pile 1 and pile 3 contain at least 1 black card
          # black cards predominate only in the second pile
          "0 < EF < 13",
          
          "2 * EF - 1 = GH",
            
          # black cards predominate only in the second pile
          # "3 * C + EF <= C + GH",
          # "3 * C + EF <= C + 2 * EF - 1",
          "2 * C <= EF - 1",                #  EF < 13 --> C < 6 
          
          # with 1 extra red there are k times as many reds as blacks
          "div(27 - 3 * C - EF, 26 - C - GH)",
          
          # Liam told me how many cards (a prime number) were initially 
          # in one of the piles
          "any(is_prime(x) for x in [EF + GH, 52 - 3 * C - EF - C - GH])",
        
          ],
          answer="(3 * C, C), (EF, GH), (26 - 3 * C - EF, 26 - C - GH)",
          d2i=dict([(0, "C")] + 
                   [(k, "E") for k in range(2, 6)] + 
                   [(k, "CE") for k in range(6, 10)] 
                  ),
          distinct="",
          reorder=0,
          verbose=256)
        
        answs = [y for _, y in p.solve()]
        
        for p in range(3):  
          ps = [a for a in answs if is_prime(sum(a[p]))]
          if len(ps) == 1:  # unique
             print(f"third pile has {sum(ps[0][2])} cards")
        

        Based on the generated code and some more analysis (we can conclude r2 is even and b1 is less than 5) only 12 (r2, b1) combinations are considered:

        from enigma import is_prime
        
        answs = []
        # r2 must be even as last pile can't have only 2 cards
        # (so we deal with odd primes only)
        for r2 in range(4, 13, 2):
          b2 = 2 * r2 - 1
          p2 = is_prime(r2 + b2)
          for b1 in range(1, min(r2 // 2, 26 - b2)):
            # 26 - b1 - b2 > 0 --> b1 < 26 - b2 
            
            # with 1 extra red there are <k> times as many reds as blacks
            (d, r) = divmod(27 - 3 * b1 - r2, 26 - b1 - b2)
            if r > 0: continue
            # a prime number of cards were initially in one of the piles
            if p2 or is_prime(52 - 4 * b1 - r2 - b2): 
              answs.append([(3 * b1, b1), (r2, b2), (26 - 3 * b1 - r2, 26 - b1 - b2)])
        
        for p in range(3):  
          ps = [a for a in answs if is_prime(sum(a[p]))]
          if len(ps) == 1:  # unique
             print(f"third pile has {sum(ps[0][2])} cards") 
        

        Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 6:31 pm on 25 May 2021 Permalink | Reply

        I wanted to visualise this problem as points on the line where two surfaces intersect. Because of the simple relationships between the red count and black count in each pile, we can easily define planes on the same 3 axes such that one plane represents the black values and the others represent the set of reds corresponding to a given value of N, where N is the ratio of reds to blacks in the third pile, after the swaps. I followed the maths through and arrived at a set of constraints such that I had to test 11 combinations of (b1,b2) to get the 4 possible results. I expect my analysis is much the same as Frits’, but the route depends on which parameters we choose to loop. My code for the solution is not very interesting, but I thought I’d share my graphical representation. The code below is based on an approach set out by banderlog013 on stackoverflow. If you draw the plot for the appropriate value of N, and run your mouse down the line of intersection, you will see that only one point has integer values for x,y,z that sum to 26.

        
        
        import numpy as np
        import plotly.graph_objects as go
        from plotly.subplots import make_subplots
        from typing import Tuple, Iterable
        
        def plotPlane(fig: go.Figure,
                      normal: Tuple[int, int, int],
                      d: int,
                      values: Iterable,
                      colorScaleName: str) -> None:
        
            # x, y, z
            x, y = np.meshgrid(values, values)
            z = (-normal[0] * x - normal[1] * y - d) * 1. / normal[2]
        
            # draw plane
            surface = go.Surface(x=x, y=y, z=z, colorscale=colorScaleName, showscale=False)
            fig.add_trace(surface, row=1, col=1)
        
        N=1#Not correct but if you have solved the puzzle you will know what is
        
        point1  = -26
        normal1 = (1,1,1)
        
        point2  = -26.5
        normal2 = (3,1/2,N)
        
        # create figure
        fig = make_subplots(rows=1, cols=1, specs=[[{'type': 'surface'}]])
        # plot two intersectioned surfaces
        values = range(1, 26)
        plotPlane(fig, normal1, point1, values, "Hot")
        plotPlane(fig, normal2, point2, values, "Greys")
        fig.show()
        
        

        Like

    • Robert Brown's avatar

      Robert Brown 9:19 am on 22 May 2021 Permalink | Reply

      Following the steps in your program, I find a different answer from the one posted by Brian.

      My values are
      b1 = 1, r1 = 3: (r1 = 3*b1): total not prime
      b2 = 23, r2 = 12: (b2+1/r2 = 2): total not prime
      b3 = 2, r3 = 11: (r3+1/b3 = 6): total (13) is prime

      Am I doing something silly?

      Like

      • Jim Randell's avatar

        Jim Randell 9:49 am on 22 May 2021 Permalink | Reply

        @Robert: There are actually four collections of piles that can be constructed following the rules of the first paragraph of the puzzle text.

        But the second paragraph allows you to identify which one of those four provides the answer to the puzzle.

        Like

        • Robert Brown's avatar

          Robert Brown 7:53 am on 23 May 2021 Permalink | Reply

          Yes, Brian’s solution has prime totals for piles 2 & 3, my alternative just for pile 3. The other two have no prime totals. So if Liam had quoted the total for pile 3, we would have a choice of 2 solutions. It follows that he quoted the total for pile 2, leading to Brian’s solution.

          Like

          • Jim Randell's avatar

            Jim Randell 11:33 am on 23 May 2021 Permalink | Reply

            @Robert: That’s correct.

            Interestingly if Liam had revealed just the initial total number of cards in one of the piles (without revealing the number of the pile), and that number was prime, it would be enough for the setter to work out the composition of each of the piles.

            The setter can then tell us the number of the pile that Liam was referring to, and the fact that the total number of cards in that pile was prime, and this is enough for us to also work out the composition of each of the piles.

            Like

    • Hugh Casement's avatar

      Hugh Casement 1:13 pm on 30 May 2021 Permalink | Reply

      More troublesome logic.
      As far as I can see, the constraints are:
      red1 = 3 × black1
      black2 + 1 = 2 × red2
      (red3 + 1) mod black3 = 0
      and the total is 52 (with no variables being 0).

      I found well over 100 sets that satisfy those conditions!
      Prime numbers abound, so I don’t see how any conclusions can be drawn.
      How is it that others found only four possible sets?

      Like

    • Hugh Casement's avatar

      Hugh Casement 6:14 pm on 30 May 2021 Permalink | Reply

      Thanks for that, Jim. You can tell it’s a long time since I had a pack of cards in my hands!

      Like

  • Unknown's avatar

    Jim Randell 8:56 am on 20 May 2021 Permalink | Reply
    Tags:   

    Teaser 2792: Easter Teaser 

    From The Sunday Times, 27th March 2016 [link] [link]

    I have written down three numbers and then consistently replaced digits by letters, with different letters for different digits, to give:

    EASTER
    SUNDAY
    TEASER

    In fact the first number is the lowest and one of these numbers is the sum of the other two.

    What is this SUNDAY‘s number?

    [teaser2792]

     
    • Jim Randell's avatar

      Jim Randell 8:56 am on 20 May 2021 Permalink | Reply

      We can get a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      This run file executes in 421ms.

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "abs(SUNDAY - TEASER) = EASTER"
      
      "E < S" # EASTER < SUNDAY
      "E < T" # EASTER < TEASER
      
      --answer="SUNDAY"
      

      Solution: SUNDAY = 816270.


      For a faster solution we can use the (experimental) [[ SubstitutedExpression.split_sum() ]] solver.

      The following Python program runs in 58ms.

      Run: [ @replit ]

      from enigma import SubstitutedExpression, printf
      
      # solve the sum <expr>
      def check(expr, *extra):
        p = SubstitutedExpression.split_sum(expr, extra=extra, answer="SUNDAY").solver()
        for (s, ans) in p.solve(verbose=0):
          printf("SUNDAY = {ans} [{expr} / {s}]", s=p.substitute(s, expr))
      
      # check the two possibilities
      check('EASTER + SUNDAY = TEASER', 'E < S')
      check('EASTER + TEASER = SUNDAY', 'E < T')
      

      There is only one possible solution even without the information that EASTER is the lowest number. So the [[ E < S ]] and [[ E < T ]] clauses could be removed.

      Like

    • GeoffR's avatar

      GeoffR 9:58 am on 31 May 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9: E; var 0..9: A; var 1..9: S;
      var 1..9: T; var 0..9: R; var 0..9: U;
      var 0..9: N; var 0..9: D; var 0..9: Y;
      
      constraint all_different ([E,A,S,T,R,U,N,D,Y]);
      
      var 100000..999999: EASTER = 100000*E + 10000*A + 1000*S + 100*T + 10*E + R;
      var 100000..999999: SUNDAY = 100000*S + 10000*U + 1000*N + 100*D + 10*A + Y;
      var 100000..999999: TEASER = 100000*T + 10000*E + 1000*A + 100*S + 10*E + R;
      
      constraint EASTER < SUNDAY /\ EASTER < TEASER;
      constraint EASTER + TEASER == SUNDAY \/ EASTER + SUNDAY == TEASER;
      
      solve satisfy;
      output ["SUNDAY's number = " ++ show(SUNDAY) ];
      % SUNDAY's number = 816270
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:39 am on 18 May 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 768: Bus ticket numbers 

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

    On a recent bus journey I purchased the tickets for my wife and myself. On each was a four-figure number, and the sum of all eight digits was twenty-five.

    I remarked upon this to my wife who thereupon asked if any digit appeared more than twice in total, and whether the sum of the digits on either ticket was equal to thirteen.

    I answered both questions and my wife was able to deduce the two numbers on the tickets.

    What were they?

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 1 (1980). The puzzle text above is taken from the book.

    [teaser768]

     
    • Jim Randell's avatar

      Jim Randell 9:41 am on 18 May 2021 Permalink | Reply

      If the ticket numbers are randomised then there is no solution. So I assumed that the tickets are consecutively numbered (I think the puzzle text should have stated this).

      This Python program runs in 84ms.

      Run: [ @replit ]

      from enigma import irange, tuples, nsplit, unpack, multiset, filter_unique, nconcat, printf
      
      # consider possible consecutive values for two tickets
      tickets = tuples((nsplit(n, 4) for n in irange(1, 9999)), 2)
      
      # the sum of all the digits is 25
      tickets = filter(unpack(lambda xs, ys: sum(xs) + sum(ys) == 25), tickets)
      
      # questions asked
      def q(xs, ys):
        # does any digit appear more than twice?
        ds = multiset.from_seq(xs, ys)
        q1 = any(v > 2 for v in ds.values())
        # do the digits on either ticket sum to 13?
        q2 = (sum(xs) == 13 or sum(ys) == 13)
        return (q1, q2)
      
      # the answers to the questions allow the tickets to be deduced
      ss = filter_unique(tickets, unpack(q)).unique
      
      # output solutions
      for (xs, ys) in ss:
        printf("tickets = {x}, {y}", x=nconcat(xs), y=nconcat(ys))
      

      Solution: The ticket numbers were 1299 and 1300.

      So the answers to questions were: No (no digit appears more than twice in total), and no (the digits on neither ticket sum to 13).

      For (yes, no), there are only three pairs: 0399 and 0400; 2199 and 2200; 3099 and 3100.

      For (no, yes) there are 132 pairs, and for (yes, yes) there are 273 pairs.

      Like

    • Frits's avatar

      Frits 8:25 pm on 31 May 2021 Permalink | Reply

      Doing the same (but not allowing leading zeroes) without using all the handy stuff in enigma.py.
      Some steps could have been combined.

      # convert digits to number
      d2n = lambda args: int("".join(map(str, args)))
      
      # return entries where the combination of columns <col1> and <col2> is unique
      def unique_cols(seq, col1=0, col2=1):
        return [s1 for s1 in seq 
                if len([1 for s2 in seq if s2[col1] == s1[col1] and 
                                           s2[col2] == s1[col2]]) == 1] 
       
      
      # consider possible consecutive values for two tickets
      tickets = list(zip((s := [[int(x) for x in str(n)] 
                          for n in range(1000, 10000)]), s[1:]))
       
      # the sum of all the digits is 25
      tickets = [(x, y) for (x, y) in tickets if sum(x) + sum(y) == 25]
      
      # store answers to the 2 questions
      tickets = [(any((s := x + y).count(p) > 2 for p in s),
                 sum(x) == 13 or sum(y) == 13, x, y) for (x, y) in tickets]
      
      # the answers to the questions allow the tickets to be deduced
      tickets = unique_cols(tickets)
      
      for t in tickets:
        print(f"tickets = {d2n(t[2])}, {d2n(t[3])}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:32 am on 16 May 2021 Permalink | Reply
    Tags: by: Andrew Pennycook   

    Brain-Teaser 34: Rouge et noir 

    From The Sunday Times, 12th November 1961 [link]

    Audrey, Barbara, Catherine, Dorothy and Elizabeth were visiting Monte Carlo and decided to form a syndicate to play roulette. They bought ten counters and played on red (an even chance) all the time. Each time red lost they doubled the stake of the lost throw, but staked only a single counter after a win, irrespective of who was playing next. In order that each should have a chance to play, they took turns in alphabetical order, to play five throws running.

    They each had three wins and two losses in their five throws. Audrey and Dorothy both started with two wins and a loss. Catherine and Elizabeth had three consecutive wins and showed profits of five and four counters respectively. Barbara was the only one who didn’t show a profit.

    What was the order of colours for the twenty-five throws?

    [teaser34]

     
    • Jim Randell's avatar

      Jim Randell 9:33 am on 16 May 2021 Permalink | Reply

      This Python program runs in 49ms.

      Run: [ @replit ]

      from enigma import printf
      
      # construct sequence of win (R)/ loss (B) outcomes
      # k = current kitty
      # s = current stake
      # w = winnings for current player
      # ps = outcomes for the current player
      # ss = all outcomes
      def solve(k, s, w=0, ps='', ss=[]):
        # player n, go g
        n = len(ss)
        g = len(ps)
        # has this player had 5 goes?
        if g == 5:
          # only B did not make a profit
          if (n != 1) != (w > 0): return
          # C had a profit of 5, E had a profit of 4
          if n == 2 and w != 5: return
          if n == 4 and w != 4: return
          # C and E had 3 wins in a row
          if (n == 2 or n == 4) and 'RRR' not in ps: return
      
          # have all 5 players had their turn?
          ss_ = ss + [(ps, w)]
          if len(ss_) == 5:
            yield (k, ss_)
          else:
            # set up the next player
            yield from solve(k, s, 0, '', ss_)
      
        elif not(s > k):
          # choose an outcome for player n:
          # A and D start with win, win, lose = RRB
          if g < 3 and (n == 0 or n == 3):
            qs = ('B' if g == 2 else 'R')
          else:
            qs = 'BR'
          for q in qs:
            ps_ = ps + q
            # each player get 3 wins and 2 losses
            if ps_.count('R') > 3 or ps_.count('B') > 2: continue
            # a win?
            if q == 'R':
              yield from solve(k + s, 1, w + s, ps_, ss)
            else:
              # a loss
              yield from solve(k - s, 2 * s, w - s, ps_, ss)
      
      # start with a kitty of 10 and a stake of 1
      for (k, ss) in solve(10, 1):
        for (n, (ps, w)) in zip("ABCDE", ss):
          printf("{n}: {ps} -> {w}")
        printf("profit = {w}", w=k - 10)
        printf()
      

      Solution: The colours were: (R R B B R) (R R R B B) (B R R R B) (R R B R B) (B B R R R)

      The corresponding wins/losses are: (+1 +1 −1 −2 +4) (+1 +1 +1 −1 −2) (−4 +8 +1 +1 −1) (+2 +1 −1 +2 −1) (−2 −4 +8 +1 +1)

      For each player: A=+3; B=0; C=+5; D=+3; E=+4.

      Making a total profit of +15.

      Like

    • Frits's avatar

      Frits 2:01 pm on 2 June 2021 Permalink | Reply

      To be run with PyPy, somehow the “denest=1” option doesn’t seem to work here.

      from enigma import SubstitutedExpression
      
      # determine score based on bet set by previous sequence  
      def score(seq, prevseq):
        # remove trailing zeroes
        while prevseq[-1] == 0:
          del prevseq[-1]
        
        bet = 2 ** (5 - len(prevseq))
        t = 0
        for x in seq:
          t = t - bet if x == 0 else t + bet
          bet = 2 * bet if x == 0 else 1
        return t 
        
      
      # the alphametic puzzle
      p = SubstitutedExpression(
      [
      # they each had three wins and two losses in their five throws
      "all(sum(x) == 3 for x in [[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], \
                                 [P,Q,R,S,T], [U,V,W,X,Y]])",
      
      # Catherine and Elizabeth had three consecutive wins and ...
      "all(x in [(1, 1, 0), (0, 0, 1), (0, 1, 1)] for x in [(K, L, N), (U, V, X)])",
      # ... showed profits of five and four counters respectively
      "score([K,L,M,N,O], [F,G,H,I,J]) == 5",
      "score([U,V,W,X,Y], [P,Q,R,S,T]) == 4",                                   
      
      # Barbara was the only one who didn't show a profit
      "score([F,G,H,I,J], [A,B,C,D,E]) < 1",  
      ],
      answer="[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], [P,Q,R,S,T], [U,V,W,X,Y]",
      digits=range(0, 2),
      # Audrey and Dorothy both started with two wins and a loss    
      d2i=dict([(0, "ABPQMW")] + [(1, "CR")]),
      env=dict(score=score),         
      distinct="",
      #denest=1, # [CPython] work around statically nested block limit
      verbose=0)
      
      # print answers
      for (_, ans) in p.solve():
        for a in ans:
          rb = "".join("R" if x else "B" for x in a)
          print(f"({rb})", end=" ")
      print("")
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:55 pm on 2 June 2021 Permalink | Reply

        @Frits: You can specify a more aggressive “denest” strategy, try [[ denest=32 ]]. (The default is 50).

        Like

  • Unknown's avatar

    Jim Randell 4:10 pm on 14 May 2021 Permalink | Reply
    Tags:   

    Teaser 3060: Even or odd 

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

    My daughter and I once played a game based on the number 13 and the following rule:

    Think of a positive whole number greater than 1.
    If it is even, halve it.
    If it is odd multiply it by 13 and add 1.
    Either of these operations is to be regarded as one step.
    Apply another step to the outcome of the first step, and then further steps successively.

    For our game, we chose different starting numbers that were odd, and the winner was the person who by the fewer number of steps reached the number 1. My daughter won because she started with the number that leads to 1 in the fewest number of steps.

    What was my daughter’s starting number?

    [teaser3060]

     
    • Jim Randell's avatar

      Jim Randell 4:31 pm on 14 May 2021 Permalink | Reply

      Working backwards from 1 we can find numbers that require increasing numbers of steps to reach 1, and then look for the first odd number.

      This Python program runs in 49ms.

      Run: [ @replit ]

      from enigma import (div, join, printf)
      
      # consider increasing numbers of steps
      k = 0
      ns = [1]
      while True:
        # take 1 step back
        even = list()
        odd = list()
        for n in ns:
          even.append(2 * n)
          x = div(n - 1, 13)
          if x is not None and x > 1 and x % 2 == 1:
            odd.append(x)
      
        k += 1
        ns = sorted(even + odd)
        printf("[{k} steps -> {ns}]")
      
        if odd:
          printf("solution = {odd} [in {k} steps]", odd=join(odd, sep=", "))
          break
      

      Solution: Her starting number was 315.

      Which takes 13 steps to arrive at 1 (as 315×13 + 1 = 4096 = 2^12).


      The first odd number can easily be found by looking for the smallest power of 2 that has a residue of 1 when divided by 13:

      >>> peek(d for (d, r) in (divmod(n, 13) for n in bit_permutations(2)) if r == 1) 
      315
      

      And you can find the answer with a calculator in a few seconds. Just repeatedly multiply 1/13 by 2 until you get an answer of the form x + 1/13, and the answer is x.

      I feel the setter could have had more fun with this sequence. See: [ @wikipedia ]

      As an extra puzzle, you might like to find the two different odd numbers, that would lead to a draw in the fewest number of steps.


      The answer to my additional puzzle is: 977211 and 25407803.

      These both reach 1 after 34 steps:

      [0] 977211; 25407803
      [1] 12703744; 330301440
      [2] 6351872; 165150720
      [3] 3175936; 82575360
      [4] 1587968; 41287680
      [5] 793984; 20643840
      [6] 396992; 10321920
      [7] 198496; 5160960
      [8] 99248; 2580480
      [9] 49624; 1290240
      [10] 24812; 645120
      [11] 12406; 322560
      [12] 6203; 161280
      [13] 80640
      [14] 40320
      [15] 20160
      [16] 10080
      [17] 5040
      [18] 2520
      [19] 1260
      [20] 630
      [21] 315
      [22] 4096 = 2^12

      [34] 1

      After the first 13 steps both sequences have reached 80640, which after another 8 steps reaches 315. And then 315 takes a further 13 steps to reach 1.

      Like

    • Frits's avatar

      Frits 9:51 pm on 14 May 2021 Permalink | Reply

      In order to reach 1 you have to reach a power of 2 first.

      from enigma import irange, inf
      
      for i in irange(2, inf):
        (d, r) = divmod(2 ** i  - 1, 13)
        if r == 0:
          print("daughter's starting number", d)
          break
      

      Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 8:55 am on 15 May 2021 Permalink | Reply

      My solution is similar to Frits’, and I have extended it to find the corresponding number of steps. I then put this in a loop to find a set of starting points that would give us a path through the solution. I failed to find a solution beyond the 18th in a reasonable time. I can’t decide whether this is because there is no further solution or my algorithm is too inefficient.

      
      def solve(d):
        p=d*2
        while (p-1)%13!=0:
          p*=2
        return (p-1)//13, len(bin(p//d))-1
      
      
      i=1
      dest=1
      while i<19:
        res=solve(dest)
        print(res)
        dest=res[0] 
        i+=1
      
      

      Regarding Jim’s bonus question, my approach above provides one possible set of starting points, each of which passes through its successors. The total number of steps is then the sum of the steps from each to its immediate successor. An alternative path is to find the first solution, but continue multipying by 2 and recording each multiple that would also derive from an odd starting point. I found that these increased in units of 2**12. So one possible solution to Jim’s challenge is to find a starting point in the list my code generates, with a cumulative number of steps to our first solution which is a multiple of 12, n. A starting point that draws this number of steps is 2**n times the first solution.

      Like

      • Brian Gladman's avatar

        Brian Gladman 11:14 pm on 15 May 2021 Permalink | Reply

        HI Tony,

        You pose the question of whether your algorithm is too inefficient to find further solutions or whether there are none. But there is another possibility, namely that the algorithm you are using can only find a subset of the solutions. And this turns out to be true.

        Your approach (which I also used) is fine for finding solutions that are reached in the minimum number of steps but it only finds solutions where the forward process lands on a power of two immediately after the initial ’13 step’ .

        But there are solutions where this doesn’t happen. For example, the forward sequence for 6203 starts 6203, 80640, 40320, 20160, 10080, 5040, 2520, 1260, 630, 315, 4096, 2048, .. with a number after the initial forward step (80640) that is not a power of two.

        This means that to answer Jim’s extra question, you have to use an algorithm such as the one Jim uses that finds all solutions for a given number of steps. This turns out to be very efficient because (somewhat to my surprise) the number of solutions only grows at a modest rate as the number of steps increase.

        Like

        • Brian Gladman's avatar

          Brian Gladman 11:48 pm on 15 May 2021 Permalink | Reply

          PS. I have quoted a sequence that your approach does find – I should have quoted one it doesn’t find.

          Like

          • Tony Brooke-Taylor's avatar

            Tony Brooke-Taylor 12:33 pm on 17 May 2021 Permalink | Reply

            Thanks Brian. I get your point about the powers of two. I had been too interested in exploring larger numbers of steps when really we only need to look at a few handfuls – and in more detail. I believe my path routine hangs beyond the 18th step because by then I am at such high powers of 2 my machine is struggling to compute.

            I have to conclude that Jim’s approach is much slicker than what I was trying to do. Nevertheless, out of pig-headedness I have extended my approach in an attempt to emulate the results from Jim’s algorithm. I still struggle to be certain of exhaustivity beyond a certain number of steps but my list is ALMOST the same as Jim’s: the first disagreement arises at 38 steps; where Jim produces a solution that I cannot replicate. To anyone who has not already moved on: I’d be grateful for any insight into what’s gone wrong.

            
            import itertools
            from functools import reduce
            
            #Finds the first odd starting point that produces input number d
            #Also reports the number of steps 
            def solve(d):
              rungs=[]
              step=1
              p=d*2
            #  while (p-1)%13!=0 and step<=50:
              while step<=13:
                p*=2
                step+=1
                if (p-1)//13 == (p-1)/13:
                  rungs+=((p-1)//13, len(bin(p//d))-2)
                  return ((p-1)//13, len(bin(p//d))-2)
            #    return rungs
            
            #Finds the ladder of starting points that produce the input number r
            #Each step in the ladder results in its predecessor in the list, with
            #the reported number of steps
            
            def ladder(r):
              i=1
              dest=r
              points=[]
              while i<50:
                res=solve(dest)
                if res == None:break
                points.append(res)
                dest=res[0]
                i+=1
              return points
            
            #Exploits Fermat's little theorem to generate a series of solutions from a single solution
            def project(rslt):
              rs = rslt[0]*13+1
              RS = [((rs*(2**(12*n))-1)//13) for n in range(1,6)]
              LT = [rslt[1]+12*n for n in range(1,6)]
              return [(rl,st) for rl,st in zip(RS,LT)]
            
            #Runs the ladder algorithm for each solution in a list
            def ladder_vector(vec):
              lad_vec=[]
              for v in vec:
                X=[l[0] for l in ladder(v[0])]
                Y=[v[1]+sum([l[1] for l in ladder(v[0])[:j+1]]) for j in range(len(ladder(v[0])))]
                lad_vec+=[(x,y) for x,y in zip(X,Y)]
              return lad_vec
            
            #Runs the project algorithm for each solution in a list
            def project_vector(vec):
              prj_vec=[]
              for v in vec:
                prj_vec+=project(v)
              return prj_vec
              
            #Using Fermat's little theorem to guarantee a list of starting points
            #Length of this list is arbitrary but long enough to find the winning results
            baseline = [((2**(12*n)-1)//13,12*n+1) for n in range(1,6)]
            
            #Alternate between Fermat and ladder to add solutions into the main list
            results=[]
            results+=baseline
            results+=ladder_vector(baseline)
            results+=project_vector(baseline)
            results+=project_vector(ladder_vector(baseline))
            results+=ladder_vector(project_vector(baseline))
            
            #Eliminate duplicates, sort and cut off results beyond solution
            final = [f for f in list(set(results)) if f[1]<51]
            final.sort(key=lambda a: a[1])
            print(*final, sep='\n')
            
            

            Like

            • Tony Brooke-Taylor's avatar

              Tony Brooke-Taylor 5:52 pm on 17 May 2021 Permalink | Reply

              Now replicated Jim’s result: needed to be smarter about looping in the main program.

              Like

              • Brian Gladman's avatar

                Brian Gladman 11:08 pm on 17 May 2021 Permalink | Reply

                Congratulations for sticking with it Tony!

                Like

  • Unknown's avatar

    Jim Randell 9:42 am on 13 May 2021 Permalink | Reply
    Tags:   

    Teaser 2501: [Triangular bipyramid] 

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

    Mark took two identical regular tetahedra and stuck them together to make a “triangular bipyramid” with six triangular faces, five vertices and nine edges. On each edge he wrote a number, choosing them so the sum of the three numbers around any face gave the same “face total”. Furthermore if you chose any vertex and added up the numbers on the three or four edges meeting there, then you got the same “vertex sum” each time. Mark then noticed that if he reversed the order of the three digits in the “face sum” then he got the “vertex sum”.

    What is the sum of the nine numbers?

    This puzzle was originally published with no title.

    [teaser2501]

     
    • Jim Randell's avatar

      Jim Randell 9:42 am on 13 May 2021 Permalink | Reply

      If the face sum is F, and the vertex sum is V, and the total sum of the values of the edges is T.

      Then adding up the sums for all 6 faces counts each edge twice:

      6F = 2T

      Also adding up the sums for all 5 vertices counts each edge twice:

      5V = 2T

      Hence:

      6F = 5V

      So: F < V and F must be divisible by 5. And we know F and V are 3 digit numbers where one is the reverse of the other.

      There is only one possible solution for this scenario, so we can calculate T (the required answer).

      This Python program finds the values of F, V, T, and then also solves the equations to find the values on the edges. It runs in 53ms.

      from enigma import (irange, div, matrix, catch, nreverse, join, printf)
      
      # consider possible 3-digit values for F (must be divisible by 5)
      for F in irange(105, 999, step=10):
        # calculate V
        V = div(6 * F, 5)
        if V > 999: break
        # check V is the reverse of F
        if nreverse(F) != V: continue
        T = div(5 * V, 2)
        printf("F={F} V={V}; T={T}")
      
        # solve the equations to find edge values
        eq = matrix.equation("abcdefghi")
        # face sums = F
        fs = list(eq(fs, F) for fs in ["abe", "acd", "bcf", "dgi", "egh", "fhi"])
        # vertex sums = V
        vs = list(eq(vs, V) for vs in ["abc", "ghi", "adeg", "befh", "cdfi"])
      
        s = catch(matrix.linear, fs + vs)
        if s:
          printf("-> {s}", s=join(s, sep=", ", enc="()"))
      

      Solution: The sum of the numbers on the edges is 1485.

      The 3 edges that form the “equator” have values of 99. The other 6 edges have values of 198 (= 2×99). And 3×99 + 6×198 = 1485.

      Each face has a sum of 495 (= 5×99).

      Each vertex has a sum of 594 (= 6×99).


      It makes sense that there are only 2 different edge values, as there is nothing to distinguish the “north pole” from the “south pole”, nor the rotation of the bipyramid. The only edges we can distinguish are the “equator” edges (q) from the “polar” edges (e).

      Which gives a much simpler set of equations to solve:

      F = 2e + q
      V = 3e = 2e + 2q
      ⇒ q = V − F; e = 2q

      So for: F = 495, V = 594, we get: q = 99, e = 198.

      Like

    • GeoffR's avatar

      GeoffR 3:52 pm on 14 October 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Face, vertex and edge descriptions of tetrahedra.
      % Top tetrahedron has faces F2 and F3 visible, and back face F1 not visible.
      % Bottom tetrahedron has faces F5 and F6 visible, and back face F4 not visible.
      % Top and bottom vertices are V1 and V5 respectively.
      
      % Two tetrahedra join at vertices V2, V3 and V4.
      % Face F2 has vertices V1, V2 and V4, Face F3 has vertices V1, V3 and V4.
      % Face F1 has vertices V1, V2 and V3.
      % e1 = V1-V2, e2 = V1-V4, e3 = V1-V3
      % e4 = V2-V3, e5 = V2-V4, e6 = V3-V4
      % e7 = V2-V5, e8 = V4-V5, e9 = V3-V5
      
      % Edges
      var 34..333:e1; var 34..333:e2; var 34..333:e3;
      var 34..333:e4; var 34..333:e5; var 34..333:e6;
      var 34..333:e7; var 34..333:e8; var 34..333:e9;
      
      % Similar edges for two identical regular tetrahedra
      constraint e1 == e7 /\ e3 == e9 /\ e2 == e8; % sloping edges
      constraint e4 == e5 /\ e5 == e6;  % junction of two  tetrahedra
      
      % Sum of the nine numbers
      var 306..2997: Edge_Sum = e1 + e2 + e3 + e4 + e5 + e6 + e7 + e8 + e9;
      
      % Vertices
      var 102..999:V1; var 102..999:V2; var 102..999:V3;
      var 102..999:V4; var 102..999:V5; 
      
      % Faces
      var 102..999:F1; var 102..999:F2; var 102..999:F3;
      var 102..999:F4; var 102..999:F5; var 102..999:F6;
      
      % Face totals all the same
      constraint F1 == e1 + e2 + e4 /\ F2 == e1 + e2 + e5;
      constraint F3 == e2 + e3 + e6;
      constraint F4 == e4 + e7 + e9;
      constraint F5 == e5 + e7 + e8 /\ F6 == e6 + e8 + e9;
      constraint F1 == F2 /\ F1 == F3 /\ F1 == F4 /\ F1 == F5 /\ F1 == F6;
      
      % Vertex totals all the same
      constraint V1 == e1 + e2 + e3 /\ V2 == e1 + e4 + e5 + e7;
      constraint V3 == e3 + e4 + e6 + e9;
      constraint V4 == e5 + e6 + e8 + e2 /\ V5 == e7 + e8 + e9;
      constraint V1 == V2 /\ V1 == V3 /\ V1 == V4 /\ V1 == V5;
      
      % Three digits of  F1 = reverse three digits of V1
      constraint F1 div 100 == V1 mod 10;
      constraint F1 div 10 mod 10 == V1 div 10 mod 10;
      constraint F1 mod 10 == V1 div 100;
      
      solve satisfy;
      
      output [ "Total edge sum = " ++ show(Edge_Sum) 
      ++ "\n" ++ "Face sum = " ++ show(F1) 
      ++ "\n" ++ "Vertex sum = " ++ show(V1)];
      
      % Total edge sum = 1485
      % Face sum = 495
      % Vertex sum = 594
      % ----------
      % ==========
      
      
      
      

      Interesting to note that Euler’s polyhedron formula (V – E + F = 2) also applies to this triangular bipyramid, as V = 5, E = 9 and F = 6, so 5 – 9 + 6 = 2

      Like

  • Unknown's avatar

    Jim Randell 9:25 am on 11 May 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 763: Old Father Prime 

    From The Sunday Times, 29th February 1976 [link]

    Albert Prime will be joining us again today [*] for the 15th celebration of the anniversary of his birth since he was home on leave during the 1914-18 war.

    On that occasion, his son David’s age plus his brother Bert’s age was equal to his brother Charlie’s age; [and] Bert’s age plus Charlie’s age was equal to Albert’s age.

    All the ages except Albert’s were prime numbers and Albert’s was the cube of David’s. All four were born on 29th February in different years and the ages above are taken by counting how many 29th Februarys they have celebrated (for example, a man born on 29th February 1956 has an age of 5 today [*]).

    In what year was Albert born?

    Note: [*] This puzzle was originally published on 29th February 1976.

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 1 (1980).

    The puzzle text above is taken from the book.

    [teaser763]

     
    • Jim Randell's avatar

      Jim Randell 9:26 am on 11 May 2021 Permalink | Reply

      B, C, D are prime, and A = D³ = B + C, also: B + D = C.

      So, given a value for D we can calculate the other values as follows:

      A = D³
      B = (A − D) / 2
      C = B + D

      And then determine the birth years.

      This Python program runs in 51ms.

      from datetime import date
      from enigma import (primes, cb, div, printf)
      
      # find the year of the k'th 29th February before year y
      def year(k, y):
        while True:
          try:
            d = date(y, 2, 29)
            if k == 0: return y
            k -= 1
          except ValueError:
            pass
          y -= 1
      
      # choose a value for D
      for D in primes.between(2, 100):
        # calculate A
        A = cb(D)
        if A > 100: break
        # calculate B
        B = div(A - D, 2)
        if B not in primes: continue
        # calculate C
        C = B + D
        if C not in primes: continue
        printf("[A={A} B={B} C={C} D={D}]")
      
        # calculate birth years
        y = year(15, 1976)
        assert 1914 <= y <= 1918
        (a, b, c, d) = (year(x, y) for x in (A, B, C, D))
        printf("born: A={a} B={b} C={c} D={d} [y={y}]")
      

      Solution: Albert was born in 1880.

      Charlie was born in 1892. Bert was born in 1904. David was born in 1908.

      Note that 1900 was not a leap year.

      In 1916, Albert celebrated his 8th anniversary leap day (he was 36). Charlie celebrated his 5th anniversary leap day (he was 24). Bert celebrated his 3rd anniversary leap day (he was 12). David celebrated his 2nd anniversary leap day (he was 8).


      The fact: B + D = C and these values are all primes, implies that one of them must be 2. D is the obvious candidate, and the other values follow immediately. (D has to be a prime, whose cube is also relatively small, so there are very few options for D anyway).

      The only remaining wrinkle is remembering to skip the year 1900 when counting 8 leap years back from 1916.

      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