Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 2:09 pm on 24 August 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 922: Additional letters 

    From The Sunday Times, 23rd March 1980 [link]

    It is true, of course, that there are a lot of letters in this puzzle, but in spite of that I thought that for once Uncle Bungle was going to write it out correctly.

    In fact there was no mistake until the third and last line across but, in that line one of the letters, I’m afraid, was incorrect.

    This is another addition sum with letters substituted for digits. Each letter consistently stands for the same digit whenever it appears, and different letters stand for different digits — or at least the should do — and they do, but for the mistake in the last line across:

    What is the correct numerical 10-figure answer to the addition sum?

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

    [teaser922]

     
    • Jim Randell's avatar

      Jim Randell 2:09 pm on 24 August 2023 Permalink | Reply

      We can use [[ bungled_sum() ]] solver (as seen in Puzzle 56 etc.) for this puzzle.

      The following Python command line executes in 146ms.

      Run: [ @replit ]

      >>> from bungled_sum import bungled_sum
      >>> bungled_sum(['YTBBEDMKD', 'YHDBTYYDD', 'EDYTERTPTY'], [2])
      
                                      T
      YTBBEDMKD + YHDBTYYDD = EDYTERTPHY / @[2,8] T -> H
      695513243 + 673596633 = 1369109876 / B=5 D=3 E=1 H=7 K=4 M=2 P=8 R=0 T=9 Y=6
      

      Solution: The answer to the addition sum is 1369109876.

      The complete sum is:

      695513243 + 673596633 = 1369109876

      which would have been correctly posed as:

      YTBBEDMKD + YHDBTYYDD = EDYTERTPHY

      i.e. the penultimate (tens) digit of the result should have been H (not T).

      Like

  • Unknown's avatar

    Jim Randell 8:46 am on 22 August 2023 Permalink | Reply
    Tags:   

    Teaser 2635: Three sisters 

    From The Sunday Times, 24th March 2013 [link] [link]

    In Shakespeare’s less well-known prequel, King Lear shared all of his estate amongst his three daughters, each daughter getting a fraction of the estate. The three fractions, each in their simplest form, had numerators less than 100 and had the same denominators. Cordelia got the least, with Regan getting more, and Goneril the most (but her share was less than three times Cordelia’s). Each of the three fractions gave a decimal which recurred after three places (as in 0.abcabc…) and each digit from 1 to 9 occurred in one of the three decimals.

    What were the three fractions?

    [teaser2635]

     
    • Jim Randell's avatar

      Jim Randell 8:46 am on 22 August 2023 Permalink | Reply

      The recurring decimal 0.(abc)… is a representation of the fraction abc/999.

      But the three fractions we are dealing with can all be reduced to fractions with a 2-digit numerator, and a common denominator.

      So the common denominator must be a divisor of 999.

      This Python program runs in 54ms. (Internal runtime is 2ms)

      Run: [ @replit ]

      from enigma import (divisors, decompose, gcd, recurring, join, printf)
      
      # return viable repetends
      def repetend(n, d):
        r = recurring(n, d)
        return (None if r.nr else r.rr)
      
      # consider possible denominator values 3 < d < 100
      for d in divisors(999):
        if d < 4: continue
        if d > 297: break
      
        # calculate ascending splits of the denominator
        for (C, R, G) in decompose(d, 3, increasing=1, sep=1, min_v=1, max_v=99):
          # G's share is less than 3 times C's
          if not (G < 3 * C): continue
          # check fractions are in lowest terms
          if any(gcd(n, d) != 1 for n in (C, R, G)): continue
      
          # find the repetends
          reps = list(repetend(n, d) for n in (C, R, G))
          if None in reps: continue
          # check the digits used
          ds = join(reps)
          if '0' in ds or len(set(ds)) < 9: continue
      
          # output solution
          (rC, rR, rG) = reps
          printf("C = {C}/{d} = 0.({rC})...; R = {R}/{d} = 0.({rR})...; G = {G}/{d} = 0.({rG})...")
      

      Solution: The fractions are: Cordelia = 6/37, Reagan = 14/37, Goneril = 17/37.

      As decimals:

      C = 0.(162)…
      R = 0.(378)…
      G = 0.(459)…

      Like

  • Unknown's avatar

    Jim Randell 5:10 pm on 20 August 2023 Permalink | Reply
    Tags: by: Robert Eastaway   

    Brain-Teaser 917: Run ’em up 

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

    Last summer, for a pleasant holiday pastime with mathematical connections, I took up the job of operating our local cricket scoreboard at matches.

    This scoreboard is the envy of all the other teams in the league. It shows:

    – the two batsmen identified by their numbers in the batting order, with their individual totals;
    – the score (i.e. the number of runs and the wickets fallen);
    – the number of overs bowled;
    – the number of extras;
    – the score of the last man out.

    During one match, while the two batsmen were in full flight, our team declared their innings closed at the end of an over, with wickets to spare. Exhausted after a busy day, I examined the board and was amazed to see that all of the numbers recorded on it used only two different digits between them.

    I noted that the total was the only three-figure number; that there were four two-figure numbers; and that two single-figure numbers appeared twice. I also observed that the score of the last man out was a factor of the total, and the division of the latter by the former still resulted in a single figure number on the board.

    I was pleased to see that all the batsmen on the board reached double figures.

    What was the final score, i.e. how many runs for how many wickets?

    How many extras were there?

    [In cricket, the batsmen are numbered from 1 upwards as they come in to bat. The world record is 77 runs in one over. The match itself was perfectly normal — no-one retired hurt etc. Ed.]

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

    [teaser917]

     
    • Jim Randell's avatar

      Jim Randell 5:11 pm on 20 August 2023 Permalink | Reply

      We start with batsmen 1 and 2 playing, and at this point there would be 0 wickets taken (and no last man out).

      When one of them is out, batsman 3 comes in to play, and there is 1 wicket taken. And when another batsman is dismissed, there are 2 wickets taken, and batsman 4 comes in to play. And so on.

      So, if w is the number of wickets taken, then the most recent of the current batsmen must be number (w + 2).

      This Python program considers possible numbers for the current batsman, and uses these along with the number of wickets taken to find the 2 available digits. It then looks for possible 3-digit numbers using those digits that can be divided by one of digits to give a 2-digit number composed of the available digits (and this is the score of the last man out).

      This is enough to get us the score in the match, and deduce the number of extras.

      This Python program runs in 51ms. (Internal runtime is 457µs).

      Run: [ @replit ]

      from enigma import (irange, subsets, nsplit, union, nconcat, div, printf)
      
      # construct <k> digit numbers from digits <ds>
      construct = lambda ds, k: subsets(ds, size=k, select="M", fn=nconcat)
      
      # consider the numbers of the batsmen on the board
      for (a, b) in subsets(irange(1, 11), size=2):
      
        # and number of wickets fallen
        w = b - 2
        if w < 1: continue
      
        # calculate digits used
        ds = union(nsplit(x) for x in (a, b, w))
        if len(ds) > 2: continue
        # suppose both available digits are determined
        assert len(ds) == 2
      
        # choose a 3-digit total score
        for t in construct(ds, 3):
          # divisible by one of the digits
          for d in ds:
            # to give the score of the last man out
            z = div(t, d)
            # which is a 2 digit number, composed of the required digits
            if z is None or z < 10 or z > 99: continue
            if not ds.issuperset(nsplit(z)): continue
      
            # output solution
            printf("score = {t} for {w} [a={a} b={b} -> w={w} ds={ds} t={t} z={z}]")
      

      Solution: (i) The score in the match was: 688 for 6.

      We have:

      batsman = 6, runs = {66, 68, 86, 88}
      batsman = 8, runs = {66, 68, 86, 88}
      score = 688 for 6
      overs = __
      extras = __
      last man out = 86

      There is a single digit value of 8 to be filled out in one of the gaps (and the other is a 2-digit number (which must be 66, 68, 86, 88)).

      The most likely scenario is that the 688 runs have been scored off more than 8 overs (= 64 balls), so the number of extras is 8.

      (Also, we wouldn’t be able to determine what the number of extras was if it was the missing 2-digit number).

      Solution: (ii) There were 8 extras.


      In the book The Sunday Times Book of Brainteasers (1994), the additional information that: “The world record is 77 runs in one over” is given.

      Presuming the record was not broken during this match, that would give a maximum of 616 runs in 8 overs, so there must be more than 8 overs. (But as noted above, if the number of extras were a 2 digit number, we could not determine it).

      Like

  • Unknown's avatar

    Jim Randell 4:29 pm on 18 August 2023 Permalink | Reply
    Tags: ,   

    Teaser 3178: Drying boards 

    From The Sunday Times, 20th August 2023 [link] [link]

    Chef Ignacio liked to prop his two identical thin rectangular chopping boards against the shelf at the end of his counter to dry. He placed the top of the first one flush with the shelf corner and rested the second on the first, as shown in the diagram. To aid drying, he positioned the second to maximise the air volume in the bounded region below it. The length of each board is an even number of cm (less than 26cm). The distance between the bases of the two boards was a whole number of mm.

    What is this distance?

    It seems that the setter intends that the height of the shelf above the counter is an exact (but not specified) number of mm. Without this requirement we can find multiple potential solutions.

    [teaser3178]

     
    • Jim Randell's avatar

      Jim Randell 5:30 pm on 18 August 2023 Permalink | Reply

      I am assuming the distance between the bases of the boards is the separation between the edges that touch the counter top.

      At the moment I don’t understand how we can work out a unique answer without knowing more information, say, the height of the shelf above the surface of the counter, or the angle of one of the boards.

      Solution: [See my comment below]

      Like

      • Jim Randell's avatar

        Jim Randell 8:26 am on 19 August 2023 Permalink | Reply

        I have now found a single solution using the additional assumption that the height of the shelf above the counter is an exact whole number of millimetres. It seems it is likely this is what the setter intended.

        If the first board makes an angle θ with the surface (0° < θ < 90°), then the maximum cross-sectional area under the second board is achieved when it makes an angle θ/2 with the surface (and the bounded area is an isosceles triangle).

        With the length of the board and the height of the shelf we can calculate the angle θ, and hence θ/2. Then cos(θ/2) is the ratio of half the board length to the required separation.

        I used the half-angle formula:

        \cos \left( \frac{\theta}{2} \right)\; =\; \sqrt{\frac{1\; +\; \cos \left( \theta \right)}{2}}

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

        Run: [ @replit ]

        from enigma import (Rational, irange, is_square_q, div, as_int, printf)
        
        Q = Rational()
        
        # solve for a board of length x (mm)
        for x in irange(20, 240, step=20):
        
          # and a shelf of height h (mm)
          for h in irange(1, x - 1):
        
            # separation between the boards on the counter = y (mm)
            d = is_square_q(x * x - h * h)
            if d is None: continue
            q = is_square_q(Q(d + x, 2 * x))
            if q is None: continue
            y = Q(x, 2 * q)
            # check for integer value
            y = as_int(y, include="+", default=None)
            if y is None: continue
        
            # output solution
            printf("x={x} h={h} -> y={y}")
        

        Solution: The boards are 125 mm apart.

        The boards are 200 mm in length (x = 200), and the shelf is height 192 mm above the counter top (h = 192).

        So the first board makes a (56, 192, 200) = 8× (7, 24, 25) right-angled triangle.

        And we have:

        cos(θ) = 7/25

        θ ≈ 73.74°

        And we calculate cos(θ/2) as:

        cos(θ/2) = √((1 + cos(θ))/2)
        = √((1 + 7/25)/2)
        = √(16/25)
        = 4/5

        θ/2 ≈ 36.87°

        And the required separation is:

        y = (x/2) / cos(θ/2)
        y = 100 / (4/5)
        y = 125


        If the height of the shelf h is unconstrained, then we can find an appropriate value to give any (reasonable) answer we choose.

        For a board of length x and a required separation distance y, we calculate the angle of θ (between 0° and 90°) as:

        cos(θ/2) = x/2y

        Then the required height h for this scenario is given by:

        h = x sin(θ)

        For example:

        x = 240, y = 140

        cos(θ/2) = 6/7
        θ ≈ 62.01°
        h ≈ 211.92 mm

        Like

        • Frits's avatar

          Frits 12:29 pm on 19 August 2023 Permalink | Reply

          I had to make two assumptions, not one, to get to the same answer.

          Luckily my initial derived rule for maximal air volume remains true.

           
          from enigma import Rational
          
          Q = Rational()
          
          M = 26 # length of each board in cm is smaller than M
          
          # air volume in the bounded region is maximal if line perpendicular to
          # the 2nd board splits the 2nd board in half (isoceles triangle)
          
          # board length in mm
          for b in range(20, 10 * M, 20):
            # assume shelf height is a whole number of mm
            for h in range(1, b):
              # assume the distance from corner to 1st board touching 
              # the counter <a> is a whole number of mm
              a = (b**2 - h**2)**(1/2)
              if not (a == (a := int(a))): continue
              # using cosine half-angle formula the following must be true
              d2 = Q(b**2 + a * b, 2 + 4 * Q(a, b) + Q(2 * a**2, b**2))
              if d2.denominator != 1: continue
              
              # distance <d> must be a whole number of mm
              if (d := d2.numerator**(1/2)) != int(d): continue
              print("answer:", int(d), "mm")
          

          Like

          • Frits's avatar

            Frits 10:12 pm on 19 August 2023 Permalink | Reply

            line 17 is not really necessary.

            Like

        • Jim Randell's avatar

          Jim Randell 1:04 pm on 19 August 2023 Permalink | Reply

          For a shorter/faster program we can use [[ pythagorean_triples() ]] from the enigma.py library (and a bit of analysis).

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

          Run: [ @replit ]

          from enigma import (pythagorean_triples, div, is_square, printf)
          
          # generate pythagorean triples for the first board (in mm)
          for (a, b, x) in pythagorean_triples(240):
            if x % 20 != 0: continue
            for (d, h) in [(a, b), (b, a)]:
              # calculate the separation of the boards (in mm)
              y = is_square(div(x**3, 2 * (d + x)))
              if y is None: continue
              # output solution (in mm)
              printf("x={x} h={h} -> y={y}")
          

          Analysis:

          Using the identity:

          2 cos²(θ/2) = 1 + cos(θ)

          we have:

          cos(θ/2) = x / 2y
          cos(θ) = d / x

          hence:

          x²/2y² = 1 + d/x

          y² = x³ / 2(d + x)

          And x and y are integers, hence d is also an integer, so (d, h, x) is a Pythagorean triple.

          Liked by 1 person

  • Unknown's avatar

    Jim Randell 8:33 am on 17 August 2023 Permalink | Reply
    Tags: ,   

    Teaser 2634: Good company 

    From The Sunday Times, 17th March 2013 [link] [link]

    Each year at this time Pat’s company gives its staff a bonus to help them “drown their shamrocks” on the Irish national holiday. A total of £500 is shared out amongst all six employees (five men and the manager Kate) whose numbers of years of service consist of six consecutive numbers. Each man gets the same whole number of pounds for each year of his service and Kate gets a higher whole number of pounds for each year of her service. This means that, although Pat does not get the lowest bonus, he does get £20 less than Kate — even though he has served the company for a year longer than her.

    How much does Pat get?

    [teaser2634]

     
    • Jim Randell's avatar

      Jim Randell 8:33 am on 17 August 2023 Permalink | Reply

      This Python program runs in 51ms. (Internal runtime is 421µs).

      Run: [ @replit ]

      from enigma import (irange, tuples, update, printf)
      
      # consider possible consecutive years of service (up to 50)
      for ys in tuples(irange(1, 50), 6):
        # total years service
        t = sum(ys)
        # choose a basic bonus amount
        for b in irange(1, 23):
          # remaining bonus
          r = 500 - t * b
          if not r > 0: break
          # make the list of basic bonuses
          bs = list(y * b for y in ys)
          # kate (not first or last) gets the remaining bonus
          for i in (1, 2, 3, 4):
            y = ys[i]
            if r % y != 0: continue
            # kate's bonus is 20 more than pat's
            (k, p) = (bs[i] + r, bs[i + 1])
            if k == p + 20:
              # output solution
              printf("pat = {p}, kate = {k} [years = {ys}, bonus = {bs}]", bs=update(bs, [(i, k)]))
      

      Solution: Pat’s bonus was £ 108.

      The six employees have worked at the company for 4, 5, 6, 7, 8, 9 years. With Kate having worked 8 years and Pat 9 years.

      Each of the non-managers receives a £12/year bonus:

      4 → £48
      5 → £60
      6 → £72
      7 → £84
      9 → £108 (Pat)

      Kate receives a £16/year bonus:

      8 → £128

      Like

    • Frits's avatar

      Frits 1:08 pm on 17 August 2023 Permalink | Reply

      # basic bonus amount: B,          we have B < 500 / (6 + 15)
      for B in range(1, 24):
        # years of service Y, ..., Y+5, we have (6 * Y + 15) * B < 500
        for Y in range(1, int((500 / B - 15) / 6) + 1):
          # index Kate in Y, ..., Y+5: I   (if I = 0 then Pat gets the lowest bonus)
          for I in range(1, 5):
            # Pat does get 20 pounds less than Kate,  K.(Y + I) - B.(Y + I + 1) = 20
            K, r = divmod(20 + B * (Y + I + 1), Y + I)
            if r: continue
            # a total of 500 pounds is shared out amongst all six employees
            if sum(B * (Y + i) if i != I else K * (Y + I) for i in range(6)) != 500:
              continue
            print(f"answer: {(Y + I + 1) * B} pounds")   
      

      or

      # basic bonus amount: B,          we have B < 500 / (6 + 15)
      for B in range(1, 24):
        # years of service Y, ..., Y+5, we have (6 * Y + 15) * B < 500
        for Y in range(1, int((500 / B - 15) / 6) + 1):
          # index Kate in Y, ..., Y+5: I   (if I = 0 then Pat gets the lowest bonus)
          sofar = sum(B * (Y + i) for i in range(6))
         
          # (Y + I).(K - B) = 500 - sofar,  I = index Kate in list years of service 
          for K, I in [(B + d[0], i) for i in range(1, 5) 
                       if not (d := divmod(500 - sofar, Y + i))[1]]:
            # Pat does get 20 pounds less than Kate
            if K * (Y + I) - B * (Y + I + 1) != 20: continue
            print(f"answer: {(Y + I + 1) * B} pounds")
      

      Like

  • Unknown's avatar

    Jim Randell 9:22 am on 15 August 2023 Permalink | Reply
    Tags:   

    Brainteaser 1644: Piecework 

    From The Sunday Times, 13th March 1994 [link]

    On a piece of card draw a grid of 1 inch squares as shown. Then cut along some of the lines to cut out various pieces. Each piece must consist of five squares, no two pieces can be the same shape, even if turned over or turned round, and all possible shapes of five squares must be included. Then with some of the pieces it is possible to make various shapes in jigsaw fashion.

    I asked my son to make a rectangle in which the longer side was a whole number multiple of the shorter side. His first attempt:

    But then he broke that up and constructed another of different size and with an even number of unused pieces.

    What were the dimensions of this new rectangle?

    [teaser1644]

     
    • Jim Randell's avatar

      Jim Randell 9:23 am on 15 August 2023 Permalink | Reply

      (See also: Enigma 611).

      There are 12 different pentominoes (if we allow rotation and reflection), and I have written polyominoes.py for dealing with them (and other polyominoes).

      This Python program starts with the 12 possible pentomino shapes, and then removes an even number of them and tries to fit them into possible rectangles.

      It runs in 2.2s.

      Run: [ @replit ]

      from enigma import (irange, subsets, divisors_pairs, seq2str, printf)
      import polyominoes
      
      # possible pentomino shapes
      tiles = polyominoes.shapes("I5 U5 T5 V5 X5 W5 L5 Y5 P5 N5 F5 Z5".split(), as_map=1)
      n = len(tiles.keys())
      
      # record grid dimensions
      rs = set()
      # an even number of tiles are unused
      for k in irange(2, n - 1, step=2):
        # choose (n - k) tiles
        for ks in subsets(tiles.keys(), size=n - k):
          ts = list(tiles[x] for x in ks)
          # consider possible rectangles for these pieces
          for (x, y) in divisors_pairs(5 * len(ts)):
            if y % x != 0: continue  # width is a multiple of height
            if (x, y) == (2, 10): continue  # exclude 2 x 10
            # can we fit the tiles into the grid?
            for g in polyominoes.fit(ts, y, x):
              rs.add((x, y))
              # output 1 example
              printf("{x} x {y} grid -> {ks} [{k} unused]", ks=seq2str(ks, sep=" "))
              polyominoes.output_grid(g)
              break
      
      # output solution
      printf("grid = {rs}", rs=seq2str(rs, sep=" ", enc=""))
      

      Solution: The rectangle was 5 in × 10 in.

      There are many possible pairs of shapes which can remain unused, and many possible ways to fit the remaining shapes into the grid. (The program prints one example packing for each set of shapes and grid size).

      Here is one:

      I have a wooden set of pentominoes, here is the same packing (slightly exploded, so you can see the individual pentominoes), along with the unused pieces:

      Like

  • Unknown's avatar

    Jim Randell 3:13 pm on 11 August 2023 Permalink | Reply
    Tags:   

    Teaser 3177: Prime birthday card 

    From The Sunday Times, 13th August 2023 [link] [link]

    On her 11th birthday, Ann’s older brother Ben gave her a card on which he had drawn a 5×5 square grid with a different prime number in each of the cells. Every 5-cell row, column and diagonal had the same sum and, noting that 1 is not a prime, he used primes that made this sum as small as possible.

    The centre cell contained Ann’s age. All but one of the primes containing a digit 7 were on the two diagonals, the largest of these being in the bottom right corner. Two cells in the far right column contained a digit 5 as did two cells of the 4th row down. Four of the cells in the middle row contained a digit 3, and the largest prime on the grid was in the same column as two single digit primes.

    What are the five prime numbers on the top row of the card?

    [teaser3177]

     
    • Jim Randell's avatar

      Jim Randell 5:42 pm on 11 August 2023 Permalink | Reply

      We can exclude 2 from the list of primes, as we need the sum of all 12 magic lines to have the same parity (as they are all equal to the magic constant).

      The magic constant of the square is the sum of the numbers used, divided by 5, so we can look for candidate sets of primes that give the smallest magic constant.

      There are two potential sets of 25 primes that would give a magic constant of 233:

      (1) primes from 3 to 103, excluding 97
      (2) primes from 3 to 107, excluding 101, 103

      And it is possible to construct a Magic Square using either of these sets, so one of these sets must be the set that Ben used.

      (I used a separate program to find the minimal sets, see: [@replit]).

      In the completed grid the centre cell is occupied by 11, so there are 8 remaining cells on the diagonals.

      The first of these sets has 8 numbers that include the digit 7. So is possible for 7 of the remaining diagonal cells to use 7 of these numbers, and the remaining number is in one of the non-diagonal cells.

      The second of these sets has 10 numbers that include the digit 7. So, even if all 8 remaining diagonal cells are filled with one of these numbers there would be 2 numbers left over, so this set is not viable.

      So the set of primes used by Ben must be set (1).

      We can now look for ways to fill out the grid as specified by the remaining requirements.

      I used a MiniZinc program to generate candidate magic squares that fulfil some (most) of the requirements, and then perform additional checks in Python to find arrangements that satisfy all the requirements. I used the minizinc.py library for this.

      This program performs an exhaustive search in 702ms.

      from enigma import (primes, ediv, nsplit, diff, chunk, unzip, implies, icount, lt, join, int2base, printf)
      from minizinc import (MiniZinc, var)
       
      # numbers to use (= set (1))
      numbers = diff(primes.between(3, 103), {97})
      # the magic constant
      k = ediv(sum(numbers), 5)
       
      # find numbers that include the digit 3, 5, 7
      (n3s, n5s, n7s) = (set(), set(), set())
      for n in numbers:
        ds = nsplit(n)
        if 3 in ds: n3s.add(n)
        if 5 in ds: n5s.add(n)
        if 7 in ds: n7s.add(n)
       
      # format a sequence for MiniZinc
      fseq = lambda xs, sep=", ", enc="{}": join(xs, sep=sep, enc=enc)
       
      # construct the model:
      #
      #  A B C D E
      #  F G H I J
      #  K L M N P
      #  Q R S T U
      #  V W X Y Z
      #
      vs = "ABCDEFGHIJKLMNPQRSTUVWXYZ"
      model = f"""
       
      include "globals.mzn";
       
      % possible numbers
      set of int: Number = {fseq(numbers)};
       
      % decision variables for the cells of the square
      {var("Number", vs)};
       
      % general constraints:
       
      % each cell is different
      constraint all_different({fseq(vs, enc="[]")});
       
      % magic lines have the same sum (= k)
      constraint all_equal([{k},
        A+B+C+D+E, F+G+H+I+J, K+L+M+N+P, Q+R+S+T+U, V+W+X+Y+Z,  % rows
        A+F+K+Q+V, B+G+L+R+W, C+H+M+S+X, D+I+N+T+Y, E+J+P+U+Z,  % cols
        A+G+M+T+Z, E+I+M+R+V  % diags
      ]);
       
      % some specific constraints:
       
      % centre cell is 11
      constraint M = 11;
       
      % and the other 4 cells in the middle row (K, L, N, P) contain a digit 3
      constraint {{K, L, N, P}} subset {fseq(n3s)};
       
      % 2 cells in the far right column contain a digit 5
      constraint card({{E, J, P, U, Z}} intersect {fseq(n5s)}) = 2;
      % as did 2 cells in the 4th row down
      constraint card({{Q, R, S, T, U}} intersect {fseq(n5s)}) = 2;
       
      % all but one of the primes containing a '7' are on the diagonals
      constraint card({fseq(n7s)} diff {{A, E, G, I, R, T, V, Z}}) = 1;
      % and the largest of these is Z
      constraint max({fseq(n7s)} intersect {{A, E, G, I, R, T, V, Z}}) = Z;
       
      solve satisfy;
      """
       
      # load the model
      p = MiniZinc(model, solver="minizinc --solver gecode --all", verbose=0)
       
      # and solve it
      for s in p.solve():
        ns = tuple(s[k] for k in vs)
        rows = list(chunk(ns, 5))
        cols = list(unzip(rows))
        (A, B, C, D, E, F, G, H, I, J, K, L, M, N, P, Q, R, S, T, U, V, W, X, Y, Z) = ns
       
        # centre cell is 11
        if not (M == 11): continue
       
        # and the other 4 cells in the middle row contain a digit 3
        if not n3s.issuperset([K, L, N, P]): continue
       
        # 2 cells in the far right column have a digit 5
        if not (len(n5s.intersection([E, J, P, U, Z])) == 2): continue
        # also 2 of the cells in the 4th row down
        if not (len(n5s.intersection([Q, R, S, T, U])) == 2): continue
       
        # all but one of the numbers containing the digit 7 are on the diagonals
        x7s = diff(n7s, {A, E, G, I, R, T, V, Z})
        if not (len(x7s) == 1): continue
        # and the largest of these numbers is Z
        if not (max(diff(n7s, x7s)) == Z): continue
       
        # the largest prime on the grid is in the same column as 2 single digit primes
        maxn = max(ns)
        if not all(implies(maxn in col, icount(col, lt(10)) == 2) for col in cols): continue
       
        # output the grid
        fmt = lambda n: int2base(n, width=3, pad=' ')
        for r in rows:
          printf("{r}", r=join(r, sep=" ", enc="[]", fn=fmt))
        printf()
      

      Solution: The numbers on the top row of the card are: 17, 7, 101, 41, 67.

      The complete grid is:

      Like

      • Frits's avatar

        Frits 9:11 am on 12 August 2023 Permalink | Reply

        Thanks. I wondered how you would do the y contains ‘x’ thing in MiniZinc without string support.

        Like

        • Jim Randell's avatar

          Jim Randell 10:55 am on 12 August 2023 Permalink | Reply

          Originally I had fewer constraints in the MiniZinc model (which is why they are all tested in Python), but it ran faster if the constraints were implemented in the model (apart from the “largest prime” constraint, which was messier to do in MiniZinc, and also slowed down the overall execution).

          As it is the model now only generates a single candidate, so the cost of verifying all the requirements in Python is negligible. (Although the repeated tests could be removed to give a shorter program).

          But I used a restricted version of the model to find the minimal sets of primes that form a Magic Square.

          Like

    • Frits's avatar

      Frits 10:09 am on 13 August 2023 Permalink | Reply

      from itertools import combinations, permutations
      
      # decompose <t> into <k> numbers from <ns>
      def decompose(t, k, ns, s=tuple()):
        if k == 1:
          if t in ns:
            yield s + (t, )
        else:
          for n in ns:
            if t - n < minPr: continue
            yield from decompose(t - n, k - 1, ns.difference([n]), s + (n ,))
      
      # decompose <t> into <k> increasing odd prime numbers from <ns>
      def decompose_inc(t, k, ns, s=[]):
        if k == 1:
          if t in ns:
            yield set(s + [t])
        else:
          for i, n in enumerate(ns):
            if t < k * n + k * (k - 1): break
            yield from decompose_inc(t - n, k - 1, ns[i + 1:], s + [n])
      
      # exclude entries that occur in <s>
      ps = lambda s: {p for p in Pr if p not in s}
      
      # 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
      
      def solve():
        sol = 0
        # all but one of the primes containing a digit 7 were on two diagonals
        # choose <no7s - 1> 7's for the diagonals
        for c7 in combinations(sevens, no7s - 1):
          rest = 2 * s - 2 * M - sum(c7)             # rest sum for other diagonals
          Y = c7[-1]     # largest of diagonal 7's being in the bottom right corner
      
          match(no7s):
            case 9: oth = ((), )       # tuple of an empty tuple
            case 8: oth = ((rest, ), ) if rest in Pr and '7' not in str(rest) \
                                       else tuple()
            case other:
              oth = tuple(decompose(rest, 9 - no7s,
                          {p for p in Pr if '7' not in str(p)}))
              if not oth: continue
      
          # for all combinations of diagonal cells without '7' (or center)
          for o in oth:
            # permute unknown diagonal values
            for A, G, S, E, I, Q, U in permutations(c7[:-1] + o):
              # only check one diagonal sum
              if A + G + M + S + Y != s: continue
              s1 = {A, G, M, S, Y, E, I, Q, U}
              # four of the cells in the middle row contained a digit 3
              for K, L, N, O in decompose(s - M, 4, {p for p in Pr.difference(s1)
                                                     if '3' in str(p)}):
                # determining J, T followed by determining P, R is a lot faster
                # than determining P, R, T and calculating J  (credit: B. Gladman)
      
                # 5th column
                for J, T in decompose(s - E - O - Y, 2, ps(s2 := s1 | {K, L, N, O})):
                  # two cells in the far right column contained a digit 5
                  if sum('5' in str(x) for x in [E, J, O, T, Y]) != 2: continue
                  # 4th row
                  for P, R in decompose(s - Q - S - T, 2, ps(s3 := s2 | {J, T})):
                    # two cells in the 4th row down contained a digit 5
                    if sum('5' in str(x) for x in [P, Q, R, S, T]) != 2: continue
      
                    F = s - (A + K + P + U)                # 1st column
                    if F in s3 | {P, R} or F not in Pr: continue
                    H = s - (F + G + I + J)                # 2nd row
                    if H in (s4 := s3 | {P, R, F}) or H not in Pr: continue
      
                    # 2nd column
                    for B, V in decompose(s - G - L - Q, 2, ps(s5 := s4 | {H})):
                      # 3rd column
                      for C, W in decompose(s - H - M - R, 2, ps(s6 := s5 | {B, V})):
                        D = s - (A + B + C + E)            # 1st row
                        X = s - (U + V + W + Y)            # 5th row
                        if any(x not in Pr for x in [D, X]): continue
                        if len(s6 | {C, W, D, X}) != 25: continue
      
                        mat = [(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)]
                        # transpose matrix
                        tmat = list(zip(*mat))
      
                        # largest prime on the grid was in the same column as two
                        # single digit primes
                        if sum(max(Pr) in x for x in tmat \
                                   if sum(y < 10 for y in x) >= 2) != 1: continue
      
                        # double check sums for rows and columns
                        if any(sum(x) != s for m in [mat, tmat] for x in m): continue
      
                        print()
                        for m in mat:
                          print(''.join([f"{str(x):>4}" for x in m]))
      
                        print("\nanswer:", mat[0])
                        # break if we have finished processing this magical sum <s>
                        sol = 1
        return sol
      
      # list of prime numbers up to 997
      primes = [3, 5, 7]
      primes += [x for x in range(11, 100, 2) if all(x % p for p in primes)]
      primes += [x for x in range(101, 1000, 2) if all(x % p for p in primes)]
      total = sum(primes[:25])
      
      # find first entry higher than total for which total = s * 5 for an odd s
      while total % 5 or total % 2 == 0:
        total += 1
      
      M = 11  # the centre cell contained Ann's age
      primes = [p for p in primes if p != M]
      
      sol = 0
      # potentially process all odd magical sums <s> up to a high number
      for s in range(total // 5, 10000, 2):
        # choose 24 prime numbers that (together with <M>) sum up to 5 * s
        for Pr in decompose_inc(5 * s - M, 24, primes):
          sevens = [p for p in Pr if '7' in str(p)]
          # 9 diagonal cells from which M is 11
          if (no7s := len(sevens)) > 9: continue
          minPr = min(Pr)
          # solve the puzzle
          sol += solve()
      
        if sol: break   
      

      Like

    • Frits's avatar

      Frits 10:10 am on 13 August 2023 Permalink | Reply

      from enigma import SubstitutedExpression
      
      # decompose <t> into <k> increasing numbers from <ns>
      def decompose_inc(t, k, ns, s=[]):
        if k == 1:
          if t in ns:
            yield set(s + [t])
        else:
          for i, n in enumerate(ns):
            if t < k * n + 2: break
            yield from decompose_inc(t - n, k - 1, ns[i + 1:], s + [n])      
      
      '''
        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
      '''
      
      # list of prime numbers
      primes = [3, 5, 7]
      primes += [x for x in range(11, 100, 2) if all(x % p for p in primes)]
      primes += [x for x in range(101, 1000, 2) if all(x % p for p in primes)]
      
      total = sum(primes[:25])
      # find first entry higher than total for which total = s * 5 for an odd s
      while total % 5 or total % 2 == 0:
        total += 1
      
      sol = 0
      # potentially process all odd magical sums <s> up to a high number
      for s in range(total // 5, 10000, 2):
        for Pr in decompose_inc(5 * s, 25, primes):
          sevens = [p for p in Pr if '7' in str(p)]
          # 9 diagonal cells from which M is 11 
          if (no7s := len(sevens)) > 9: continue
          
          exprs = []
         
          # diagonal  
          exprs += ["A + G + M + S + Y == " + str(s)] 
          exprs += ["sum('7' in str(x) for x in [A, G, M, S]) >= 2"]
          exprs += ["max(x for x in [A, G, M, S, Y] if '7' in str(x)) == Y"]
          
          exprs += ["E + I + M + Q + U == " + str(s)]
          # check for <no7s - 1> 7's on the diagonals
          exprs += ["sum('7' in str(x) for x in [A, G, M, S, Y, E, I, Q, U]) == " +\
                    str(no7s - 1)]
          exprs += ["max(x for x in [A, G, M, S, Y, E, I, Q, U] \
                        if '7' in str(x)) == Y"]
      
          # 3rd row
          exprs += ["K + L + M + N + O == " + str(s)] 
          # four of the cells in the middle row contained a digit 3
          exprs += ["sum('3' in str(x) for x in [K, L, M, N, O]) == 4"]
          
          # 5th column
          exprs += ["E + J + O + T + Y == " + str(s)]
          # two cells in the far right column contained a digit 5
          exprs += ["sum('5' in str(x) for x in [E, J, O, T, Y]) == 2"]
      
          # 4th row
          exprs += ["P + Q + S + T + R == " + str(s)] 
          # two cells in the 4th row down contained a digit 5
          exprs += ["sum('5' in str(x) for x in [P, Q, R, S, T]) == 2"]
      
          # remaining horizontal
          exprs += ["A + B + C + D + E == " + str(s)] 
          exprs += ["F + G + H + I + J == " + str(s)] 
          exprs += ["U + V + W + X + Y == " + str(s)] 
      
          # remaining vertical
          exprs += ["A + F + K + U + P == " + str(s)] 
          exprs += ["B + G + L + Q + V == " + str(s)] 
          exprs += ["C + H + M + R + W == " + str(s)] 
          exprs += ["D + I + N + S + X == " + str(s)] 
      
          # largest prime on the grid was in the same column 
          # as two single digit primes
          exprs += ["sum(" + str(max(Pr)) + " in x for x in [(A, F, K, P, U), \
                    (B, G, L, Q, V), (C, H, M, R, W), (D, I, N, S, X), \
                    (E, J, O, T, Y)] if sum(y < 10 for y in x) >= 2) == 1"]
        
          # the alphametic puzzle
          p = SubstitutedExpression(
            exprs,
            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))",
            s2d=dict(M=11),
            base=max(Pr) + 1,
            digits=Pr,
            #denest=8,      # use denest if not run with PyPy
            verbose=0,      # use 256 to see the generated code
          )
      
          # print answers
          for ans in p.answers():
            sol = 1
            print()
            for a in ans:
              print(''.join([f"{str(x):>4}" for x in a]))
            print(f"\nanswer: {ans[0]}")
          
        if sol: break   
      

      Like

      • NigelR's avatar

        NigelR 11:28 am on 13 August 2023 Permalink | Reply

        Hi Frits
        I could be missing something, and it may not be relevant to this problem, but the ‘break’ condition (line 10) in your function decompose_inc seems to risk missing some valid decompositions. For example, if you call decompose_inc(10,3,[1,2,3,4,5]), it doesn’t yield {1,4,5}. If you change the condition to ‘if t < k * n + 1: break' it seems to work correctly. Is there a reason for using +2 rather than +1?

        Like

        • Frits's avatar

          Frits 2:10 pm on 13 August 2023 Permalink | Reply

          Hi Nigel, I chose the “+ 2” as there always is a difference of at least 2 between odd prime numbers.

          Like

          • NigelR's avatar

            NigelR 5:46 pm on 13 August 2023 Permalink | Reply

            Thanks Frits. I can now see that it is tailored to the context rather than intended as a generic decompose generator, which is how I was trying to use it!

            Like

    • Frits's avatar

      Frits 2:09 pm on 13 August 2023 Permalink | Reply

      A MiniZinc solution with a matrix based on Jim’s program and Brian Gladman’s initial setup.
      It runs in 411ms.

       
      from enigma import (nsplit, join)
      from minizinc import MiniZinc
      
      # decompose <t> into <k> increasing numbers from <ns>
      def decompose_inc(t, k, ns, s=[]):
        if k == 1:
          if t in ns:
            yield set(s + [t])
        else:
          for i, n in enumerate(ns):
            if t < k * n + 2: break
            yield from decompose_inc(t - n, k - 1, ns[i + 1:], s + [n])      
      
      # format a sequence for MiniZinc
      fseq = lambda xs, sep=", ", enc="{}": join(xs, sep=sep, enc=enc)
      
      # list of prime numbers up to 997
      primes = [3, 5, 7]
      primes += [x for x in range(11, 100, 2) if all(x % p for p in primes)]
      primes += [x for x in range(101, 1000, 2) if all(x % p for p in primes)]
      
      total = sum(primes[:25])
      # find first entry higher than total for which total = s * 5 for an odd s
      while total % 5 or total % 2 == 0:
        total += 1
      
      sol = 0
      # potentially process all odd magical sums <s> up to a high number
      for s in range(total // 5, 10000, 2):
        # choose 25 prime numbers that sum up to 5 * s
        for Pr in decompose_inc(5 * s, 25, primes):
          sevens = [p for p in Pr if '7' in str(p)]
          # 9 diagonal cells from which center is 11 
          if (no7s := len(sevens)) > 9: continue
          
          # find numbers that include the digit 3, 5, 7
          (n3s, n5s, n7s) = (set(), set(), set())
          for n in Pr:
            ds = nsplit(n)
            if 3 in ds: n3s.add(n)
            if 5 in ds: n5s.add(n)
            if 7 in ds: n7s.add(n)
          
          magic = s
          model = f"""
       
          include "globals.mzn";
      
          % possible numbers
          set of int: Number = {fseq(Pr)};
      
          set of int: SIZE = 1..5; 
          
          array[SIZE, SIZE] of var Number: sq;
          
          %var set of int: r3 = array2set(row(sq, 3));
          %var set of int: r4 = array2set(row(sq, 4));
          %var set of int: c5 = array2set(col(sq, 5));
         
          % the centre cell contained Ann's age
          constraint sq[3, 3] = 11;
          
          constraint alldifferent(i in SIZE, j in SIZE) (sq[i, j]);
          
          % same row sums
          constraint forall(i in SIZE) (sum(j in SIZE) (sq[i, j]) = {magic});
          % same column sums
          constraint forall(j in SIZE) (sum(i in SIZE) (sq[i, j]) = {magic});
          % same diagonal sums
          constraint sum(i in SIZE) (sq[i, i]) = {magic};
          constraint sum(i in SIZE) (sq[i, 6 - i]) = {magic};
          
          % all primes but one with '7' are located on the diagonals
          % and the largest of these is sq[5, 5]
          constraint sum(i, j in SIZE where i == j \/ i + j == 6) (sq[i, j] in {fseq(n7s)} /\ sq[i, j] <= sq[5, 5]) == {no7s - 1};
          
          % and the other 4 cells in the middle row contain a digit 3
          % constraint card(r3 intersect {fseq(n3s)}) = 4;
          constraint sum(j in SIZE) (sq[3, j] in {fseq(n3s)}) = 4;
          
          % 2 cells in the far right column contain a digit 5
          %constraint card(c5 intersect {fseq(n5s)}) = 2;
          constraint sum(i in SIZE) (sq[i, 5] in {fseq(n5s)}) = 2;
          
          % as did 2 cells in the 4th row down
          %constraint card(r4 intersect {fseq(n5s)}) = 2;
          constraint sum(j in SIZE) (sq[4, j] in {fseq(n5s)}) = 2;
          
          solve satisfy;
          """      
          
          # load the model
          p = MiniZinc(model, solver="minizinc --solver gecode --all", verbose=0)
           
          # and solve it
          for s in p.solve():
            sq = s['sq']
            for y in sq:
              print(y)
            print("\nanswer:", sq[0])  
            sol = 1
            
        if sol: break  
      

      Like

      • Frits's avatar

        Frits 5:03 pm on 13 August 2023 Permalink | Reply

        The largest prime requirement is not needed to find a unique solution.
        Here is the MiniZinc constraint (it is faster to check in afterwards in Python).

           
        % largest prime on the grid was in the same column as two single digit primes
        constraint sum(j in SIZE) ({max(Pr)} in col(sq, j) /\ 
                   card(array2set(col(sq, j)) intersect {{3, 5, 7}}) >= 2) == 1;
        

        Like

    • Hugo's avatar

      Hugo 11:19 am on 20 August 2023 Permalink | Reply

      OEIS no. A164843 confirms that 233 is the smallest sum for an order-5 prime magic square.

      Is it possible to arrange the same twenty-five primes (alternatively with 97 and 107 instead of 101 and 103) to form a ‘panmagic’ square, i.e where the broken diagonals also sum to 233?

      Like

      • Jim Randell's avatar

        Jim Randell 5:50 pm on 20 August 2023 Permalink | Reply

        I added these constraints into my MiniZinc program, and it has been running some for some time to see if it is possible to find a construction (which makes me suspect it is not). But I will keep you posted if it finds anything.

        Like

        • Hugo's avatar

          Hugo 6:38 pm on 20 August 2023 Permalink | Reply

          Thanks, Jim.
          I too suspect it won’t work with sum 233. On the tangled web I found one with sum 395. There may be one with a smaller sum than that, but my program would take about 1000 years to find it.

          Like

  • Unknown's avatar

    Jim Randell 9:19 am on 10 August 2023 Permalink | Reply
    Tags:   

    Teaser 2484: [Snooker bag] 

    From The Sunday Times, 2nd May 2010 [link] [link]

    Josh put all the snooker balls except the cue [ball] into a bag. He and I then played an imaginary frame. At each person’s turn, he drew a ball. If it was red, he scored a point, discarded the red and drew again. If that was a “colour” (i.e. not red), he scored the appropriate number of points, and returned it to the bag, then tried again for a red. If a player drew the wrong ball, it was returned to the bag and his break was over.

    On one break, the first four balls drawn earned me points; at that stage, the chance of this happening was one in N, N being the number of points I’d got from the break so far.

    Which two colours had I drawn?

    This puzzle was originally published with no title.

    [teaser2484]

     
    • Jim Randell's avatar

      Jim Randell 9:19 am on 10 August 2023 Permalink | Reply

      There are 15 reds and 6 coloured balls in a standard snooker set.

      If at the start of the go there are R reds, then the probability of the first ball being red (for 1 point) is:

      P1 = R / (R + 6)

      and the chance of the second ball being a colour (for x points) is:

      P2 = 6 / (R + 5)

      the chance of the third ball being red (for 1 point) is:

      P3 = (R − 1) / (R + 5)

      and the chance of the fourth ball being a colour (for y points) is:

      P4 = 6 / (R + 4)

      The probability of getting this far is the product of these probabilities.

      And we want this product to give a value of 1/N for some positive integer N that is the total score from 2 colours.

      This Python program runs in 56ms. (Internal runtime is 91µs).

      from enigma import (irange, div, Decompose, printf)
      
      # decompose a total into a set of colours (2..7 points)
      decompose = Decompose(increasing=1, sep=0, min_v=2, max_v=7)
      
      # consider number of reds at the start of the break
      for R in irange(2, 15):
        # calculate the inverse of the overall probability
        # = (product of denominators) / (product of numerators)
        # which we want to be an integer N
        N = div((R + 6) * (R + 5) * (R + 5) * (R + 4), R * 6 * (R - 1) * 6)
        if not N: continue
        # find points for the 2 colours
        for cs in decompose(N - 2, 2):
          printf("R={R} N={N} -> cs={cs}")
      

      Solution: The colours were pink (6 points) and black (7 points).

      So there were 4 reds at the start of the break, the probabilities being:

      R = 4
      P1 = 4/10 = 2/5
      P2 = 6/9 = 2/3
      P3 = 3/9 = 1/3
      P4 = 6/8 = 3/4
      product = 1/15
      N = 15 = 6 + 7

      Like

  • Unknown's avatar

    Jim Randell 8:05 am on 8 August 2023 Permalink | Reply
    Tags:   

    Teaser 2639: Prime number 

    From The Sunday Times, 21st April 2013 [link] [link]

    I have given each letter of the alphabet a different whole-number value between 1 and 99 so that each letter represents one or two digits. In this way, for example, a three- letter word can represent a number of three, four, five or six digits. With my values it turns out that:

    PRIME = NUMBER.

    Furthermore, rather fortuitously, each letter used in that display has a value equal to an odd prime number.

    What is the number PRIME?

    [teaser2639]

     
    • Jim Randell's avatar

      Jim Randell 8:06 am on 8 August 2023 Permalink | Reply

      We have encountered a puzzle similar to this before (see: Teaser 2628), and I wrote some generic code to solve that puzzle, which can also be used to solve this puzzle.

      The following Python 3 program runs in 64ms. (Internal runtime is 8.8ms).

      Run: [ @replit ]

      from enigma import (primes, subsets, filter2, group, item, update, remove, translate, join, printf)
      
      # solve() routine copied from teaser2628r.py:
      
      # replace letters in <X>, <Y> with values from <ns>, so the strings formed are equal
      # <ns> groups values by their final digit
      # return the map: letter -> value
      def _solve(X, Y, ns, d):
        # are we done?
        if X == Y:
          yield d
        elif X and Y:
          # remove any common suffix
          while X and Y and X[-1] == Y[-1]: (X, Y) = (X[:-1], Y[:-1])
          if not (X and Y): return
          # split the final characters into numeric / non-numeric
          (nums, nons) = filter2((lambda x: x.isdigit()), [X[-1], Y[-1]])
          # different final numerics = failure
          if len(nums) > 1: return
          # choose values with the same final digit
          # if there is a numeric value use that, otherwise use the groups
          for k in (nums or ns.keys()):
            ss = ns.get(k, [])
            for vs in subsets(ss, size=len(nons), select='P'):
              # update the strings
              d_ = update(d, nons, vs)
              (X_, Y_) = (translate(x, d_, embed=0) for x in (X, Y))
              ns_ = update(ns, [(k, remove(ss, *vs))])
              # and solve for the translated strings
              yield from _solve(X_, Y_, ns_, d_)
      
      # replace letters in <X>, <Y>
      def solve(X, Y, ns):
        # group values by their final digit
        ns = group(map(str, ns), by=item(-1), fn=set)
        # and call the solver
        return _solve(X, Y, ns, dict())
      
      # now solve the puzzle:
      
      # possible numeric values
      ns = list(primes.irange(3, 99))
      
      # word values we are interested in
      (w1, w2) = ("PRIME", "NUMBER")
      
      # turn a word into a string
      fmt = lambda w, sep=':': join((d.get(x, x) for x in w), sep=sep)
      
      # solve the puzzle
      for d in solve(w1, w2, ns):
        # output solution
        printf("{w1}={f1} {w2}={f2}", f1=fmt(w1), f2=fmt(w2))
      

      Solution: PRIME = 531713717.

      We have:

      PRIME = 53:17:13:71:7
      NUMBER = 5:31:71:3:7:17

      Like

      • Frits's avatar

        Frits 7:54 pm on 8 August 2023 Permalink | Reply

        One performance improvement could be to add:

        vars2 = len(nons) == 2

        and

        if vars2 and len(vs[0] + vs[1]) != 3: continue

        I have more performance optimisations but this seems to be the one with the most impact.

        Like

        • Jim Randell's avatar

          Jim Randell 8:42 am on 9 August 2023 Permalink | Reply

          @Frits: To keep things generic we could just skip selections of 2 values where they have the same length. That is a simple change.

          More complicated would be to store candidate values by the final digit and the number of values required and populate it only with different length pairs, and that would avoid the recalculation at each step.

          Like

    • Frits's avatar

      Frits 12:59 pm on 8 August 2023 Permalink | Reply

      Without much analysis (except that both PRIME and NUMBER must have the same trailing digit F).

       
      from enigma import SubstitutedExpression, join
      
      # build string of valid numbers
      P = [3, 5, 7]
      P += [x for x in range(11, 100, 2) if all(x % p for p in P)]
      P = "{" + join(P, ",") + "}" 
      
      # check for same leading digits
      leading = lambda s1, s2: all(x == y for x, y in zip(join(s1), join(s2)))
      # check for same trailing digits
      trailing = lambda s1, s2: all(x == y for x, y in zip(join(s1)[::-1], join(s2)[::-1]))
       
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          #      PRIME             NUMBER  
          # PQ RF IJ MA EF == NO UV MA BC EF RF"
          
          # first check from the back
          "EF in @primes",
          "RF in @primes and len(s2 := {EF, RF}) == 2",
          "trailing([EF], [RF])",
          
          "MA in @primes and len(s3 := s2 | set([MA])) == 3",
          "trailing([MA, EF], [EF, RF])",
          
          # now check from the front
          "PQ in @primes and len(s4 := s3 | set([PQ])) == 4",
          "NO in @primes and len(s5 := s4 | set([NO])) == 5",
          "leading([PQ, RF], [NO])",
      
          "UV in @primes and len(s6 := s5 | set([UV])) == 6",
          "leading([PQ, RF], [NO, UV, MA])",
          
          "IJ in @primes and len(s7 := s6 | set([IJ])) == 7",
          "leading([PQ, RF, IJ], [NO, UV, MA])",
          
          "BC in @primes and BC not in s7",
          
          # check all digits
          "join(@prime) == join(@number)",
        ],
        answer="join(@prime)",
        d2i=dict([(k, "QFJAOVC") for k in [0, 2, 4, 6, 8]]),
        macro=dict([("primes", P)] + [("prime", "[PQ, RF, IJ, MA, EF]")] + \
                   [("number", "[NO, UV, MA, BC, EF, RF]")]
        ),
        distinct="",
        env=dict(leading=leading, trailing=trailing),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"answer: {ans}")
      

      Like

    • Jim Randell's avatar

      Jim Randell 4:46 pm on 8 August 2023 Permalink | Reply

      Here is a run file for this puzzle.

      It would be fairly straightforward to write a program that used this format for a generic solution.

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --base=100
      
      # digits = primes.between(3, 99)
      --digits="3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97"
      
      # check the concatenations match (working from the end)
      --code="check = lambda xs, ys, **kw: zip_eq(join(xs), join(ys), reverse=1, **kw)"
      
      # check the full strings
      "check([P, R, I, M, E], [N, U, M, B, E, R], strict=1)"
      
      # for performance check the partial strings
      "check([E], [R])"
      "check([M, E], [E, R])"
      "check([I, M, E], [B, E, R])"
      "check([R, I, M, E], [M, B, E, R])"
      "check([P, R, I, M, E], [U, M, B, E, R])"
      
      --template=""
      --answer="((P, R, I, M, E), (N, U, M, B, E, R))"
      
      # zip_eq(..., strict=1) only works in Python 3.10+
      --code="require('sys.version', '3.10')"
      

      Like

  • Unknown's avatar

    Jim Randell 5:21 pm on 6 August 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 606: Pints all round 

    From The Sunday Times, 18th February 1973 [link]

    “Little puzzle here”, says Bell at the pub, squeezing himself in. “Move up. Now, we are seven sat round the table. I am No. 1, naturally, and you are 2, 3, up to 7 and back to No. 1 clockwise”.

    “These cards numbered 1 to 7. I shuffle and deal. If you have your own number, say ‘Hit!’. Only one hit? Good! Pass the cards to your left one place. Double hit? Bad”.

    “The trick is to find out how the cards should be dealt for a single hit each time the cards are passed. I’ll make it easy. I always get the first hit and then numbers 2 and 6 get their hits as early as possible after that. Everything’s clockwise, numbering, dealing and passing”.

    “Usual prize one pint”.

    “Difficult? It’s a pushover. If you’re thirsty I’ll give you a hint. Let’s have a look at your cards. Anybody can see number 6 is going to come up last. Swap them round a bit. Hold on! Now you’ve got 5 then 2 then 7, so when 5 gets a hit so does 7. You’ve got to avoid that kind of thing”.

    In what order should the cards be dealt, beginning with No. 1 and proceeding clockwise?

    This puzzle is included in the book Sunday Times Brain Teasers (1974).

    [teaser606]

     
    • Jim Randell's avatar

      Jim Randell 5:22 pm on 6 August 2023 Permalink | Reply

      This Python program considers all possible arrangements of cards, and runs the process looking for a single hit on each turn. And it reports those with 7 turns that each give a single hits, and the first hit is 1. And I look for cases where the index sum of cards 2 and 6 in minimised.

      It runs in 63ms. (Internal runtime is 11.2ms).

      Run: [ @replit ]

      from enigma import (Accumulator, irange, rotate, subsets, singleton, printf)
      
      # find sequences of single hits
      def hits(ss):
        hs = list()
        while True:
          # we want a single hit on each turn
          h = singleton(x for (x, y) in enumerate(ss, start=1) if x == y)
          if h is None: break
          hs.append(h)
          if len(hs) == len(ss): break
          # pass the cards to the left
          ss = rotate(ss, -1)
        # return the order of the hits
        return hs
      
      r = Accumulator(fn=min, collect=1)
      
      # consider possible arrangements of the cards
      for ss in subsets(irange(1, 7), size=len, select='P'):
        hs = hits(ss)
        # single hit each time
        if not (len(hs) == 7): continue
        # the first hit is 1
        if not (hs[0] == 1): continue
        # accumulate values by positions for 2 and 6
        r.accumulate_data(hs.index(2) + hs.index(6), ss)
      
      # output solution
      for ss in r.data:
        printf("{ss}")
      

      Solution: The cards should be dealt in the order: 1, 5, 7, 3, 6, 4, 2.

      Like

  • Unknown's avatar

    Jim Randell 4:39 pm on 4 August 2023 Permalink | Reply
    Tags:   

    Teaser 3176: Equal shares 

    From The Sunday Times, 6th August 2023 [link] [link]

    When I married, my wife insisted that everything be equally shared between us, which I readily agreed to, provided it included the gardening.

    The garden is rectangular, with a smaller rectangular vegetable plot, of different relative dimensions, in one corner, the area of which is less than 7 per cent of that of the whole garden. The rest of the garden is lawned.

    To satisfy my wife, I constructed the shortest straight length of partition that would split the garden in half, so that half the vegetable plot and half the lawn were each side of the partition. The length of the partition was 30 metres, which exactly equalled the perimeter of the vegetable plot. Both before and after partitioning, all side lengths were an exact number of metres.

    What is the area of the lawn in each half?

    [teaser3176]

     
    • Jim Randell's avatar

      Jim Randell 5:40 pm on 4 August 2023 Permalink | Reply

      See also: Puzzle #213.

      I am assuming the layout is similar to this previous puzzle, with the vegetable plot fully occupying a corner of the garden.

      I used the [[ line_intersect() ]] function in the enigma.py library to determine where the partition meets the boundaries of the garden.

      This Python program runs in 170ms.

      Run: [ @replit ]

      from enigma import (Rational, irange, subsets, decompose, line_intersect, sq, catch, printf)
      
      Q = Rational()
      h = Q(1, 2)
      
      # check for partition crossing L/R boundaries
      def check1(x, y, a, b):
        r = line_intersect((h * a, h * b), (h * x, h * y), (0, 0), (0, y), internal=2, div=Q)
        return r.y.denominator == 1 and sq(x) + sq(y - 2 * r.y) == 900
      
      # check for partition crossing U/D boundaries
      def check2(x, y, a, b):
        r = line_intersect((h * a, h * b), (h * x, h * y), (0, 0), (x, 0), internal=2, div=Q)
        return r.x.denominator == 1 and sq(x - 2 * r.x) + sq(y) == 900
      
      # consider the dimensions of the garden
      for (y, x) in subsets(irange(1, 50), size=2):
        # consider dimensions of the vegetable plot
        for (a, b) in decompose(15, 2, increasing=0, sep=1, min_v=1):
          if not (a < x and b < y and Q(a, b) not in {Q(x, y), Q(y, x)} and a * b < Q(7, 100) * x * y): continue
          # look for a solution
          if any(catch(fn, x, y, a, b) for fn in [check1, check2]):
            # calculate allocation of lawn
            lawn = x * y - a * b
            printf("lawn allocation = {r} m^2 [x={x} y={y}; a={a} b={b}]", r=h * lawn)
      

      Solution: Each half has 270 sq m of lawn.

      And 18 sq m of vegetable plot.

      The garden is a 18 m × 32 m and the vegetable plot is 3 m × 12 m.

      Like

      • Jim Randell's avatar

        Jim Randell 10:53 pm on 4 August 2023 Permalink | Reply

        Or here is an alternative approach:

        We know the veg plot has a perimeter of 30, and integer sides, so we can generate possibilities for the dimensions. We can then look for an integer distance from a corner for the partition to hit the perimeter, and that gives us a line through the centre of the veg plot, and we can extend that line until it is 30m long, which gives us the overall dimensions of the garden.

        This Python program runs in 55ms. (Internal runtime is 332µs).

        Run: [ @replit ]

        from enigma import (irange, decompose, ihypot, div, printf)
        
        # consider the (a, b) veg plot, it has a perimeter of 30
        for (a, b) in decompose(15, 2, increasing=0, sep=1, min_v=1):
        
          # and where the partition hits the perimeter of the veg plot
          for d2 in irange(2, a - 1, step=2):
        
            # calculate the size of the (x, y) garden
            h = ihypot(a - d2, b)
            if h is None: continue
            x = div(30 * (a - d2), h)
            y = div(30 * b, h)
            if x is None or y is None: continue
            x += d2
            # check area and ratio conditions
            if not (100 * a * b < 7 * x * y): continue
            if a * y == x * b or a * x == y * b: continue
        
            # calculate the allocation of lawn
            lawn = (x * y - a * b) * 0.5
            printf("lawn allocation = {lawn:g} m^2 [a={a} b={b} d={d} -> x={x} y={y}]", d=d2 // 2)
        

        Like

        • Frits's avatar

          Frits 11:23 pm on 4 August 2023 Permalink | Reply

          @Jim, I don’t immediately see with this program how you can guarantee with that also the lawn is divided in half.

          Like

          • Jim Randell's avatar

            Jim Randell 11:30 pm on 4 August 2023 Permalink | Reply

            @Frits: Because the line that forms the partition passes through both the centre of the the veg plot and the centre of the entire garden, so each is divided exactly in half.

            Like

            • Frits's avatar

              Frits 12:04 am on 5 August 2023 Permalink | Reply

              @JIm, Thanks. I now see that the partition passes through the centre of the entire garden because of the adjustment of the x variable.

              Like

      • Frits's avatar

        Frits 10:37 am on 5 August 2023 Permalink | Reply

        Similar to the alternative approach.

        from enigma import pythagorean_triples, div
        
        # (a - 2 * d)^2 + b^2 = h^2, max(a - 2 * d, b) = 12 with d > 0
        for (p, q, h) in pythagorean_triples(12):
          # check both combinations of p and q
          for (j, k) in [(p, q), (q, p)]:
            a, b, amin2d = 15 - j, j, k
            if a < b: continue
            
            (d, r) = divmod(a - amin2d, 2)
            if not r and d > 0:
              x = div(30 * amin2d, h) 
              y = div(30 * b, h)
              if x is None or y is None: continue
              x += 2 * d
              if not (100 * a * b < 7 * x * y): continue
              if a * y == x * b or a * x == y * b: continue
                 
              # calculate the allocation of lawn
              lawn = (x * y - a * b) / 2
              print(f"answer: {lawn:g} m^2")   
        

        The pair (x, y) (without the addidion of 2*d) can also be easily be determined by using pythagorean_triples(30) as x^2 + y^2 = 30^2. From the pair it is not known which is x and which is y.

        Unfortunately (for performance reasons) there is no builtin reverse order in pythagorean_triples(). I am not sure yet how to continue if you know x and y.

        Like

        • Jim Randell's avatar

          Jim Randell 12:11 pm on 5 August 2023 Permalink | Reply

          Here’s a solution that starts from possible values of (x − 2d, y):

          from enigma import (irange, sum_of_squares, subsets, div, printf)
          
          # provide a stream of the permutations of elements in <seq>
          def permute(seq, select='P'):
            for ss in seq:
              yield from subsets(ss, size=len, select=select)
          
          # the length of the partition is 30m
          for (x_, y) in permute(sum_of_squares(900, 2, min_v=2, sep=1)):
            for b in irange(1, min(y - 1, 14)):
              h = div(30 * b, y)
              if h is None: continue
              a = 15 - b
              d2 = div(a * y - b * x_, y)
              if d2 is None or d2 < 2 or d2 % 2 != 0: continue
              x = x_ + d2
              # check area and ratio conditions
              if not (100 * a * b < 7 * x * y): continue
              if a * y == x * b or a * x == y * b: continue
          
              # calculate the allocation of lawn
              lawn = (x * y - a * b) * 0.5
              printf("lawn allocation = {lawn:g} m^2 [a={a} b={b} d={d} -> x={x} y={y}]", d=d2 // 2)
          

          Like

        • Jim Randell's avatar

          Jim Randell 1:11 pm on 6 August 2023 Permalink | Reply

          @Frits: The [[ pythagorean_triples() ]] approach is a very efficient way to solve the puzzle.

          There are only a few triangles that can fit in the vegetable plot, and only (3, 4, 5) gives a viable 2d value. And then one of the orientations is rejected by the area test.

          Like

    • Frits's avatar

      Frits 10:48 pm on 4 August 2023 Permalink | Reply

         
      from enigma import SubstitutedExpression, is_square
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        if d == 0: vs.update('A')
        # bit
        if d > 1: vs.update('X') 
        # MN < 30
        if d > 2: vs.update('M')
        # PQ < 42 (Situation1 : (PQ - 2 * H)**2 + MN**2 == 900 and H <= 6)
        if d > 4: vs.update('P')
        # b = 15 - A <= 14  as A = 1...7
        if d > 6: vs.update('GH')
        if d > 7: vs.update('A')
        
        d2i[d] = vs
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # check 2 situations depending on bit <X>
          # situation 1 (X = 1):  (0, H) is where the partition crosses the y-axis
          # situation 2 (X = 0):  (G, 0) is where the partition crosses the x-axis
          
          # length of the partition was 30 metres
          "X == 1 or P < 3",     # if X == 0 then PQ**2 < 900
          "PQ < 42",
          
          # set variable H to zero if not applicable
          "X == 1 or H == 0",
          # length of the partition was 30 metres
          "X == 0 or div(PQ - is_square(900 - MN**2), 2) == H",
         
          # set variable G to zero if not applicable
          "X == 0 or G == 0",
          # length of the partition was 30 metres
          "X == 1 or div(MN - is_square(900 - PQ**2), 2) == G",
         
          # garden and plot have dimensions MN by PQ and A by b = 15 - A (A: 1...7)
          "A < MN < PQ",
          
          # plot area is less than 7 per cent of that of the whole garden
          "100 * A * (b := 15 - A) < 7 * MN * PQ",
          
          # different relative dimensions
          "b * MN != A * PQ",
          "b < PQ",
        
          # situation 1: (0, H) must be on line through (A/2, b/2) and (MN/2, PQ/2)
          # line y = d.x + H, d = (PQ - b) / (MN - A)
          # X == 0 or b / 2 - (A / 2) * d == H leads to
          "X == 0 or A * (PQ - b) == (b - 2 * H) * (MN - A)",
          
          # situation 2: (G, x) must be on line through (A/2, b/2) and (MN/2, PQ/2)
          # line y = d.x + c  with c = b / 2 - A * d
          # X == 1 or d * G + b / 2 - A * d == 0 leads to
          "X == 1 or 2 * (A - G) * (PQ - b) == b * (MN - A)",
        ],
        answer="(MN * PQ - A * b) / 2, (MN, PQ), (A, b), (G, H), X, (PQ - b) / (MN - A)",
        d2i=d2i,
        distinct="",
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"answer: {ans[0] if ans[0] % 1 else int(ans[0])} m^2")
      

      Like

      • Frits's avatar

        Frits 10:53 am on 5 August 2023 Permalink | Reply

        A new maximum for the long side of the garden (PQ) seems to be 36. This follows from the x^2 + y^2 = 30^2 formula as mentioned in the text with the alternative approach program.

        Like

    • Hugo's avatar

      Hugo 5:35 pm on 13 August 2023 Permalink | Reply

      My first idea was a garden 30×14 with a 1-metre-wide strip of vegetable bed along a short side; the dividing line runs across the middle, parallel to the long sides. Though the area of the bed is certainly less than 7% of the total, it’s not exactly in one corner.

      Then I remembered Puzzle 213 and soon found the expected solution.
      There would be another with the garden 26×24 and the bed 11×4, but its area is then just over 7% of the total. It too involves a 3:4:5 triangle (scaled up sixfold), something I somehow suspected.

      Like

  • Unknown's avatar

    Jim Randell 9:28 am on 3 August 2023 Permalink | Reply
    Tags:   

    Teaser 2640: Megan’s number 

    From The Sunday Times, 28th April 2013 [link] [link]

    I had ten cards and each was labelled with a different digit. Megan chose three of these cards and arranged them to give a number. Beth then chose some of the remaining cards and also arranged them as a number. Finally Jan took some, or all, of the remaining cards and produced her number.

    If Megan multiplied her number by its last digit, she would get Beth’s number, and if Megan multiplied her number by its first digit she would get Jan’s number.

    Who has the biggest number and what is it?

    [teaser2640]

     
    • Jim Randell's avatar

      Jim Randell 9:29 am on 3 August 2023 Permalink | Reply

      It is straightforward to just consider possible 3-digit numbers for Megan. (Although we could reduce the number of candidates with some analysis).

      This Python program runs in 50ms. (Internal runtime is 1.4ms).

      Run: [ @replit ]

      from enigma import (irange, is_duplicate, printf)
      
      # Megan's number has 3 different digits
      for M in irange(100, 999):
        # Beth's number
        B = M * (M % 10)
        # J's number
        J = M * (M // 100)
        # all digits are distinct
        if is_duplicate(M, B, J): continue
      
        # who has the maximum number?
        d = { M: 'M', B: 'B', J: 'J' }
        m = max(d.keys())
        printf("max = {x} = {m} [M={M} B={B} J={J}]", x=d[m])
      

      Solution: Beth’s number is the largest. It is 819.

      The numbers are:

      M = 273
      B = 273 × 3 = 819
      J = 273 × 2 = 546

      Like

  • Unknown's avatar

    Jim Randell 9:11 am on 1 August 2023 Permalink | Reply
    Tags:   

    Brainteaser 1636: Bubble sum 

    From The Sunday Times, 16th January 1994 [link]

    My son recently sent me a rather nice GCSE project concerned with non touching soap bubbles. Imagine you are blowing bubbles either separately or into other bubbles. For instance, if you blew three bubbles, you could do this in exactly four ways:

    How many configurations are there for nine bubbles?

    [teaser1636]

     
    • Jim Randell's avatar

      Jim Randell 9:11 am on 1 August 2023 Permalink | Reply

      This program constructs the different arrangement of bubbles. An empty bubble is represented by the empty tuple, and bubbles are enclosed by placing them in a tuple in order. But when the arrangement is printed we don’t print the outermost enclosing brackets of the tuple (which allows for separate bubbles).

      This Python program runs in 96ms. (Internal runtime is 37ms).

      Run: [ @replit ]

      from enigma import (decompose, cproduct, uniq, wrap, join, arg, printf)
      
      # generate unique configurations for <n> bubbles
      @wrap(uniq)
      def bubbles(n):
        if n == 0:
          yield ()
        else:
          # combine two smaller configurations
          for (x, y) in decompose(n, 2, increasing=1, sep=0, min_v=1):
            for (bx, by) in cproduct([bubbles(x), bubbles(y)]):
              yield tuple(sorted(bx + by))
          # or enclose n - 1 bubbles
          for b in bubbles(n - 1):
            yield (b,)
      
      # format a bubble configuration (default: without outermost enclosure)
      def fmt(bs, enc=""):
        return join((fmt(b, enc="()") for b in bs), sep=" ", enc=enc)
      
      n = arg(9, 0, int)
      printf("[bubbles({n})]")
      for (i, bs) in enumerate(bubbles(n), start=1):
        printf("{i}: {bs}", bs=fmt(bs))
      

      Solution: For 9 bubbles there are 719 configurations.

      The program is fast enough up to about 15 bubbles (235,381 configurations) without bothering about caching results.


      A configuration of bubbles can be represented as a tree, with an arc between bubble X and bubble Y if X directly contains Y. We also add arcs from a root bubble to all outermost bubbles (this does the job of the outermost enclosing tuple in my program above). And for n bubbles this gives us an unlabelled rooted tree with (n + 1) nodes.

      So we can count the number of bubble configurations by determining the number of unlabelled rooted trees.

      And this is sequence OEIS A000081, which has the following recurrence relation:

      \displaystyle a(n+1) = \frac{1}{n} \sum_{k=1}^{n} \left( \sum_{d | k}^{}{d \cdot a(d)} \right) a(n-k+1)

      Fortunately this is implemented fairly easily:

      Run: [ @replit ]

      from enigma import (irange, divisors, cache, arg, printf)
      
      # the number of different ways to draw diagrams with n bubbles is the
      # number of unlabelled rooted trees with (n + 1) nodes = OEIS A000081
      @cache
      def a(n):
        if n < 2: return n
        r = 0
        for k in irange(1, n - 1):
          r += sum(d * a(d) for d in divisors(k)) * a(n - k)
        return r // (n - 1)
      
      # number of configurations for <n> bubbles
      bubbles = lambda n: a(n + 1)
      
      n = arg(9, 0, int)
      k = bubbles(n)
      printf("{n} bubbles -> {k} arrangements")
      

      And this allows us to calculate much larger numbers (although for very large numbers we need to increase the recursion limit on the Python interpreter).

      Like

  • Unknown's avatar

    Jim Randell 11:35 am on 30 July 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 941: A Brain Teaser 

    From The Sunday Times, 3rd August 1980 [link]

    I showed Charlie the division sum illustrated above, and told him that, as usual in this sort of problem, each letter represents one particular digit throughout, and different letters represent different digits. This baffled Charlie, as he has always been hopeless at division, so I reminded him that any division sum can be re-interpreted in terms of  multiplication, thus:

    A × BRAIN = TEASER

    That made Charlie chuckle (he’s a simple lad), but it didn’t help him much. So to assist him with his arithmetic, I reminded him of the fact that:

    TEN + TWO is a multiple of 4.

    He then had enough information to interpret each letter correctly, but to save him some work I also reminded him that:

    NINE and NINETEENTEN are multiples of 9

    whereas:

    NINE + TEN is not a multiple of 2, 3 or 5.

    What does ANSWER equal?

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

    [teaser941]

     
    • Jim Randell's avatar

      Jim Randell 11:36 am on 30 July 2023 Permalink | Reply

      We can solve this puzzle using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      The following run file executes in 79ms. (Internal runtime of the generated program is 2.8ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "A * BRAIN = TEASER"
      "(TEN + TWO) % 4 = 0"
      
      "NINE % 9 = 0"
      "(NINETEEN - TEN) % 9 = 0"
      "all((NINE + TEN) % n > 0 for n in [2, 3, 5])"
      
      --answer="ANSWER"
      

      Solution: ANSWER = 780196.

      The initial alphametic division has two solutions:

      327026 ÷ 46718 = 7
      397096 ÷ 56728 = 7

      However the first of these is eliminated by the requirement: “TEN + TWO is a multiple of four”.

      Like

    • GeoffR's avatar

      GeoffR 4:19 pm on 30 July 2023 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 1..9:B; var 0..9:R; var 0..9:I; 
      var 1..9:N; var 1..9:T; var 0..9:E; var 0..9:S; 
      var 0..9:W; var 0..9:O; 
      
      constraint all_different([A, B, R, I, N, T, E, S, W, O]);
      
      var 100..999:TEN == 100*T + 10*E + N;
      var 100..999:TWO == 100*T + 10*W + O;
      var 1000..9999:NINE == 1010*N + 100*I + E;
      var 10000..99999:BRAIN == 10000*B + 1000*R + 100*A + 10*I + N;
      var 100000..999999:TEASER == 100000*T + 10010*E + 1000*A + 100*S + R;
      var 100000..999999:ANSWER == 100000*A + 10000*N + 1000*S + 100*W + 10*E + R;
      var 10000000..99999999:NINETEEN == 10010001*N + 1000000*I + 1000*T + 10110*E;
      
      constraint A * BRAIN = TEASER;
      % TEN + TWO is a multiple of 4.
      constraint (TEN + TWO) mod 4 == 0;
      
      % NINE and NINETEEN − TEN are multiples of 9
      constraint NINE mod 9 == 0 /\ (NINETEEN - TEN) mod 9 == 0;
      
      % NINE + TEN is not a multiple of 2, 3 or 5.
      constraint (NINE + TEN) mod 2 > 0 /\ (NINE + TEN) mod 3 > 0  /\ (NINE + TEN) mod 5 > 0;
      
       solve satisfy;
       output["ANSWER = " ++ show(ANSWER) ++ "\n"
       ++ "[A, B, R, I, N, T, E, S, W, O]  = " ++ show ([A, B, R, I, N, T, E, S, W, O] )]; 
      
      % ANSWER = 780196
      % [A, B, R, I, N, T, E, S, W, O]  = 
      % [7, 5, 6, 2, 8, 3, 9, 0, 1, 4]
      % ----------
      % ==========
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:38 pm on 28 July 2023 Permalink | Reply
    Tags:   

    Teaser 3175: Blindfold roulette 

    From The Sunday Times, 30th July 2023 [link] [link]

    Our roulette wheel has fewer than 100 equal-sized sectors. Before each game a fixed number (fewer than half) of the sectors are randomly designated winning ones and the others as losing. A player can see which are the winning sectors, then is blindfolded. A ball is then placed at random in a winning sector and the player chooses to spin (S) the wheel or move (M) the ball clockwise by one sector. The game continues from there (the player has the same  choices) and ends when the ball lands in a losing sector.

    Alf’s longest winning run was MMSM while Bert’s was SSS and Charlie’s was MMMS. Being expert logicians, they had always chosen the option that gave the better chance of the ball landing in a winning sector. Remarkably, the probabilities of achieving each run of success were the same.

    How many sectors are there and how many are winning sectors?

    [teaser3175]

     
    • Jim Randell's avatar

      Jim Randell 6:55 pm on 28 July 2023 Permalink | Reply

      This is my first stab at a program that explores the solution space looking for viable solutions. It is not very fast (it runs in 1.31s), and it stops after the first viable solution is found. (A second viable solution is eliminated by rejecting situations where both plays have the same probability).

      It works by considering possible the total number of sectors n, and the number of winning sectors w, and then dividing the winning sectors into contiguous clumps, and calculating the corresponding probabilities.

      Run: [ @replit ]

      from enigma import (Rational, irange, decompose, intersect, divf, seq2str, printf)
      
      Q = Rational()
      
      # with <n> sectors, <w> winning in clumps <ws>, find probability of seq <seq>
      def prob(n, w, ws, seq):
        P = 1
        # calculate probability of a good spin
        S = Q(w, n)
        # calculate probability of a good move
        zs = ws  # winning zones
        for v in seq:
          z = sum(zs)
          zs = list(x - 1 for x in zs if x > 1)
          M = Q(sum(zs), z)
          # check the sequence
          if v == 'M' and M > S:
            P *= M
          elif v == 'S' and S > M:
            P *= S
            zs = ws
          else:
            return
        return P
      
      # with <n> sectors, <w> winning, collect common probabilities for sequences <ss>
      def solve(n, w, ss):
        # collect probabilities
        d = dict((k, set()) for k in ss)
        # break the sectors into k contiguous blocks
        for k in irange(1, w):
          for ws in decompose(w, k, increasing=1, sep=0, min_v=1):
            # calculate the probabilities for A, B, C
            for (k, v) in d.items():
              P = prob(n, w, ws, k)
              if P is not None: v.add(P)
        # return common probabilities
        return intersect(d.values())
      
      # solve the puzzle
      def main():
        # consider number of sectors <n>
        for n in irange(9, 99):
          # consider number of winning sectors <w>
          for w in irange(4, divf(n - 1, 2)):
            # find common probabilities
            ps = solve(n, w, ['MMSM', 'SSS', 'MMMS'])
            if ps:
              printf("n={n} w={w} -> P={ps}", ps=seq2str(ps))
              return  # stop at the first solution
      
      main()
      

      Solution: There are 54 sectors in total, and 18 winning sectors.

      In this case the probability of a good spin is:

      P(S) = 18/54 = 1/3

      So the probability of B’s run of SSS is:

      P(SSS) = (1/3)³ = 1/27

      And a configuration where each winning sector is isolated from the other winning sectors makes the probability of a good M move zero, so this is possible (although this is not the only viable configuration to permit this – there are 19 in total).

      For A there are 2 possible configurations, one of them is:

      (1, 1, 1, 1, 2, 3, 3, 3, 3)

      Of the 18 winning sectors there are 9 that have a winning sector as a clockwise neighbour:

      P(M) = 9/18 = 1/2 [which is > 1/3]

      And of those 9 sectors only 4 of them have 2 neighbouring winning sectors going clockwise:

      P(MM|M) = 4/9 = 1/2 [which is > 1/3]

      And there are no winning sectors with 3 neighbouring clockwise winning sectors:

      P(MMM|MM) = 0 [which is < 1/3]

      So at this point A would choose S, which resets the situation back to the start, so the next choice (if he gets one) would be M.

      Hence the overall probability of MMSM is:

      P(MMSM) = (9/18)×(4/9)×(1/3)×(9/18) = 1/27

      which is the same as B’s run.

      For C, with a configuration (chosen from 9 possibilities) of:

      (2, 2, 2, 2, 2, 4, 4)

      We get:

      P(M) = 11/18
      P(MM|M) = 4/11
      P(MMM|MM) = 2/4 = 1/2

      P(MMMS) = (11/18)×(4/11)×(2/4)×(1/3) = 1/27


      However if we permit situations where the probabilities of M and S are equal then there is a further solution with 64 total sectors and 16 winning sectors.

      The probability of a good S is:

      P(S) = 16/64 = 1/4

      And choosing a configuration (of 12 the possible configurations) where each winning sector is isolated we get:

      P(SSS) = (1/4)³ = 1/64

      For A the only possible distribution is:

      (1, 1, 2, 2, 2, 2, 3, 3)

      which gives:

      P(M) = 8/16 = 1/2
      P(MM|M) = 2/8 = 1/4 [same as P(S)]

      P(MMSM) = (8/16)×(2/8)×(1/4)×(8/16) = 1/64

      And for C there are 14 possible distributions, we choose:

      (2, 2, 2, 3, 3, 4)

      which gives:

      P(M) = 10/16 = 5/8
      P(MM|M) = 4/10 = 2/5
      P(MMM|MM) = 1/4 [same as P(S)]

      P(MMMS) = (10/16)×(4/10)×(1/4)×(1/4) = 1/64

      So in order for the puzzle to have a unique answer, we have to eliminate scenarios where when choosing between M and S that calculated probabilities are the same

      Like

      • Jim Randell's avatar

        Jim Randell 10:55 am on 30 July 2023 Permalink | Reply

        By analysing the probabilities associated with the given sequences (which I will post later) we can eliminate a lot of the testing done by my program above.

        Here it is adapted to include the conditions:

        decomposition for A must include a clump of size 3 or more
        decomposition for C must include a clump of size 4 or more
        n² divides w³

        (This last condition is the same as suggested by Frits in his comment below).

        It calculates the probability for B, and then finds example decompositions for A and C that give the same probability.

        This Python program fully explores the solution space and runs in 92ms. (Internal runtime is 27ms).

        Run: [ @replit ]

        from enigma import (Rational, irange, divf, div, decompose, first, printf)
        
        Q = Rational()
        
        # with <n> sectors, <w> winning in clumps <ws>, find probability of seq <seq>
        def prob(n, w, ws, seq):
          P = 1
          # calculate probability of a good spin
          S = Q(w, n)
          # calculate probability of a good move
          (zs, d) = (ws, w)  # winning zones
          for v in seq:
            # calculate new zones
            zs = list(x - 1 for x in zs if x > 1)
            n = sum(zs)
            M = Q(n, d)
            # check the sequence
            if v == 'M' and M > S:
              P *= M
              d = n
            elif v == 'S' and S > M:
              P *= S
              (zs, d) = (ws, w)
            else:
              return
          return P
        
        # with <n> sectors, <w> winning, sequence <seq>
        # find winning clumps <ws> that give probability <p>
        # subject to max(<ws>) >= <m>
        def solve(n, w, seq, m, p):
          for k in irange(1, w):
            for ws in decompose(w, k, increasing=1, sep=0, min_v=1):
              if ws[-1] < m: continue
              if prob(n, w, ws, seq) == p:
                yield ws
        
        # consider possible n, w values such that n^2 divides w^3
        for n in irange(9, 99):
          for w in irange(4, divf(n - 1, 2)):
            k3 = div(w**3, n**2)
            if k3 is None: continue
        
            # calculate probability for B (SSS)
            # a split into <w> clumps of size 1 will do
            B = prob(n, w, [1] * w, "SSS")
        
            # find examples for C (MMMS) and A (MMSM):
            # for A we need at least one clump of size 3 or more
            As = first(solve(n, w, "MMSM", 3, B), 1)
            if not As: continue
        
            # for C we need at least one clump of size 4 or more
            Cs = first(solve(n, w, "MMMS", 4, B), 1)
            if not Cs: continue
        
            # output solution
            printf("n={n} w={w} -> P={B}, A={As} C={Cs}")
        

        Here is the analysis:

        C was able to make 3 consecutive Ms, so there must have been a block of at least 4 contiguous winning sectors in that game. Hence the wheel must have at least 9 sectors in total.

        Similarly in the game where A made 2 consecutive Ms, there must have been a block of at least 3 contiguous winning sectors.

        If the wheel has n sectors in all, and w winning sectors, then the probability of winning on a spin is:

        P(S) = w/n

        (which is less than 50% by definition).

        So for B we have:

        P(SSS) = w³/n³

        regardless of the configuration of the winning sectors.

        Except we also require there to be some configuration where:

        P(S) > P(M)

        But if every winning sector is in a clump of 1 (which is possible as there are fewer than half winning sectors) we have:

        P(M) = 0

        so the inequality above can be satisfied.

        Now if we consider a situation where k1 of the winning sectors are “good” (i.e. they are directly anti-clockwise from another winning sector), then we have:

        P(M) = k1/w

        For the next move suppose only k2 of the k1 sectors are “good”, we have:

        P(MM) = (k1/w)(k2/k1) = k2/w

        and so on:

        P(MMM) = (k1/w)(k2/k1)(k3/k2) = k3/w

        where: (w, k1, k2, k3) form a decreasing sequence.

        And so for C we have:

        P(MMMS) = (k3/w)(w/n) = k3/n

        And this must be the same as P(SSS) above:

        k3/n = w³/n³
        k3 = w³/n²

        So: n² must divide into w³ (as k3 is an integer).

        This narrows us down to just 4 possible (n, w) pairs:

        (27, 9)
        (54, 18)
        (64, 16)
        (81, 27)

        Which have give the following probabilities for B

        (27, 9) → 1/27
        (54, 18) → 1/27
        (64, 16) → 1/64
        (81, 27) → 1/27

        And for C gives k3 values of:

        (27, 9) → 1
        (54, 18) → 2
        (64, 16) → 1
        (81, 27) → 3

        For A we get:

        P(MMSM) = k1.k2 / n.w

        Giving the following k1.k2 values:

        (27, 9) → 9 (not possible)
        (54, 18) → 36 = 12×3 = 9×4
        (64, 16) → 16 = 8×2
        (81, 27) → 81 (not possible)

        So there are only 2 pairs to consider, and we need to find appropriate decompositions of the winning sectors for A and C that give the appropriate probability (same as B), and where the choice to play M or S is always that with the larger probability.

        Like

    • Frits's avatar

      Frits 2:01 pm on 29 July 2023 Permalink | Reply

      Extra analysis:

      for MMMS we have sum(x – 3 for x in ws if x > 3) * n^2 = w^3
      for MMSM we have sum(x – 1 for x in ws if x > 1) * sum(x – 2 for x in ws if x > 2) *n^2 = w^4

      This adaptation of Jim’s program checks the full solution space in 30ms.

       
      from enigma import (Rational, irange, decompose, intersect, divf, seq2str, printf)
       
      Q = Rational()
       
      # with <n> sectors, <w> winning in clumps <ws>, find probability of seq <seq>
      def prob(n, w, ws, seq):
        P = 1
        # calculate probability of a good spin
        S = Q(w, n)
        # calculate probability of a good move
        zs = ws  # winning zones
        for v in seq:
          z = sum(zs)
          zs = list(x - 1 for x in zs if x > 1)
          M = Q(sum(zs), z)
          
          # check the sequence
          if v == 'M' and M > S:
            P *= M
          elif v == 'S' and S > M:
            P *= S
            zs = ws
          else:
            return
          
        return P if P == S**3 else None
       
      # with <n> sectors, <w> winning, collect common probabilities for sequences <ss>
      def solve(n, w, ss):
        # collect probabilities
        d = dict((k, set()) for k in ss)
      
        w3byn2, w4byn2 = w**3 // n**2, w**4 // n**2
        
        # break the sectors into k contiguous blocks
        for k in irange(1, w):
          for ws in decompose(w, k, increasing=1, sep=0, min_v=1):
            # we have a solution if P = (w/n)^3 has been found for both MMSM and MMMS
            if d[ss[0]] and d[ss[2]]:
              return d[ss[0]]
            
            fn = lambda s, y: sum(x - y for x in s if x > y) 
            # check if P can be calculated as (w/n)^3
            if (fn(ws, 3) != w3byn2 or d[ss[2]]) and \
               (fn(ws, 1) * fn(ws, 2) != w4byn2 or d[ss[0]]): continue
            
            # calculate only the probabilities for MMSM and MMMS  
            # (as P for SSS equals (w/n)^3 if ws = [1, 1, ..., 1, 1])
            for s in [ss[0], ss[2]]:
              P = prob(n, w, ws, s)
              if P: d[s] = {P}
        # return common probabilities
        return intersect(d.values())
       
      # solve the puzzle
      def main():
        # consider number of sectors <n>
        for n in irange(9, 99):
          # consider number of winning sectors <w>
          for w in irange(4, divf(n - 1, 2)):
            # skip early if probability for MMSM and MMMS can't be (w/n)^3
            if w**4 % n**2 or w**3 % n**2: continue
            # find common probabilities
            ps = solve(n, w, ['MMSM', 'SSS', 'MMMS'])
            if ps:
              printf("n={n} w={w} -> P={ps}", ps=seq2str(ps))
              # continue to check the full solution space
       
      main()  
      

      Like

    • Frits's avatar

      Frits 9:04 pm on 30 July 2023 Permalink | Reply

      Trying to perform decomposition with SubstitutedExpression().
      In order to not use too many variables the maximum number of decomposition variables is calculated.
      The programs runs in 185ms.

       
      from enigma import SubstitutedExpression, divf, div
      
      # function used in probability formula for M
      fn = lambda s, y: sum(x - y for x in s if x > y) 
      
      symbols = "ABCDEFGHIJKLMNOPQRabcdefghijklmopqrtuvxyx"
      syms2 = ["ST", "UV", "WX", "YZ"]
      
      # consider possible n, w values such that n^2 divides w^3
      for n in range(9, 100):
        for w in range(4, divf(n - 1, 2) + 1):
          if w**3 % n**2: continue
          
          # try to decompose <w> into  A, B, .. , xx 
          # maximum number of elements to decompose <w>
          mx = (w + 1) - (w**2 // n + 2)
          # maximum number of 2-digit variables needed
          n2 = w // 10
          
          # invalid digit / symbol assignments
          d2i = {d: set() for d in range(10)}
          for i in range(n2 - 1):
            for d in range((w // (n2 - i)) // 10 + 1, 10):
              d2i[d].add(syms2[i][0])
          
          # build expressions
          exprs = []
          for i in range(mx - 1):
            j = mx - n2 - i
            cur = symbols[i] if j > 0 else syms2[-j]
            nxt = symbols[i + 1] if j > 1 else syms2[0] if j == 1 else syms2[(1 - j)]
            
            if i < mx - 2:
              exprs.append(f"{{{cur}}} <= {{{nxt}}}")
            else:
              exp = f"{{{cur}}} <= {{{nxt}}}"  # save for later
              
            # restrict values of single digit variables
            if len(cur) == 1:
              for d in range(w // (mx - i) + 1, 10):
                d2i[d].add(cur)
            else:
              # restrict values by adding an expression
              exprs.append(f"{{{cur}}} <= {w // (mx - i)} ")
      
          vars = [f"{{{s}}}" for s in symbols[:mx - n2]] 
          vars += [f"{{{s}}}" for s in syms2[:n2]]
          svars = ','.join(vars)
          
          # direct assignment of last variable
          exprs.append(f"{w} - sum([{','.join(vars[:-1])}]) = {vars[-1]}",)
          exprs.append(exp)
          
          exp = ""
          # for MMMS we have sum(x – 3 for x in ws if x > 3) * n^2 = w^3
          exp += f"(c1 := (fn(svars:= [{svars}], 3) * {n**2} == {w**3}) and "
          exp += f"{n} * fn([{svars}], 1) > {w**2} and "
          exp += f"{n} * fn([{svars}], 2) > {w} * fn([{svars}], 1) and "
          exp += f"{n} * fn([{svars}], 3) > {w} * fn([{svars}], 2) and "
          exp += f"{n} * fn([{svars}], 4) < {w} * fn([{svars}], 3)) or "
          
          # for MMSM we have sum(x – 1 for x in ws if x > 1) * 
          #                  sum(x - 2 for x in ws if x > 2) * n^2 = w^4
          exp += f"(c2 := (fn([{svars}], 1) * fn([{svars}], 2) * {n**2} == {w**4}) "
          exp += f"and {n} * fn([{svars}], 1) > {w**2} and "
          exp += f"{n} * fn([{svars}], 2) > {w} * fn([{svars}], 1) and "
          exp += f"{n} * fn([{svars}], 3) < {w} * fn([{svars}], 2))"
          exprs.append(exp)
          
          # the alphametic puzzle
          p = SubstitutedExpression(
            exprs,
            answer="c1, c2, vars",
            d2i=d2i,
            distinct="",
            env=dict(fn=fn),
            reorder=0,
            verbose=0,    # use 256 to see the generated code
          )
      
          # print answers
          cnts = [0, 0]
          for ans in p.answers():
            #print(f"{ans}")
            if ans[0]: cnts[0] = 1
            if ans[1]: cnts[1] = 1
          
            if cnts == [1, 1]:
              print(f"answer: n={n}, w={w}")
              break  
      

      Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 7:11 pm on 3 August 2023 Permalink | Reply

      You can do a lot with Frits’ MMMS result and the limits for n and w, to constrain the search space. I used a slightly different paradigm from Jim’s, considering numbers of winning segments with clockwise winning neighbours, so my nomenclature is a bit different. Also I am coding in Rust now so won’t reproduce my solution here. However, I have written up a solution which can be applied manually in the comments of my code at:

      https://github.com/TuonyBT/teaser3175/blob/master/src/main.rs

      Like

    • Frits's avatar

      Frits 9:52 pm on 8 August 2023 Permalink | Reply

      There is a different answer (n=64, w=16) when a winning run is supposed to be followed by a loss.

      from enigma import (Rational, irange, decompose, intersect, divf, seq2str, printf)
      
      fn = lambda s, y: sum(x - y for x in s if x > y) 
       
      Q = Rational()
       
      # with <n> sectors, <w> winning in clumps <ws>, find probability of seq <seq>
      def prob(n, w, ws, seq, target):
        P = 1
        # calculate probability of a good spin
        S = Q(w, n)
        # calculate probability of a good move
        zs = ws  # winning zones
        for v in seq:
          z = sum(zs)
          zs = list(x - 1 for x in zs if x > 1)
          M = Q(sum(zs), z)
          
          # check the sequence
          if v == 'M' and M > S:
            P *= M
          elif v == 'S' and S > M:
            P *= S
            zs = ws
          else:
            return
        
        P *= (1 - fn(ws, 1) / w) if seq[-1] == 'S' else (1 - fn(ws, 2) / fn(ws, 1)) 
        return P if P == S**3 * (n - w) / n else None
        
      
      # with <n> sectors, <w> winning, collect common probabilities for sequences <ss>
      def solve(n, w, ss):
        # collect probabilities
        d = dict((k, set()) for k in ss)
      
        # probability for winning run SSS
        F =  (n - w) * w**4 / n**3
        
        # maximum number of elements to decompose <w>
        mx = (w + 1) - (w**2 // n + 2)
        # break the sectors into k contiguous blocks
        for k in irange(1, mx):
          for ws in decompose(w, k, increasing=1, sep=0, min_v=1):
            # we have a solution if P = (w/n)^3 has been found for both MMSM and MMMS
            if d[ss[0]] and d[ss[2]]:
              return d[ss[0]]
            
            # check if P can be calculated as F
            if (fn(ws, 3) * w - fn(ws, 1) * fn(ws, 3) != F or d[ss[2]]) and \
               (fn(ws, 1) * fn(ws, 2) - fn(ws, 2)**2 != F or d[ss[0]]): continue
       
            # calculate only the probabilities for MMSM and MMMS  
            # (as P for SSS equals F if ws = [1, 1, ..., 1, 1])
            for s, t in [(ss[0], (w/n)**3 ), (ss[2], (w/n)**3)]:
              P = prob(n, w, ws, s, t)
              if P: d[s] = {P}
        # return common probabilities
        return intersect(d.values())
      
      # fn(a, b) = sum(x – b for x in a if x > b)
      # for SSS we have probablility (w/n)**3 * (n - w) / n
      # for MMMS we have fn(ws, 3) * w - fn(ws, 1) * fn(ws, 3) = (n - w) * w**4 / n**3
      # for MMSM we have fn(ws, 1) * fn(ws, 2) - fn(ws, 2)**2  = (n - w) * w**4 / n**3
      
      # solve the puzzle
      def main():
        # consider number of sectors <n>
        for n in irange(9, 99):
          # consider number of winning sectors <w>
          for w in irange(4, divf(n - 1, 2)):
            # skip if probability for MMSM and MMMS can't be (w/n)^3 * (n - w) / n
            if (w**4 * n - w**5) % n**2: 
              continue
            
            # find common probabilities
            ps = solve(n, w, ['MMSM', 'SSS', 'MMMS'])
            if ps:
              printf("\nn={n} w={w} -> P={ps}\n", ps=seq2str(ps))
              # continue to check the full solution space
       
      main()   
      

      Like

      • Jim Randell's avatar

        Jim Randell 10:08 pm on 9 August 2023 Permalink | Reply

        I interpreted the probabilities as being those of achieving the sequences given, and not being concerned about what happened on the next play.

        But I augmented my program to include the probability of a loss on the next play of each run and I got 4 answers to the puzzle:

        n=27, w=9, P=2/81
        n=54, w=18, P=2/81
        n=64, w=16, P=3/256
        n=81, w=27, P=2/81

        For example, with the first of these we have 27 sectors and 9 of them are winning, so:

        The probability of a good spin is 9/27 = 1/3 (and the probability of a bad spin is 2/3).

        So for B the probability of (S=win, S=win, S=win, S=lose) is:

        P(SSS(S)) = (1/3)×(1/3)×(1/3)×(1 − 1/3) = 2/81

        For A the configuration could be (1, 2, 3, 3), giving the probability of (M=win, M=win, S=win, M=win, M=lose) as:

        P(MMSM(M)) = (5/9)×(2/5)×(1/3)×(5/9)×(1 − 2/5) = 2/81

        For C the configuration could be (1, 4, 4), giving the probability of (M=win, M=win, M=win, S=win, M=lose) as:

        P(MMMS(M)) = (6/9)×(4/6)×(2/4)×(1/3)×(1 − 6/9) = 2/81

        Like

        • Frits's avatar

          Frits 10:57 am on 10 August 2023 Permalink | Reply

          @Jim, you are right. Using rational numbers class Q in line 28/29 also yields 4 answers.

             
          P *= (1 - Q(fn(ws, 1), w)) if seq[-1] == 'S' else (1 - Q(fn(ws, 2), fn(ws, 1))) 
          return P if n * P == S**3 * (n - w) else None
          

          Like

  • Unknown's avatar

    Jim Randell 9:30 am on 27 July 2023 Permalink | Reply
    Tags:   

    Teaser 2632: Times table 

    From The Sunday Times, 3rd March 2013 [link] [link]

    I have placed the numbers 1 to 16 (in no particular order) in a four-by-four table. As you go across each row of the table (from left to right) the four numbers are increasing, and as you go down each column the four numbers are increasing. If you read the top row as one long number, it is a four-figure number which equals the product of the four numbers in the second row.

    What are the four numbers in the second row?

    [teaser2632]

     
    • Jim Randell's avatar

      Jim Randell 9:31 am on 27 July 2023 Permalink | Reply

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py to solve this puzzle.

      The following run file executes in 152ms. (Internal runtime for the generated code is 76ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C D
      #  E F G H
      #  I J K L
      #  M N P Q
      
      --base=17
      --digits="1-16"
      
      # rows are increasing
      "A < B" "B < C" "C < D"
      "E < F" "F < G" "G < H"
      "I < J" "J < K" "K < L"
      "M < N" "N < P" "P < Q"
      
      # columns are increasing
      "A < E" "E < I" "I < M"
      "B < F" "F < J" "J < N"
      "C < G" "G < K" "K < P"
      "D < H" "H < L" "L < Q"
      
      # the top row gives a 4-digit product of the numbers in the second row
      "D < 10"
      "nconcat(A, B, C, D, base=10) == E * F * G * H"
      
      --answer="(E, F, G, H)"
      
      --template="(A B C D) (E F G H) (I J K L) (M N P Q)"
      

      Solution: The numbers in the second row are: 2, 7, 8, 13.

      The first two rows are:

      (1, 4, 5, 6)
      (2, 7, 8, 13)

      And we have:

      2 × 7 × 8 × 13 = 1456

      The bottom two rows are:

      (3, 9|10, 10|11|12, 14|15)
      (9|10|11, 11|12, 14|15, 16)

      So there are 10 ways that the grid can be filled out.

      Like

      • Frits's avatar

        Frits 3:25 pm on 27 July 2023 Permalink | Reply

        @Jim,

        Which tags do you use for text in the grey commentary boxes ?
        I can see blockquotes are used in the final html.

        Like

    • Frits's avatar

      Frits 3:06 pm on 27 July 2023 Permalink | Reply

      Same concept but with calculating upper and lower bounds.

      from enigma import SubstitutedExpression
      
      symbols = "ABCDEFGHIJKLMNPQ"
      
      # (row i, col j) has i * j - 1 lower values  (rows and cols 1-4)
      # (row i, col j) has (5 - i) * (5 - j) - 1 higher values   
      # position <p> in symbols has rowno p // 4 + 1 and colno p % 4 + 1
      
      # for each symbol determine lower and higher values
      lh = [(symbols[p], (p // 4 + 1) * (p % 4 + 1) - 1, 
                         (4 - p // 4 ) * (4 - p % 4) - 1) for p in range(16)]
      
      # invalid digit / symbol assignments
      d2i, s2d = dict(), dict()
      for d in range(1, 17):
        vs = set()
        if d > 9: vs.update('ABCD')
        
        for s, lw, hi in lh:
          if d <= lw: vs.update(s)
          if d > 16 - hi: vs.update(s)
          
        d2i[d] = vs
        
      if 1:
        print("Allowed values:")
        for s in symbols:
          print(s, ":", end=" ")
          c, v = 0, 0
          for k, vs in d2i.items():
            if s not in vs:
              print(k, end=" ")
              c += 1
              v = k
          print()
          if c == 1:
            s2d[s] = v
      
      distinct = ''.join([x for x in symbols if x not in s2d])
      
      #  A B C D
      #  E F G H
      #  I J K L
      #  M N P Q
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
         "A < B",
         "B < F",
         "A < E",
         "E < F",
         "B < C",
         "C < G",
         "F < G",
         "C < D",
         "D < H",
         "G < H",
         
         # the top row gives a 4-digit product of the numbers in the second row
         "nconcat(A, B, C, D, base=10) == E * F * G * H",
         
         "E < I",
         "F < J",
         "I < J",
         "G < K",
         "J < K",
         "H < L",
         "K < L",
         "I < M",
         "J < N",
         "M < N",
         "K < P",
         "N < P",
         "L < Q",
         "P < Q",
        ],
        answer="((A, B, C, D), (E, F, G, H), (I, J, K, L), (M, N, P, Q))",
        base=17,
        d2i=d2i,
        s2d=s2d,
        distinct=distinct,
        digits=range(1, 17),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"{ans}")   
      
        
      Allowed values:
      A : 1
      B : 2 3 4 5
      C : 3 4 5 6 7 8 9
      D : 4 5 6 7 8 9
      E : 2 3 4 5
      F : 4 5 6 7 8
      G : 6 7 8 9 10 11
      H : 8 9 10 11 12 13 14
      I : 3 4 5 6 7 8 9
      J : 6 7 8 9 10 11
      K : 9 10 11 12 13
      L : 12 13 14 15
      M : 4 5 6 7 8 9 10 11 12 13
      N : 8 9 10 11 12 13 14
      P : 12 13 14 15
      Q : 16
      

      Like

    • GeoffR's avatar

      GeoffR 7:17 pm on 27 July 2023 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % ABCD
      % EFGH
      % IJKL
      % MNPQ
      
      % Numbers in the table
      var 1..16:A; var 1..16:B; var 1..16:C; var 1..16:D;
      var 1..16:E; var 1..16:F; var 1..16:G; var 1..16:H;
      var 1..16:I; var 1..16:J; var 1..16:K; var 1..16:L;
      var 1..16:M; var 1..16:N; var 1..16:P; var 1..16:Q;
      % Read the top row as one long number
      var 1000..9999:ABCD == 1000*A + 100*B + 10*C + D;
      
      constraint all_different([A,B,C,D,E,F,G,H,I,J,K,L,M,N,P,Q]);
      
      % The four numbers are increasing in each row
      constraint A < B /\ B < C /\ C < D;
      constraint E < F /\ F < G /\ G < H;
      constraint I < J /\ J < K /\ K < L;
      constraint M < N /\ N < P /\ P < Q;
      
      % The four numbers are increasing in each column
      constraint A < E /\ E < I /\ I < M;
      constraint B < F /\ F < J /\ J < N;
      constraint C < G /\ G < K /\ K < P;
      constraint D < H /\ H < L /\ L < Q;
      
      % If you read the top row as one long number,  
      % it is a four-figure number which equals the product 
      % of the four numbers in the second row.
      constraint ABCD == E * F * G * H;
      
      solve satisfy;
      output [" Four numbers in second row  are " ++ show([E, F, G, H])];
      
      % Four numbers in second row  are [2, 7, 8, 13]
      % ----------
      % ==========
      

      Like

  • Unknown's avatar

    Jim Randell 11:12 am on 25 July 2023 Permalink | Reply
    Tags:   

    Teaser 2631: Family progression 

    From The Sunday Times, 24th February 2013 [link] [link]

    I have five grandchildren: from oldest to youngest they are Imogen, Madeline, Alex, Matthew and Laurence. They were all born in different years and no two have a birthday in the same month. They were all born before noon and for each grandchild I have noted their time and date of birth as five numbers [in the following order]:

    minute, hour, DD, MM, YY

    (none of the numbers is zero, but they may have a leading zero).

    Surprisingly, for each grandchild the five numbers form an arithmetic progression (i.e. increasing or decreasing by a fixed amount), and also in each case the sum of two of the five numbers equals the sum of the other three.

    With the children in the order given above, list the months in which they were born.

    [teaser2631]

     
    • Jim Randell's avatar

      Jim Randell 11:13 am on 25 July 2023 Permalink | Reply

      If (mm, hh, DD, MM, YY) forms an arithmetic progression, then so does its reverse (YY, MM, DD, hh, mm) which is perhaps a more sensible order for date and time.

      This Python program considers possible birth date/times that form appropriate sequences (from that start of the year 2000 up to the date of publication of the puzzle). And then collects sequences from 5 different years that occur in 5 different months.

      It runs in 66ms. (Internal runtime is 539µs).

      Run: [ @replit ]

      from datetime import datetime
      from enigma import (irange, cproduct, div, catch, subsets, group, attr, join, printf)
      
      # (abbreviated) month names to use
      month = "??? Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()
      
      # only consider dates before this (= publication date of puzzle)
      M = datetime(2013, 2, 24)
      
      # generate possible birth dates
      def generate():
        # hh is 01-11, MM is 1-12
        # choose values for hh and MM
        for (hh, MM) in cproduct([irange(1, 11), irange(1, 12)]):
          # calculate the common difference
          d = div(MM - hh, 2)
          if not d: continue
          # fill in the remaining values
          (mm, DD, YY) = (hh - d, hh + d, MM + d)
          # does it form a valid date?
          if mm < 1 or YY < 1: continue
          t = catch(datetime, 2000 + YY, MM, DD, hh, mm)
          if t is None or not t < M: continue
          # calculate the sum of the progression
          s = 5 * mm + 10 * d
          # is the sum of 2 of the numbers equal to the sum of the other 3
          if s % 2 == 1: continue
          ns = (mm, hh, DD, MM, YY)
          if not any(2 * sum(ss) == s for ss in subsets(ns, size=2)): continue
          # viable date
          printf("[{t} = {ns}, d={d}, s={s}]")
          yield t
      
      # collect dates by year
      g = group(generate(), attr('year'))
      
      # select 5 birth years, oldest to youngest
      for ks in subsets(sorted(g.keys()), size=5):
        # select birthdates
        for ds in cproduct(g[k] for k in ks):
          # collect the months
          ms = list(d.month for d in ds)
          # there must be 5 different months
          if len(set(ms)) != 5: continue
          # output solution
          for (i, d) in enumerate(ds, start=1):
            printf("{i} -> {d}")
          printf("months = {ms}", ms=join((month[m] for m in ms), sep=", "))
          printf()
      

      Solution: The months are: March, June, May, July, October.

      And the birth date/times are:

      Imogen = 2002-03-04 05:06, seq = (06, 05, 04, 03, 02)
      Madeline = 2004-06-08 10:12, seq = (12, 10, 08, 06, 04)
      Alex = 2006-05-04 03:02, seq = (02, 03, 04, 05, 06)
      Matthew = 2008-07-06 05:04, seq = (04, 05, 06, 07, 08)
      Laurence = 2012-10-08 06:04, seq = (04, 06, 08, 10, 12)

      Like

    • Frits's avatar

      Frits 3:24 pm on 25 July 2023 Permalink | Reply

      Using the new “decl” setting to remember assignment variables when denesting is requested.

      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        if d > 1: vs.update('ACEGISTUVW')
        # as half the sum (2.5a + 5k) is an integer the year must be even
        if d % 2: vs.update('BDFHJ')
        # maximum difference between elements is three
        if not (0 < d < 4): vs.update('KLMNO')
      
        d2i[d] = vs
        
      # YY, MM, DD, hh, mm 
      # arithmetic progression: 
      # AB, AB + -1**S * K, AB + 2 * -1**S * K, .. , AB + 4 * -1**S * K
      
      # year, positive difference and sign
      vars = [("AB", "K", "S"), ("CD", "L", "T"), ("EF", "M", "U"), 
              ("GH", "N", "V"), ("IJ", "O", "W")]
      
      exprs, ms = [], ""
      # build expressions        
      for i, (a, k, s) in enumerate(vars):
        # decreasing ages
        exprs.append(("0" if not i else vars[i - 1][0]) + " < " + a + " <= " + str(8 + i))
        inc = "(-1)**" + s + " * " + k
        ms += a + " + " + inc + ", "
      
        exprs.append("0 < " + a + " + " +  inc + " <= 12")        # month
        exprs.append("0 < " + a + " + 3 * " + inc + " <= 11")     # hours
        exprs.append("0 < " + a + " + 4 * " + inc + " <= 59")     # minutes
        
        # make half the sum with highest and 1st or 2nd highest number
        y = "(" + s + " and 2.5 * " + a + " + 5 * " + inc 
        y += " in {2 * " + a + " + " + inc + ", 2 * " + a + " + 2 * " + inc + "}) or "
        y += "(not " + s + " and 2.5 * " + a + " + 5 * " + inc 
        y += " in {2 * " + a + " + 6 * " + inc + ", 2 * " + a + " + 7 * " + inc + "})"
        exprs.append(y)
        
      # born on 5 different months
      exprs.append("len(set(ms := [" + ms[:-1] +"])) == 5")
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        exprs=exprs,
        answer="ms, \
                (AB, AB + (-1)**S * K, AB + 2 * (-1)**S * K, AB + 3 * (-1)**S * K, AB + 4 * (-1)**S * K), \
                (CD, CD + (-1)**T * L, CD + 2 * (-1)**T * L, CD + 3 * (-1)**T * L, CD + 4 * (-1)**T * L), \
                (EF, EF + (-1)**U * M, EF + 2 * (-1)**U * M, EF + 3 * (-1)**U * M, EF + 4 * (-1)**U * M), \
                (GH, GH + (-1)**V * N, GH + 2 * (-1)**V * N, GH + 3 * (-1)**V * N, GH + 4 * (-1)**V * N), \
                (IJ, IJ + (-1)**W * O, IJ + 2 * (-1)**W * O, IJ + 3 * (-1)**W * O, IJ + 4 * (-1)**W * O)",
                
        d2i=d2i,
        distinct="",
        decl="global ms",     # new feature for remembering assignment variables
        denest=1,
        verbose=0,            # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"{ans[0]}")   
      

      Like

  • Unknown's avatar

    Jim Randell 11:24 am on 23 July 2023 Permalink | Reply
    Tags: by: Aubrey Jacobus   

    Brainteaser 1091: The nutty pirates 

    From The Sunday Times, 3rd July 1983 [link]

    Five pirates stole a sackful of nuts, and, taking them back to their lair, decided they would divide them equally the next day.

    During the night, the chief, not trusting the others, decided he would take his share first. He divided the nuts into five parts and there was one nut left over: this he gave to the pet monkey. Taking his share, he went back to bed.

    Each pirate in turn followed his example, i.e. dividing the remaining nuts into five parts, with a remainder of one on each occasion to be given to the monkey. Next morning, they found the remaining nuts were still divisible by five with one nut left over.

    Fortunately for this story, the pirates were able to count at a rate of no more than two nuts per second throughout the night.

    How many nuts were in the sack at the start?

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

    [teaser1091]

     
    • Jim Randell's avatar

      Jim Randell 11:25 am on 23 July 2023 Permalink | Reply

      See also: Enigma 258, Puzzle #86, Teaser (1956-12-13).

      Presumably we are looking for the smallest solution.

      A straightforward approach runs in 63ms. (Internal runtime is 953µs).

      Run: [ @replit ]

      from enigma import (irange, inf, ediv, printf)
      
      # suppose each pirate receives n nuts from the final pile
      for n in irange(1, inf):
        try:
          # then the size of the final pile is ...
          N5 = 5 * n + 1
          # the size of the previous pile ...
          N4 = ediv(5 * N5, 4) + 1
          # the size of the previous pile ...
          N3 = ediv(5 * N4, 4) + 1
          # the size of the previous pile ...
          N2 = ediv(5 * N3, 4) + 1
          # the size of the previous pile ...
          N1 = ediv(5 * N2, 4) + 1
          # the size of the initial pile
          N = ediv(5 * N1, 4) + 1
          # output solution
          printf("n={n} -> N={N}")
          break
        except ValueError:
          continue
      

      Solution: There were 15621 nuts in the sack at the start.

      The monkey gets 1 nut. Pirate 1 takes 3124 nuts, and leaves 12496.

      The monkey gets 1 nut. Pirate 2 takes 2499 nuts, and leaves 9996.

      The monkey gets 1 nut. Pirate 3 takes 1999 nuts, and leaves 7996.

      The monkey gets 1 nut. Pirate 4 takes 1599 nuts, and leaves 6396.

      The monkey gets 1 nut. Pirate 5 takes 1279 nuts, and leaves 5116.

      The monkey gets 1 nut. The pirates each take 1023 nuts.

      So the nuts for each pirate are:

      Pirate 1: 3124 + 1023 = 4147
      Pirate 2: 2499 + 1023 = 3522
      Pirate 3: 1999 + 1023 = 3022
      Pirate 4: 1599 + 1023 = 2622
      Pirate 5: 1279 + 1023 = 2302
      Monkey: 1 + 1 + 1 + 1 + 1 + 1 = 6
      Total: 4147 + 3552 + 3022 + 2622 + 2302 + 6 = 15651

      The number of nuts counted in the night is:

      15621 + 12496 + 9996 + 7996 + 6396 = 52505

      Which at a rate of 2/sec is 26252.5 seconds = 437.54 minutes = 7.29 hours.


      In general with M pirates the initial number of nuts is:

      N = k.(M^M) − M + 1

      And after each of the pirates had performed their procedure the remaining number of nuts is:

      R = k.((M − 1)^M) − M + 1

      In this case we have M = 5, and (R − 1) is divisible by M.

      k=1: N = 3121, R = 1020, (not viable)
      k=2: N = 6246, R = 2044, (not viable)
      k=3: N = 9371, R = 3068, (not viable)
      k=4: N = 12496, R = 4092, (not viable)
      k=5: N = 15621, R = 5116, (viable)

      And we get viable solutions whenever k is a multiple of 5, so the next smallest solution is:

      k=10: N = 31246, R = 10236

      The total number of nuts counted would be 115276, which would take more than 16 hours to count, which could be done if the pirates operated somewhere with sufficiently long nights (which would require them being at a latitude beyond 80°N/S, so is unlikely).

      Like

  • Unknown's avatar

    Jim Randell 4:32 pm on 21 July 2023 Permalink | Reply
    Tags:   

    Teaser 3174: Pensioners’ outing 

    From The Sunday Times, 23rd July 2023 [link] [link]

    A group of pensioners took a trip in a minibus, which had 11 seats for passengers, three on the back row, and two on each of the four other rows. The ages in years of the passengers were all different double-digit integers greater than 64, and for each pair of those ages there was no common factor other than 1. All seats were occupied, and, on any particular row of seats, the digits in the ages of passengers were all different. The sum of the ages of passengers on the back row was the largest possible with these conditions.

    What was the age in years of the most elderly passenger sitting on the back row?

    [teaser3174]

     
    • Jim Randell's avatar

      Jim Randell 5:10 pm on 21 July 2023 Permalink | Reply

      This Python program runs in 65ms. (Internal run time is 4ms).

      Run: [ @replit ]

      from enigma import (irange, subsets, gcd, is_duplicate, printf)
      
      # possible ages (no repeated digits)
      ages = list(n for n in irange(65, 99) if n % 11 > 0)
      
      # find pairs of ages with different digits that are coprime
      pairs = list((x, y) for (x, y) in subsets(ages, 2) if gcd(x, y) == 1 and not is_duplicate(x, y))
      
      # extend the pairs to triples
      triples = list()
      for (x, y) in pairs:
        for z in ages:
          if z > y and gcd(x, z) == 1 and gcd(y, z) == 1 and not is_duplicate(x, y, z):
            triples.append((x, y, z))
      
      # choose <k> more pairs from <ps>
      def choose(k, ps, ns, rs):
        if k == 0:
          yield rs
        else:
          # choose the next pair
          for (i, (a, b)) in enumerate(ps):
            # check numbers are all different
            if a in ns or b in ns: continue
            # and are pairwise coprime
            if not all(gcd(a, n) == 1 and gcd(b, n) == 1 for n in ns): continue
            # solve for remaining pairs
            yield from choose(k - 1, ps[i + 1:], ns + (a, b), rs + [(a, b)])
      
      # solve the puzzle
      def solve():
        # consider back rows in order (largest sum to smallest sum)
        for r1 in sorted(triples, key=sum, reverse=1):
          # find 4 more pairs to go with these
          yield from choose(4, pairs, r1, [r1])
      
      # find the first solution
      for rs in solve():
        printf("rows = {rs}")
        break  # we only need one solution
      

      Solution: The age of the eldest passenger on the back row is 95.

      The ages in the rows are:

      (73, 86, 95) (back row)
      (67, 91)
      (71, 89)
      (79, 81) or (79, 83)
      (83, 97) or (81, 97)

      The sum of the ages in the back row being 254.

      Like

    • Frits's avatar

      Frits 1:52 pm on 24 July 2023 Permalink | Reply

      from math import gcd
      
      diffdgts = lambda *ns: len(set(s := ''.join(str(n) for n in ns))) == len(s)
      
      # check that element <a> is coprime with elements in <s>
      cp = lambda a, s: all(gcd(a, x) == 1 for x in s)
                        
      # decompose <t> into <k> increasing numbers from <ns> with all different digits
      def decompose(t, k, ns, s=[]):
        if k == 1:
          if t in ns and t > s[-1] and diffdgts(*(s_ := s + [t])):
            yield s_
        else:
          for (i, n) in enumerate(ns):
            if not diffdgts(*(s_ := s + [n])): continue
            yield from decompose(t - n, k - 1, ns[i + 1:], s_)
      
      #  A B
      #  C D
      #  E F
      #  G H
      # I J K
      
      sols = set()
      # check sum of ages from (90+80+70+6+5+3) to (80+70+60+0+1+3)
      for soa in range(254, 213, -1):
        # back row
        for I, J, K in decompose(soa, 3, [x for x in range(65, 99) if x % 11]):
          # check if I, J and K are coprime
          if not cp(I, [J]): continue
          if not cp(K, [I, J]): continue
          
          for A in range(65, 85):
            if not cp(A, sA := [I, J, K]):  continue
            for B in range((A // 10 + 1) * 10, 99):
              if not cp(B, sB := sA + [A]) or not diffdgts(A, B): continue
              for C in range(A + 1, 86):
                if not cp(C, sC := sB + [B]):  continue
                for D in range((C // 10 + 1) * 10, 99):
                  if not cp(D, sD := sC + [C]) or not diffdgts(C, D): continue
                  for E in range(C + 1, 87):
                    if not cp(E, sE := sD + [D]):  continue
                    for F in range((E // 10 + 1) * 10, 99):
                      if not cp(F, sF := sE + [E]) or not diffdgts(E, F): continue
                      for G in range(E + 1, 88):
                        if not cp(G, sG := sF + [F]):  continue
                        for H in range((G // 10 + 1) * 10, 99):
                          if not cp(H, sG + [G]) or not diffdgts(G, H): continue
                          # don't assume there is a unique answer
                          sols.add(K)
                  
        if sols:
          print(f"answer: {', '.join(str(s) for s in sols)} in years")
          break   # we don't need to check a lower sum of ages   
      

      Like

  • Unknown's avatar

    Jim Randell 8:51 am on 20 July 2023 Permalink | Reply
    Tags:   

    Teaser 2633: Magic mushrooms 

    From The Sunday Times, 10th March 2013 [link] [link]

    Enid and her famous five (Anne, Dick, George, Julian and Timmy) were joined by Pixie in a mushroom hunt. They each picked two varieties, the fourteen overall being:

    Bird’s Nest, Beef Steak, Blue Toothed, Cannon Ball, Death Cap, Devil’s Um, Inky Cap, Old Man of the Woods, Parasol, Poison Pie, Stinkhorn, Slippery Jack, Tree Ears and The Gypsy.

    For each of these hunters, if you wrote down their name and the two varieties of mushroom they picked, then for any two from the three you would find that there were just two letters of the alphabet that occurred in both.

    What mushrooms were picked by: (a) Pixie and (b) Enid?

    It seems likely that “Devil’s Um” is a typo for “Devil’s Urn” (which is a type of mushroom), however the answer to the puzzle is the same whichever you use, so I have stuck with the spelling used in the original puzzle and in the 2020 book.

    Also, in Enid Blyton’s Famous Five, Timmy is a dog.

    [teaser2633]

     
    • Jim Randell's avatar

      Jim Randell 8:52 am on 20 July 2023 Permalink | Reply

      The [[ grouping ]] functionality in the enigma.py library was specifically written for puzzles like these.

      The following Python program runs in 61ms. (Internal runtime is 909µs).

      Run: [ @replit ]

      from enigma import grouping
      
      names = ["Enid", "Anne", "Dick", "George", "Julian", "Timmy", "Pixie"]
      
      picks = [
        "Bird's Nest", "Beef Steak", "Blue Toothed", "Cannon Ball", "Death Cap",
        "Devil's Um", "Inky Cap", "Old Man of the Woods", "Parasol",
        "Poison Pie", "Stinkhorn", "Slippery Jack", "Tree Ears", "The Gypsy"
      ]
      
      # form 2-gangs
      for gs in grouping.gangs(2, names, picks, grouping.share_letters(2)):
        # output the groups
        grouping.output_gangs(names, gs)
      

      Solution: (a) Pixie picked Devil’s Um/Urn and The Gypsy. (b) Enid picked Blue Toothed and Slippery Jack.

      The full solution is:

      Enid: Blue Toothed, Slippery Jack [de, ei, el]
      Anne: Beef Steak, Cannon Ball [ae, an, ab]
      Dick: Death Cap, Stinkhorn [cd, ik, ht]
      George: Poison Pie, Tree Ears [eo, er, es]
      Julian: Bird’s Nest, Parasol [in, al, rs]
      Timmy: Inky Cap, Old Man of the Woods [iy, mt, an]
      Pixie: Devil’s Um/Urn, The Gypsy [ei, ep, es]

      (Common letters are listed as: [namepick1, namepick2, pick1pick2]).

      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