Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:48 am on 29 September 2022 Permalink | Reply
    Tags:   

    Teaser 2747: Marble jar 

    From The Sunday Times, 17th May 2015 [link] [link]

    At our local fete one of the games consisted of guessing the number of marbles in a jar: some of the marbles were red and the rest were blue. People had to guess how many there were of each colour.

    The organiser gave me a couple of clues. Firstly, he told me that there were nearly four hundred marbles altogether. Secondly, he told me that if, when blindfolded, I removed four marbles from the jar, then the chance that they would all be red was exactly one in a four-figure number.

    How many red marbles were there, and how many blue?

    [teaser2747]

     
    • Jim Randell's avatar

      Jim Randell 8:49 am on 29 September 2022 Permalink | Reply

      If there are T marbles in total (T = “nearly 400”) and R of them are red, then the probability of removing 4 red marbles is:

      P = (R / T) × ((R − 1) / (T − 1)) × ((R − 2) / (T − 2)) × ((R − 3) / (T − 3))
      P = (R × (R − 1) × (R − 2) × (R − 3)) / (T × (T − 1) × (T − 2) × (T − 3))

      And this probability is “one in a four-figure number”, so the largest it can be is 1/1000 and the smallest is 1/9999.

      The following Python program considers total numbers of marbles from 399 down to 350, and then looks for numbers of red marbles that give an appropriate value for P.

      It runs in 57ms. (Internal run time is 1.2ms).

      Run: [ @replit ]

      from enigma import (irange, printf)
      
      # consider total number of marbles (nearly 400)
      for T in irange(399, 350, step=-1):
        # denominator of P
        d = T * (T - 1) * (T - 2) * (T - 3)
        # R = number of red marbles
        for R in irange(4, T):
          # numerator of P
          n = R * (R - 1) * (R - 2) * (R - 3)
          # calculate p = 1/P
          (p, x) = divmod(d, n)
          if p > 9999: continue
          if p < 1000: break
          if x == 0:
            # output solution
            printf("red = {R}, blue = {B}; total = {T} -> P = 1/{p}",B=T - R)
      

      Solution: There were 45 red marbles and 342 blue marbles.

      So, 387 marbles in total. And the probability of choosing 4 red is 1/6176.

      Like

  • Unknown's avatar

    Jim Randell 8:45 am on 27 September 2022 Permalink | Reply
    Tags:   

    Teaser 2749: Round the river 

    From The Sunday Times, 31st May 2015 [link] [link]

    My school holds “Round the river” runs — a whole number of metres to a bridge on the river and then the same number of metres back. Some years ago I took part with my friends Roy, Al, David and Cy. We each did the outward half at our own steady speeds (each being a whole number of centimetres per minute). For the return half I continued at my steady speed, Roy increased his speed by 10%, Al increased his speed by 20%, David increased his by 30%, and Cy increased his by 40%. We all finished together in a whole number of minutes, a little less than half an hour.

    What (in metres) is the total length of the race?

    [teaser2749]

     
    • Jim Randell's avatar

      Jim Randell 8:46 am on 27 September 2022 Permalink | Reply

      The speeds are whole numbers of centimetres per minute, so we will work in units of centimetres and minutes.

      If the distance to bridge is x centimetres, then x must be divisible by 100.

      And the time is t minutes (less than 30).

      If the 5 speeds are: a, b, c, d, e, then we have:

      t = x/a + x/a = 2x / a
      t = x/b + x/1.1b = 21x / 11b
      t = x/c + x/1.2c = 11x / 6c
      t = x/d + x/1.3d = 23x / 13d
      t = x/e + x/1.4e = 12x / 7e

      From which we see that x must also be divisible by 11, 6, 13, 7.

      Placing some sensible limits on distance and speeds, we can suppose x is less than 10km (= 10,000m = 1,000,000cm), and that speeds are less than 15mph (≈ 40,000cm/min).

      This Python program runs in 61ms. (Internal runtime is 129µs).

      Run: [ @replit ]

      from enigma import (irange, mlcm, div, printf)
      
      # x must be a multiple of m
      m = mlcm(100, 11, 6, 13, 7)
      
      # consider possible total times: "a little less than half an hour"
      for t in irange(29, 21, step=-1):
      
        # consider possible values of x (up to 10km)
        for x in irange(m, 1000000, step=m):
      
          # calculate the speeds
          vs = [
            div(20 * x, 10 * t),
            div(21 * x, 11 * t),
            div(22 * x, 12 * t),
            div(23 * x, 13 * t),
            div(24 * x, 14 * t),
          ]
          # check speeds
          if None in vs or vs[-1] * 1.4 > 40000: continue
      
          # output solution
          printf("d={d} metres [t={t} x={x} {vs}]", d=div(2 * x, 100))
      

      Solution: The total length of the race is 6006m.

      And the race took exactly 25 minutes.

      The outward speeds are:

      1: 24024 cm/min (≈ 8.96 mph); 12.5 min out + 12.5 min back (@ 8.96 mph)
      2: 22932 cm/min (≈ 8.55 mph); 13.1 min out + 11.9 min back (@ 9.40 mph)
      3: 22022 cm/min (≈ 8.21 mph); 13.6 min out + 11.4 min back (@ 9.85 mph)
      4: 21252 cm/min (≈ 7.92 mph); 14.1 min out + 10.9 min back (@ 10.30 mph)
      5: 20592 cm/min (≈ 7.68 mph); 14.6 min out + 10.4 min back (@ 10.75 mph)

      Note that the speeds on the return leg are not all whole numbers of cm/min.

      Like

  • Unknown's avatar

    Jim Randell 10:53 am on 25 September 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 418: Bell’s weights 

    From The Sunday Times, 11th May 1969 [link]

    “Now”, says Bell at the pub, “look intelligent and imaginative even if you don’t look beautiful by any means”. We swallow the insult and look solemn. Bell likes his jokes and we like his puzzles.

    “Imagine you have some scales, but only three weights. However, you have a heap of Grade A sand, and a couple of bags; so you make two extra weights, one as heavy as all the first three put together, and the other twice as heavy as all the first three. Whereupon all the remaining sand is removed to a great distance”.

    “With these five weights you must be able to weigh 1 ounce, 2 ounces, 3, 4, and so on, as far as possible. No gaps in that lot. It’s how far you can to the first gap I’m after. Usual prize — one pint for the best score before closing time”.

    What should Bell’s five weights be to give the highest possible score?

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

    [teaser418]

     
    • Jim Randell's avatar

      Jim Randell 10:54 am on 25 September 2022 Permalink | Reply

      This Python program considers increasing values for the total of the first 3 weights, from 3 to 40.

      It runs in 351ms.

      Run: [ @replit ]

      from enigma import (irange, inf, decompose, subsets, peek, Accumulator, printf)
      
      def unreachable(ws):
        # collect possible weights
        vs = set()
        # choose multipliers for each weight
        for ms in subsets((1, 0, -1), size=len(ws), select="M"):
          v = sum(m * w for (m, w) in zip(ms, ws))
          if v > 0: vs.add(v)
        # find the first unreachable value
        return peek(v for v in irange(1, inf) if v not in vs)
      
      # record maximum unreachable weight
      r = Accumulator(fn=max, collect=1)
      
      # consider t = a + b + c
      for t in irange(3, 40):
        for (a, b, c) in decompose(t, 3, increasing=1, sep=0, min_v=1):
          # calculate the first unreachable value
          ws = (a, b, c, t, 2 * t)
          v = unreachable(ws)
          r.accumulate_data(v, ws)
          if v == r.value: printf("[t={t}: {ws} -> unreachble = {v}]")
      
      printf("values = 1 .. {x}", x=r.value - 1)
      for ws in r.data:
        printf("-> {ws}")
      

      Solution: The 5 weights are: 4, 6, 9, 19, 38.

      And this set of weights can be used to weigh all values from 1 to 64.


      Using a set of balancing scales each weight can go in the left pan, right pan, or neither.

      For for n weights there are 3^n possibilities.

      But these include, not using any weights (all in neither pan), and each combination has a mirror image.

      So the total maximum possible number of different weights would be:

      W(n) = (3^n − 1) / 2

      For 5 weights we have: W(5) = 121, and using a set consisting of increasing powers of 3 we can achieve this maximum and weigh all values between 1 and 121:

      1, 3, 9, 27, 81

      But for a set of the form:

      (a, b, c, a + b + c, 2(a + b + c))

      there are 70 different arrangements. So the maximum number of different values we could achieve would be no more than this. And we can use the program to perform an exhaustive check up to this total, but there are no better solutions.

      Like

  • Unknown's avatar

    Jim Randell 4:33 pm on 23 September 2022 Permalink | Reply
    Tags:   

    Teaser 3131: Heads up savings 

    From The Sunday Times, 25th September 2022 [link] [link]

    Little Spencer saves 5p coins in a jar, and when they reach £10, deposits them in his savings account. He likes playing with the coins. In one game, after first turning them all heads up, he places them in a row on the table. Starting from the left, he then turns over every 2nd coin until he has reached the end of the row. He then again starts from the left, and this time turns over every 3rd coin. He repeats this for every 4th, 5th coin etc, until finally he turned over just one coin, the last in the row.

    At the end of the game I could see that if Spencer had exactly 75 per cent more coins he would have an increase of 40 per cent in the number showing heads. However, if he had exactly 50 per cent fewer coins, he would have a decrease of 40 per cent in the number showing heads.

    What is the value of his 5p savings?

    There are now 750 Teaser puzzles available on the S2T2 site.

    [teaser3131]

     
    • Jim Randell's avatar

      Jim Randell 5:09 pm on 23 September 2022 Permalink | Reply

      (See also: Puzzle #08).

      Here is a constructive solution.

      It runs in 52ms. (Internal runtime is 409µs).

      Run: [ @replit ]

      from enigma import (irange, csum, div, printf)
      
      # play the game with <n> coins
      def coins(n):
        # each coin starts out showing heads = 1
        vs = [1] * n
        # allow the coins to be indexed from 1
        vs.insert(0, None)
        # every <k> coins
        for k in irange(2, n):
          # flip coin k, 2k, 3k, ....
          for i in irange(k, n, step=k):
            vs[i] ^= 1
        vs.pop(0)
        return vs
      
      # it is enough to calculate one sequence, and then use prefixes
      heads = list(csum(coins(350), empty=1))
      
      # consider initial number of coins
      for n in irange(1, 200):
        # we need 1.75 times the number of coins, and 0.5 times the number
        n1 = div(175 * n, 100)
        n2 = div(50 * n, 100)
        if n1 is None or n2 is None: continue
      
        # h1 is 1.4 times h; h2 is 0.6 times h
        (h, h1, h2) = (heads[x] for x in (n, n1, n2))
        if not (100 * h1 == 140 * h and 100 * h2 == 60 * h): continue
      
        # output solution
        printf("{n} coins = {h} heads; {n1} coins = {h1} heads; {n2} coins = {h2} heads")
      

      Solution: The total value of the coins is £1.40.

      Spencer has 28 coins.

      After performing the procedure there are 5 coins remaining heads up (= coins 1, 4, 9, 16, 25).

      If he had 1.75× the number of coins (= 49 coins), then 7 would remain heads up (= coins 1, 4, 9, 16, 25, 36, 49).

      And if he had 0.5× the number of coins (= 14 coins), then 3 would remain heads up (= coins 1, 4, 9).

      Like

      • Jim Randell's avatar

        Jim Randell 5:46 pm on 23 September 2022 Permalink | Reply

        Using the fact that the coins that remain heads up are exactly those in the perfect square numbered positions (numbering from 1), we can get a shorter (and faster) program.

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

        Run: [ @replit ]

        from enigma import (irange, isqrt, div, printf)
        
        # play the game with <n> coins, return the number of heads
        heads = isqrt
        
        # consider initial number of coins
        for n in irange(1, 200):
          # we need 1.75 times the number of coins, and 0.5 times the number
          n1 = div(175 * n, 100)
          n2 = div(50 * n, 100)
          if n1 is None or n2 is None: continue
        
          # h1 is 1.4 times h, h2 is 0.6 times h
          (h, h1, h2) = (heads(x) for x in (n, n1, n2))
          if not (100 * h1 == 140 * h and 100 * h2 == 60 * h): continue
        
          # output solution
          printf("{n} coins = {h} heads; {n1} coins = {h1} heads; {n2} coins = {h2} heads")
        

        Like

    • NigelR's avatar

      NigelR 10:50 am on 26 September 2022 Permalink | Reply

      Irrespective of the number of times coin n in the row is flipped, its final H/T depends on whether n has an odd or even number of factors. (I hadn’t spotted the elegance of the perfect squares!).
      PS: I think the answer sought was actually the value of the coins, not the number.

      
      # Test whether n has an even number of factors
      countfac = lambda n: True if [1 if n % i == 0 else 0 for i in range(1, (n//2)+1)].count(1) %2==0 else False
      heads={x:1 for x in range(1,200)} #initialise heads as dictionary 
      for x in range(2,200):
          heads[x]=heads[x-1]
          if countfac(x): heads[x]+=1  #heads[n] now has cumulative number of heads for row of n coins
      #test for output conditions. Number of coins must be divisible by 4 if 75% greater is integer 
      for x in range(4,200,4):
          if x*1.75>200:continue
          y=int(x*1.75)
          z=int(x/2)
          if heads[y]!=heads[x]*1.4:continue
          if heads[z]!=heads[x]*0.6:continue
          value = divmod(5*x,100)
          print(x ,'coins in row, ',heads[x],'of which are heads. Value of savings is £',value[0],'and',value[1],'p')
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:24 pm on 26 September 2022 Permalink | Reply

        @NigelR: I left determining the total value of a certain number of 5p coins as a simple exercise for the reader ;-).

        You could shorten this line of code somewhat:

        countfac = lambda n: True if [1 if n % i == 0 else 0 for i in range(1, (n // 2) + 1)].count(1) % 2 == 0 else False
        

        (True if ... else False) is just the same as evaluating ... (in a boolean context):

        countfac = lambda n: [1 if n % i == 0 else 0 for i in range(1, (n // 2) + 1)].count(1) % 2 == 0
        

        and then in the list comprehension, (1 if ... else 0) is also the same as ... (in a boolean context; in Python False and True are just 0 and 1 in disguise):

        countfac = lambda n: [n % i == 0 for i in range(1, (n // 2) + 1)].count(1) % 2 == 0
        

        and we don’t need to construct the list, just to count the number of 1’s in it:

        countfac = lambda n: sum(n % i == 0 for i in range(1, (n // 2) + 1)) % 2 == 0
        

        Also note that Python’s builtin range function does not include the endpoint. So if you want to go to 350 in line 4 you need to specify a stop value of 351. Similarly in line 8, to check up to 200 coins you need to specify a stop value of 201.

        (I tend to use the inclusive irange() function from the enigma.py library, which includes both endpoints, to avoid this kind of “fencepost” error).

        Like

        • NigelR's avatar

          NigelR 8:40 pm on 26 September 2022 Permalink | Reply

          JIm: Thank you so much for taking the time to unpick my messy code and for your very helpful advice – your countfac is much simpler and I’m now wiser! I couldn’t work out what on earth I’d done in the lambda an hour after writing it!
          Agree on the stop value of 351, but I did think about the 200/201 and concluded that Spencer would have banked the lot if it had reached 200, and hence he would only have been playing with up to 199 coins. Perhaps I’m overthinking it!

          Like

        • Jim Randell's avatar

          Jim Randell 10:08 am on 27 September 2022 Permalink | Reply

          Yes the (200, 350) case is a bit of a grey area. I decided he might like one last play with his coins before he banked them, so I might as well check it, as I prefer solutions that exhaustively explore the solution space.

          As it turns out it doesn’t provide an answer, so it doesn’t really matter if you check it or not.

          Like

    • NigelR's avatar

      NigelR 10:58 am on 26 September 2022 Permalink | Reply

      … and,of course, the 75% greater number is only hypothetical and hence can be greater than 200. My line 4 should go to 350, and line 10 is unnecessary.

      Like

  • Unknown's avatar

    Jim Randell 9:38 am on 22 September 2022 Permalink | Reply
    Tags:   

    Teaser 2742: Hymns bored 

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

    Peter became bored during the Sunday service, so his mind turned to the three three-figure hymn numbers displayed on the board, all chosen from the five hundred hymns in the hymnal. He noticed that the sum of the digits for each hymn was the same, that one hymn number was the average of the other two, and that no digit appeared more than once on the board.

    What (in increasing order) were the three hymn numbers?

    [teaser2742]

     
    • Jim Randell's avatar

      Jim Randell 9:39 am on 22 September 2022 Permalink | Reply

      We can treat this as an alphametic puzzle, and use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve the puzzle.

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

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # if the hymn numbers are: ABC, DEF, GHI
      --answer="(ABC, DEF, GHI)"
      
      # the hymns are in order
      "A < D < G"
      
      # hymns are numbered from 1 to 500
      "G < 5"
      
      # each hymn has the same digit sum
      "all_same(A + B + C, D + E + F, G + H + I)"
      
      # one hymn number is the average of the other two: DEF = (ABC + GHI) / 2
      "div(ABC + GHI, 2) = DEF"
      
      # allow leading zeros
      --invalid=""
      

      Solution: The hymn numbers are: 157, 283, 409.

      Like

    • GeoffR's avatar

      GeoffR 12:00 pm on 22 September 2022 Permalink | Reply

      
      from itertools import permutations
      
      for p1 in permutations('1234567890', 9):
          A, B, C, D, E, F, G, H, I = p1
          if '0' in (A, D, G):continue
          
          # check digit sums are same for three numbers
          total = int(A) + int(B) + int(C)
          if int(D) + int(E) + int(F) != total:continue
          if int(G) + int(H) + int(I) != total:continue
          
          # find three hymn numbers in increasing order
          ABC, DEF = int(A + B + C), int(D + E + F)
          GHI = int(G + H + I)
          if not ABC < DEF < GHI:continue
          # check all hymn numbers are less than 500
          if not all( x < 500 for x in (ABC, DEF, GHI)):continue
          
          # One hymn number was the average of the other two
          if 2 * DEF == ABC + GHI:
              print(f"Three hymn numbers are {ABC}, {DEF} and {GHI}.")
      
      # Three hymn numbers are 157, 283 and 409.  
      
      

      Like

    • GeoffR's avatar

      GeoffR 1:23 pm on 22 September 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 0..9:B; var 0..9:C;
      var 1..9:D; var 0..9:E; var 0..9:F;
      var 1..9:G; var 0..9:H; var 0..9:I;
      
      constraint all_different([A,B,C,D,E,F,G,H,I]);
      
      var 100..500:ABC = 100*A + 10*B + C;
      var 100..500:DEF = 100*D + 10*E + F;
      var 100..500:GHI = 100*G + 10*H + I;
      
      constraint A + B + C == D + E + F /\ A + B + C == G + H + I;
      constraint ABC < DEF /\ DEF < GHI;
      constraint 2 * DEF == ABC + GHI;
      
      solve satisfy;
      output ["Three hymn numbers were " ++ show(ABC) ++ ", " ++ show(DEF) 
      ++ " and " ++ show(GHI) ];
      
      % Three hymn numbers were 157, 283 and 409
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:37 am on 20 September 2022 Permalink | Reply
    Tags:   

    Teaser 2744: The school run 

    From The Sunday Times, 26th April 2015 [link] [link]

    Each of the three houses of Merryhouse School entered four students in the cross-country race. Points were awarded with 12 for the winner, 11 for second, and so on down to 1 for the tail-ender (from Berry House). When the points were added up, all houses had equal points. Three of the runners from Cherry House were in consecutive positions, as were just the two middle-performers from Derry House.

    Which house did the winner come from, and what were the individual scores of its runners?

    [teaser2744]

     
    • Jim Randell's avatar

      Jim Randell 7:38 am on 20 September 2022 Permalink | Reply

      There are tri(12) = 78 points awarded, so each of the three houses gets 26 points.

      We can treat this puzzle as an alphametic problem in base 13, distributing values 1-12 to each of the runners, and use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve it.

      When the puzzle says that 3 from team C and 2 from team D are consecutive, I assumed that this implies that exactly the specified number are consecutive for those teams. (For a looser interpretation we can change the != at line 31 to an or).

      The following run file executes in 67ms. (The internal run time of the generated program is 265µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # suppose points are:
      #
      #       1  2  3  4
      #  B =  E  F  G  H
      #  C =  I  J  K  L
      #  D =  M  N  P  Q
      
      SubstitutedExpression
      
      # allocate points 1-12
      --base=13
      --digits=1-12
      
      # teams are in order
      "E > F > G > H"  # team B
      "I > J > K > L"  # team C
      "M > N > P > Q"  # team D
      
      # team B has the tail-ender
      "H = 1"
      
      # each team had the same number of points (= 26)
      "E + F + G + H == 26"
      "I + J + K + L == 26"
      "M + N + P + Q == 26"
      
      # team C has 3 consecutive points
      "J == K + 1"
      "(I == J + 1) != (K == L + 1)"
      
      # team D has middle 2 consecutive points
      "N == P + 1"
      "M > N + 1"
      "P > Q + 1"
      
      --template=""
      

      Solution: The winner was from Derry House, which was awarded 12 + 6 + 5 + 3 points.

      The points awarded are:

      B: 11 + 10 + 4 + 1 = 26
      C: 9 + 8 + 7 + 2 = 26
      D: 12 + 6 + 5 + 3 = 26

      Like

  • Unknown's avatar

    Jim Randell 10:09 am on 18 September 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 560: Ribbon counter 

    From The Sunday Times, 26th March 1972 [link]

    “Puzzle here”, says Bell at the pub. “Chap has a ribbon shop, sells the stuff by the inch, no commercial sense. He’s barmy anyway; look how he measures it. His counter is exactly 100 inches long and he’s marked it off into 16 bits; 6 of 11 inches, 2 of 6 inches, 3 of 5 inches, 1 of 3 inches and 4 of 1 inch, and he can measure any number of inches up to a hundred, that is, by picking the right pair of marks.

    “You have to sort the spaces out; but I’ll tell you, all the 11 inches are together round about the middle — well, a bit to the right, but not as much as 4 inches off centre. You get the idea? For most measurements he’s using a kind of feet and inches with eleven inches to the foot”.

    “Young Green is nearly right: he can’t measure 99 inches unless there’s a 1-inch space at one end, but he doesn’t need a 1-inch at the other end for 98 inches; he does it with two 1-inch spaces at the same end; but there might be a 1-inch at the other end, all the same, and there might not”.

    “In answer to two foolish questions, the ribbon must be measured single thickness, no folding; and it’s a straight counter, it’s not circular”.

    “Usual prize, one pint”.

    How were the spaces arranged from left to right?

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

    [teaser560]

     
    • Jim Randell's avatar

      Jim Randell 10:10 am on 18 September 2022 Permalink | Reply

      The puzzle is describing a sparse ruler of length 100 with 17 marks. (See: Teaser 119).

      However, in this case we are told that one end of the ruler has two 1-segments (lets put them at the start), and the six 11-segments all occur together in the middle-ish (displaced slightly to the right, but not by more than 3 inches), so we can look for arrangements of the remaining segments that give a viable ruler.

      This Python program runs in 104ms. (Internal runtime is 49ms).

      Run: [ @replit ]

      from enigma import (multiset, subsets, csum, reverse, printf)
      
      # segment groups we are given
      ss1 = (1, 1)  # start with (1, 1)
      ss2 = (11,) * 6  # 11s are in the middle
      
      # remaining segments
      segs = multiset.from_dict({ 6: 2, 5: 3, 3: 1, 1: 2 })
      n = segs.size()
      
      # consider the remaining segments
      for rs in subsets(segs, size=n, select="mP"):
        # accumulate sufficient segments to place the 11s close to the middle
        (t, i) = (rs[0] + 35, 1)
        while True:
          t += rs[i]
          i += 1
          if t < 47 or t == 50: continue
          if t > 53: break
          # construct the sequence of segments
          ss = ss1 + rs[:i] + ss2 + rs[i:]
          # construct the sequence of marks
          ms = list(csum(ss, empty=1))
          # check all 100 differences are achievable
          if len(set(b - a for (a, b) in subsets(ms, size=2))) == 100:
            # do we want the mirror image?
            if t < 50: (ss, ms) = (reverse(ss), reverse(ms))
            # output segments and marks
            printf("{ss} -> {ms}")
      

      Solution: The segments are as follows:

      (1, 1, 3, 5, 5, 5, 11, 11, 11, 11, 11, 11, 6, 6, 1, 1)

      The centre of the 11’s is 53 inches from the left edge.


      Using the code I wrote for Teaser 119 we find there are only 2 sparse rulers of length 100 with 17 marks (and it is not possible to construct a length 100 ruler with fewer marks):

      (0, 1, 2, 5, 10, 15, 20, 31, 42, 53, 64, 75, 86, 92, 98, 99, 100)
      (0, 1, 2, 8, 14, 25, 36, 47, 58, 69, 80, 85, 90, 95, 98, 99, 100)

      {+1^2, +3, +5^3, +11^6, +6^2, +1^2}
      {+1^2, +6^2, +11^6, +5^3, +3, +1^2}

      The second being the mirror image of the first (which is clear from the representation in difference format).

      Like

      • Frits's avatar

        Frits 11:38 am on 19 September 2022 Permalink | Reply

        @Jim, you still have to put the latest version of enigma.py to the magwag site.

        Like

        • Jim Randell's avatar

          Jim Randell 3:57 pm on 19 September 2022 Permalink | Reply

          I’ve uploaded enigma.py version 2022-09-17, which has all my recent changes in it.

          The most interesting change is that SubstitutedExpression.split_sum() can now take multiple simultaneous sums to split. In general it is now faster to use split_sum() than it is to use the specialised SubstitutedSum solver.

          Like

    • Frits's avatar

      Frits 3:15 pm on 19 September 2022 Permalink | Reply

      Similar reusing parts of Jim’s code.

         
      from itertools import permutations
      
      # put bits (1, 1) up front 
      
      # list of numbers to use besides (1, 1) and (11, 11, 11, 11, 11, 11)
      bits = (1, 1, 3, 5, 5, 5, 6, 6)
      
      # segment groups we are given
      ss1 = (1, 1)     # start with (1, 1)
      ss2 = (11,) * 6  # 11s are in the middle
      
      ps = set()
      target = list(range(1, 101))
      
      for p in permutations(bits):
        if p in ps: continue
        # determine where to put the six 11s
        t = 2    # first 2 bits
        for i, b in enumerate(p):
          t += b
          # 1 to 3 inches off centre (to the right) meaning 51 - 53
          if t < 18: continue
          if t > 20: break
          
          # construct the sequence of segments
          ss = ss1 + p[:i+1] + ss2 + p[i+1:]
          
          # collect lenghts of all possible consecutive bits
          ls = set(sum(ss[i:j]) for i in range(len(ss) - 1) 
                   for j in range(i + 1, len(ss) + 1))
          
          if sorted(ls) == target:
            print(ss)
        ps.add(p)
      

      Like

      • Frits's avatar

        Frits 3:50 pm on 19 September 2022 Permalink | Reply

        This only handles the situation if the two 1-segments appear at the start (at the left).

        Like

  • Unknown's avatar

    Jim Randell 5:01 pm on 16 September 2022 Permalink | Reply
    Tags:   

    Teaser 3130: Making squares 

    From The Sunday Times, 18th September 2022 [link] [link]

    Liam has nine identical dice. Each die has the usual numbers of spots from 1 to 6 on the faces, with the numbers of spots on opposite faces adding to 7. He sits at a table and places the dice in a 3×3 square block arrangement.

    As I walk round the table I see that (converting numbers of spots to digits) each vertical face forms a different three-figure square number without a repeating digit.

    As Liam looks down he sees six three-digit numbers (reading left to right and top to bottom) formed by the top face of the block, three of which are squares. The total of the six numbers is less than 2000.

    What is that total?

    [teaser3130]

     
    • Jim Randell's avatar

      Jim Randell 5:29 pm on 16 September 2022 Permalink | Reply

      At first I found multiple solutions. But you can find a unique answer to the puzzle if you ensure the dice really are identical.

      This Python program runs in 261ms.

      Run: [ @replit ]

      from enigma import (powers, nsplit, subsets, seq_all_same_r, nconcat, irange, printf)
      
      # some useful routines for checking dice ...
      
      # value on opposite face
      #      0  1  2  3  4  5  6
      opp = [0, 6, 5, 4, 3, 2, 1]
      
      # valid edge?
      edge = lambda x, y: x != y and x != opp[y]
      
      # construct (x, y) -> z corner maps for a right handed die
      def corners(x, y, z):
        d = dict()
        for (a, b, c) in [(x, y, z), (x, z, opp[y]), (x, opp[y], opp[z]), (x, opp[z], y)]:
          for (p, q, r) in [(a, b, c), (b, c, a), (c, a, b)]:
            d[(p, q)] = r
            d[(opp[p], opp[r])] = opp[q]
        return d
      
      # valid corner?
      # -> 0 if invalid; 1 if right-handed; -1 if left-handed
      def corner(x, y, z, d=corners(1, 2, 3)):
        r = d.get((x, y), 0)
        if r == z: return 1
        if r == opp[z]: return -1
        return 0
      
      # now on with the puzzle ...
      
      # possible 3-digit squares (as numbers) / vertical squares (as digits)
      (sqs, sqvs) = (set(), set())
      for n in powers(10, 31, 2):
        ds = nsplit(n)
        if min(ds) < 1 or max(ds) > 6: continue
        sqs.add(n)
        if len(set(ds)) == 3: sqvs.add(ds)
      
      # consider the layout:
      #
      #     W V U
      #    +-----+
      #  K |A B C| T
      #  L |D E F| S
      #  M |G H I| R
      #    +-----+
      #     N P Q
      
      # choose the squares around the edges (all different)
      for ((K, L, M), (N, P, Q), (R, S, T), (U, V, W)) in subsets(sqvs, size=4, select="P"):
      
        # choose the top faces for the corner dice
        for (A, C, G, I) in subsets(irange(1, 6), size=4, select="M"):
          corners = [(A, W, K), (G, M, N), (I, Q, R), (C, T, U)]
          r = seq_all_same_r(corner(*vs) for vs in corners)
          if not (r.same and r.value != 0): continue
      
          # choose the remaining top faces
          for (B, D, E, F, H) in subsets(irange(1, 6), size=5, select="M"):
            edges = [(D, L), (H, P), (F, S), (B, V)]
            if not all(edge(*vs) for vs in edges): continue
      
            # construct the 6 top numbers
            numbers = [(A, B, C), (D, E, F), (G, H, I), (A, D, G), (B, E, H), (C, F, I)]
            ns = list(nconcat(*vs) for vs in numbers)
            # the sum is less than 2000
            t = sum(ns)
            if not (t < 2000): continue
            # three of them are squares
            if sum(n in sqs for n in ns) != 3: continue
      
            # output solution
            printf("{t} <- {hs} {vs}; [{K}{L}{M}, {N}{P}{Q}, {R}{S}{T}, {U}{V}{W}]", hs=ns[:3], vs=ns[3:])
      

      (or a faster variation on [@replit]).

      Solution: The total is 1804.

      The dice are laid out as follows:

      So the total is: (115 + 441 + 445) + (144 + 144 + 515) = 1804.

      Of the six numbers read off from the top of the arrangement the square numbers are: 441 (= 21²) and 144 (twice; = 12²).

      Note that each of the corner dice is left-handed (i.e. a mirror image of a “standard” die), and so, as the dice are all identical, they must all be left-handed.

      If we are allowed to mix left- and right-handed dice, then there are many possible layouts (and many possible answers to the puzzle).

      Like

      • Frits's avatar

        Frits 9:50 pm on 16 September 2022 Permalink | Reply

        Thanks to Jim, hopefully with the correct solution.

           
        from itertools import permutations, product
        from functools import reduce
        
        # convert digits to number
        d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
        
        # 3-digit squares using digits 1-6
        sqs = [s for x in range(10, 27) 
               if not any(y in (s := str(x * x)) for y in "7890")]
        
        # 3-digit squares with different digits       
        side_sqs = [int(s) for s in sqs if len(set(s)) == 3]
        sqs = set(int(s) for s in sqs)
        
        #    +-------+   so bottom = 4, left = 5, back = 6
        #   /   3   /.    
        #  /       /2
        # +-------+ .
        # |   1   | 
        # |       |.
        # +-------+ 
        
        # we have nine identical dice 
        # so with two clockwise adjacent faces, say (2, 3),
        # the upper face is fixed (I have set it to 6)
        d = dict()
        # set the number facing up for clockwise pair (a, b)
        d[(1, 2)] = 4
        d[(1, 3)] = 2
        d[(2, 3)] = 6
        d[(2, 6)] = 4
        d[(3, 5)] = 6
        d[(3, 6)] = 2
        d[(4, 5)] = 1
        d[(4, 6)] = 5
        d[(5, 6)] = 3
        
        # add decreasing pairs
        for k, v in d.copy().items():
          d[k[::-1]] = 7 - v
          
        for p in permutations(side_sqs, 4):
         
          edges = []
          corners = []
          # calculate candidate numbers facing up for each corner and each edge
          for x, y in zip(p, p[1:] + (p[0], )):
            # connect squares x and y
            corner_sides = (x % 10, y // 100)
            
            if corner_sides not in d: break
            corners.append([d[corner_sides]])
            edges.append([i for i in range(1, 7) 
                          if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}])
          
          # each corner should have only one candidate
          if len(corners) != 4: continue
          
          edges = edges[1:] + [edges[0]]
          
          # 3x3 block of dice
          block = [corners[2], edges[1], corners[1],
                   edges[2], range(1, 7), edges[0],  
                   corners[3], edges[3], corners[0]]
         
          for p2 in product(*block):
           # six three-digit numbers on top face
           F = [d2n([p2[3 * i + j] for j in range(3)]) for i in range(3)] + \
               [d2n([p2[3 * j + i] for j in range(3)]) for i in range(3)]
           # the total of the six numbers is less than 2000 ...
           if sum(F) >= 2000: continue
           # ... three of which are squares .
           if sum([f in sqs for f in F]) != 3: continue
             
           print("    ", str(p[2])[::-1])
           print(str(p[3])[0], p2[:3], str(p[1])[2])
           print(str(p[3])[1], p2[3:6], str(p[1])[1])
           print(str(p[3])[2], p2[6:], str(p[1])[0])
           print("    ", p[0])
           print()
           print("total:", sum(F))
        

        Like

        • Frits's avatar

          Frits 10:03 pm on 16 September 2022 Permalink | Reply

          The side squares should be seen when walking anti-clockwise around the table (so the top and right squares are printed reversed).

          Like

    • Hugh Casement's avatar

      Hugh Casement 7:17 am on 17 September 2022 Permalink | Reply

      Are they left-handed dice? I can’t find any solution with standard dice (such as Frits shows).
      Or perhaps you have to walk round the table with your head upside down.

      Like

      • Jim Randell's avatar

        Jim Randell 7:25 am on 17 September 2022 Permalink | Reply

        @Hugh: Yes. There is only a solution if left-handed dice are used (at least in the corners of the layout – and as the dice are identical then the rest must be left-handed too).

        Like

      • Frits's avatar

        Frits 9:40 am on 17 September 2022 Permalink | Reply

        I set up my dice right-handed (I didn’t even know the term right-handed) based on numbers facing up when going clock wise. However, the corner arrangements in the block have to be read anti-clockwise so I should have used the 7-complement of my hardcoded values.

        My solution is the same as Jim’s and is left-handed. I will post a new version checking both left-handed and right-handed dice (using Brian Gladman’s function for determining the hardcoded values).

        Like

    • Frits's avatar

      Frits 1:52 pm on 17 September 2022 Permalink | Reply

      Supporting right-hand and left-hand dice and with a recursive version of Brian Gladman’s function for third face values.

         
      from itertools import permutations, product
      from functools import reduce
       
      # find the third face anti-clockwise around a dice vertex
      # given two faces in anti-clockwise order
      def f3(fi, se, rh=True):
        # invalid first/second combination
        if fi == se or fi + se == 7: return 0
         
        # only calculate for 3 low increasing pairs 
        if (fi, se) in {(1, 2), (1, 3), (2, 3)}:
          c = 0 if rh else 7  # complement related
          return abs(c - (fi * (2 * se - 1)) % 9)            # artificial formula
         
        # as of now work towards low increasing pairs
        if se < fi: return 7 - f3(se, fi, rh)
        elif fi > 3 and se > 3: return f3(7 - fi, 7 - se, rh) # double complement
        elif fi > 3: return 7 - f3(7 - fi, se, rh)
        elif se > 3: return 7 - f3(fi, 7 - se, rh)
          
      # convert digits to number
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
       
      # 3-digit squares using digits 1-6
      sqs = [s for x in range(10, 27) 
             if not any(y in (s := str(x * x)) for y in "7890")]
       
      # 3-digit squares with different digits       
      side_sqs = [int(s) for s in sqs if len(set(s)) == 3]
      sqs = set(int(s) for s in sqs)
         
      for p in permutations(side_sqs, 4):
        
        # check both right-handed and left-handed dice
        for rh in [1, 0]:
          edges = []
          corners = []
          # calculate candidate numbers facing up for each corner and each edge
          for x, y in zip(p, p[1:] + (p[0], )):
            # connect squares x and y and calculate top face
            top = f3(x % 10, y // 100, rh)
            if not top: break
            corners.append([top])
            edges.append([i for i in range(1, 7) 
                          if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}])
          else:    
            edges = edges[1:] + [edges[0]]    # rearrange 
       
            # 3x3 block of dice
            block = [corners[2], edges[1], corners[1],
                     edges[2], range(1, 7), edges[0],  
                     corners[3], edges[3], corners[0]]
       
            for p2 in product(*block):
             # six three-digit numbers on top face
             F = [d2n([p2[3 * i + j] for j in range(3)]) for i in range(3)] + \
                 [d2n([p2[3 * j + i] for j in range(3)]) for i in range(3)]
             # the total of the six numbers is less than 2000 ...
             if sum(F) >= 2000: continue
             # ... three of which are squares 
             if sum([f in sqs for f in F]) != 3: continue
       
             print("    ", str(p[2])[::-1])
             print(str(p[3])[0], p2[:3], str(p[1])[2])
             print(str(p[3])[1], p2[3:6], str(p[1])[1])
             print(str(p[3])[2], p2[6:], str(p[1])[0])
             print("    ", p[0], "\n")
             print("total:", sum(F), 
                   "(right-handed" if rh else "(left-handed", "dice)")
      

      Like

    • Frits's avatar

      Frits 11:39 pm on 17 September 2022 Permalink | Reply

      Based on Jim’s first posted program. This program runs in 90ms.

         
      from enigma import SubstitutedExpression
       
      # consider the layout:
      #
      #     W V U
      #    +-----+
      #  K |A B C| T
      #  L |D E F| S
      #  M |G H I| R
      #    +-----+
      #     N P Q
      
      # corners for a right-handed die
      die = {(1, 2, 3), (1, 3, 5), (1, 5, 4), (1, 4, 2), 
             (6, 4, 5), (6, 5, 3), (6, 3, 2), (6, 2, 4)}
       
      # check faces anti-clockwise around a corner (= x, y, z)
      # 1 = right-handed, -1 = left-handed
      def corner(x, y, z):
        ss = {x, y, z}
        return (1 if die.intersection({(x, y, z), (y, z, x), (z, x, y)}) else -1)
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
         "W != 7 - K",
         "is_square(KLM)",
         "U != 7 - T",
         "is_square(UVW)",
         "R != 7 - Q",
         "is_square(RST)",
         "N != 7 - M",
         "is_square(NPQ)",
         
         "seq_all_different([KLM, NPQ, RST, UVW])",
         # corner checks
         "A not in {7 - W, K, 7 - K}",
         "C not in {7 - U, T, 7 - T}",
         "I not in {7 - R, Q, 7 - Q}",
         "G not in {7 - M, N, 7 - N}",
         # edge checks
         "D != 7 - L", 
         "B != 7 - V", 
         "F != 7 - S", 
         "H != 7 - P", 
         
         "seq_all_same(corner(*vs) for vs in [(A, W, K), (G, M, N), \
                                              (I, Q, R), (C, T, U)])",
         
         "sum([ABC, DEF, GHI, ADG, BEH, CFI]) < 2000",
                                           
         "sum([1 for x in [ABC, DEF, GHI, ADG, BEH, CFI] if is_square(x)]) == 3",
        ],
        answer="(ABC, DEF, GHI), (KLM, NPQ, RST, UVW), \
                sum([ABC, DEF, GHI, ADG, BEH, CFI])",
        
        distinct=("KLM","NPQ","RST","WVU","AWK","UCT","IRQ","MGN", \
                  "DL","BV","FL","HP"),
        env=dict(corner=corner),
        digits=range(1, 7),
        reorder=0,
        denest=32,    # [CPython] work around statically nested block limit
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(" ", str(ans[1][3])[::-1])
        print(f"{str(ans[1][0])[0]} {ans[0][0]} {str(ans[1][2])[2]}")
        print(f"{str(ans[1][0])[1]} {ans[0][1]} {str(ans[1][2])[1]}")
        print(f"{str(ans[1][0])[2]} {ans[0][2]} {str(ans[1][2])[0]}")
        print(" ", ans[1][1], "  sum", ans[2], "\n")
      

      Like

    • Frits's avatar

      Frits 11:02 pm on 22 September 2022 Permalink | Reply

      More efficient version.

         
      from itertools import permutations, product
      from functools import reduce
      from collections import defaultdict
      
      # find the third face anti-clockwise around a dice vertex
      # given two faces in anti-clockwise order
      def f3(fi, se, rh=True):
        # invalid first/second combination
        if fi == se or fi + se == 7: return 0
        
        # only hardcode for 3 low increasing pairs 
        match (fi, se):
          case (1, 2): return 3 if rh else 4
          case (1, 3): return 5 if rh else 2
          case (2, 3): return 1 if rh else 6
          case _:
            # work towards low increasing pairs
            if se < fi: return 7 - f3(se, fi, rh)
            elif fi > 3 and se > 3: return f3(7 - fi, 7 - se, rh)
            elif fi > 3: return 7 - f3(7 - fi, se, rh)
            elif se > 3: return 7 - f3(fi, 7 - se, rh)
      
      # convert digits to number
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
      
      # 3-digit squares using digits 1-6
      sqs = [s for x in range(10, 27) 
             if not any(y in (s := str(x * x)) for y in "7890")]
       
      # map hundreds and units digits to tens digits in squares
      hu2ts = defaultdict(set)
      for s in sqs:
        h, t, u = (int(d) for d in s)
        hu2ts[h, u].add(t)
       
      # 3-digit squares with different digits       
      side_sqs = [int(s) for s in sqs if len(set(s)) == 3]
      sqs = set(int(s) for s in sqs)
        
      for p in permutations(side_sqs, 4):
        # check both right-handed and left-handed dice
        for rh in [1, 0]:
          edges = []
          corners = []
          # calculate candidate numbers facing up for each corner and each edge
          for x, y in zip(p, p[1:] + (p[0], )):
            # connect squares x and y and calculate top face
            top = f3(x % 10, y // 100, rh)
            if not top: break
            corners.append(top)
            edges.append([i for i in range(1, 7) 
                          if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}])
          else:    
            edges = edges[1:] + [edges[0]]    # rearrange 
            
            # c[2] e[1] c[1]   
            # e[2]  .   e[0]
            # c[3] e[3] c[0]
            
            # skip if sum of the four top numbers that don't use the center
            # is already too high
            if 100 * (2 * corners[2] + corners[1] + corners[3]) + 4 * 10 + \
               corners[3] + 2 + corners[0] + corners[1] + 222 > 1999:
              continue
            
            # which of these four top numbers can make a square number
            ts = []
            for i, (a, b) in enumerate([(1, 0), (2, 1), (2, 3), (3, 0)]):
              # can we form a square with 2 corners?
              if len(hu2ts[corners[a], corners[b]]):
                # which edge candidates result in a square
                cands = hu2ts[corners[a], corners[b]] & set(edges[i])
                if cands:
                  ts.append((i, cands))
            
            if not ts: continue    # we can't make a square
            elif len(ts) == 1:
              # only one top number can make a square, reduce the edge candidates
              edges[ts[0][0]] = list(ts[0][1])
           
            for e4 in product(*edges):
              # four three-digit numbers on top face (without the center)
              T4 = [d2n([corners[1], e4[0], corners[0]]),
                    d2n([corners[2], e4[1], corners[1]]),
                    d2n([corners[2], e4[2], corners[3]]),
                    d2n([corners[3], e4[3], corners[0]])]
              
              if sum(T4) + 222 > 1999: continue
              
              # center die
              for c in range(1, 7):
                # add two missing top numbers
                T6 = T4 + [d2n([e4[1], c, e4[3]]), d2n([e4[2], c, e4[0]])]
                
                # the total of the six numbers is less than 2000 ...
                if sum(T6) >= 2000: continue
                # ... three of which are squares 
                if sum([t in sqs for t in T6]) != 3: continue
               
                print(" ", str(p[2])[::-1])
                print(str(p[3])[0], T6[1], str(p[1])[2])
                print(str(p[3])[1], T6[3], str(p[1])[1])
                print(str(p[3])[2], T6[5], str(p[1])[0])
                print(" ", p[0], end="      ")
                print("total:", sum(T6), 
                      "(right-handed" if rh else "(left-handed", "dice)")
      

      Like

  • Unknown's avatar

    Jim Randell 8:46 am on 15 September 2022 Permalink | Reply
    Tags:   

    Teaser 2735: Two to choose 

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

    I told Sue a two-figure number and I told Terry another two-figure number, one of which was a multiple of the other. I explained this to them but knew that neither of them would be able to work out the other number. (In fact, if they had to guess the other number Sue had three times as many choices as Terry — but I did not tell them that).

    When Sue confirmed that it was impossible for her to work out Terry’s number, he was then able to work out her number.

    What were their numbers?

    [teaser2735]

     
    • Jim Randell's avatar

      Jim Randell 8:47 am on 15 September 2022 Permalink | Reply

      In the following Python program I construct a map from 2-digit numbers to possible “other” numbers (other 2-digit numbers that are proper multiples or divisors of the first number).

      We know (but S and T do not) that neither of them can work out the others number, so we can remove those numbers that only have a single associated number from consideration.

      When S reveals she cannot work out T’s number, T knows that S’s number must be one of these candidates, so the associated numbers for T’s number must include exactly one of the candidate numbers.

      The program runs in 57ms. (Internal run time is 204µs).

      Run: [ @replit ]

      from enigma import (defaultdict, irange, singleton, intersect, printf)
      
      # collect 2-digit numbers that are proper multiples of other 2-digit numbers
      d = defaultdict(list)
      for a in irange(10, 49):
        for b in irange(2 * a, 99, step=a):
          d[a].append(b)
          d[b].append(a)
      
      # candidates that have multiple other numbers
      ks = set(k for (k, vs) in d.items() if len(vs) > 1)
      
      # T can work out S's number (knowing S cannot work out T's)
      for T in ks:
        ss = d[T]
        S = singleton(intersect([ks, ss]))
        if S is not None:
          ts = d[S]
          # in the solution we are looking for S has 3x as many initial choices as T
          if len(ts) == 3 * len(ss):
            printf("S={S} [-> {ts}; T={T} [-> {ss}]")
      

      Solution: Sue’s number was 14. Terry’s number was 98.

      We have: 98 = 7 × 14.

      S has 14, so she knows that T has one of {28, 42, 56, 70, 84, 98}.

      And T has 98, so initially he knows that S has one of {14, 49}. So S has three times as many options for T as T has for S.

      But if S had 49 there are no 2-digit divisors and the only 2-digit multiple is 98, so S would be able to work out T’s value. And as S cannot work out T’s value she cannot have 49, so T can deduce that she must have 14.

      Like

    • Frits's avatar

      Frits 11:18 am on 15 September 2022 Permalink | Reply

         
      # candidates that have multiple other numbers
      cands = {x: lst for x in range(10, 100) 
               if len(lst := [y for y in range(10, 100) 
                              if x != y and not(y % x and x % y)]) > 1}
      
      # Terry must have 2 candidates and Sue 6 for the "three times" requirement
      for terry, sues in cands.items():
        if len(sues) != 2: continue
        # only one of Terry's numbers for Sue must be ambiguous
        if len([sue := s for s in sues if s in cands]) != 1 or \
           len(cands[sue]) != 6: continue
        print("Terry =", terry, "Sue =", sue)
      

      or

         
      # Terry must have 2 candidates and Sue 6 for the "three times" requirement
      
      # candidates that have two or six other numbers
      cands = {x: lst for x in range(10, 100) 
               if len(lst := [y for y in range(10, 100) 
                              if x != y and not(y % x and x % y)]) in {2, 6}}
      
      for sue, terries in cands.items():
        if len(terries) != 6: continue
        # find Terry's where <sue> is one of the two candidates
        for terry, sues in cands.items():
          if len(sues) != 2 or sue not in sues: continue
          # determine other candidate (not <sue>)
          other = [s for s in sues if s != sue][0]
          # <other> may not have multiple other numbers
          if len([x for x in range(10, 100) if x != other and 
                 not(other % x and x % other)]) > 1: continue
          print("Terry =", terry, "Sue =", sue)
      

      Like

  • Unknown's avatar

    Jim Randell 10:07 am on 13 September 2022 Permalink | Reply
    Tags:   

    Teaser 2728: Garden design 

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

    I have a square garden with sides a whole number of metres in length. It is surrounded by a fence with posts at the corners and then at one metre intervals. I wish to make the garden into four triangular beds surrounding a lawn that has four sides of different lengths. To mark out the lawn I choose one post on each of the sides of the garden and I stretch a length of string around those four posts. I can create my lawn in various ways but the length of string needed is always one of two possible values. I have chosen one arrangement using the smaller of the two lengths.

    What is the area of my lawn?

    [teaser2728]

     
    • Jim Randell's avatar

      Jim Randell 10:08 am on 13 September 2022 Permalink | Reply

      In this program I round results (in metres) to 4 decimal places, to get measurements to with sub-millimetre accuracy, but we don’t have to worry about floating point inaccuracies when we compare measurements.

      This Python program runs in 53ms. (Internal run time is 1.0ms).

      Run: [ @replit ]

      from enigma import (defaultdict, subsets, irange, hypot, seq_all_different, tuples, printf)
      
      # measure float quantities to 4dp
      measure = lambda x: round(x, 4)
      
      # look for solutions with an <n> by <n> square
      def solve(n, k=None):
        # choose the positions of the posts
        d = defaultdict(set)
        for ps in subsets(irange(1, n - 1), size=4, select="M"):
          # perpendicular dimensions of corner triangles
          ts = list((x, n - y) for (x, y) in tuples(ps, 2, circular=1))
          # calculate the sides of the central quadrilateral
          ds = list(hypot(x, y) for (x, y) in ts)
          # they should all be different lengths
          if not seq_all_different(ds, fn=measure): continue
          # calculate the total length of string required
          s = measure(sum(ds))
          # caclulate area
          a = measure(n * n - 0.5 * sum(x * y for (x, y) in ts))
          d[s].add(a)
          # early termination?
          if k and len(d.keys()) > k: return
        if k and len(d.keys()) != k: return
        return d
      
      # consider an n x n garden
      for n in irange(3, 100):
        d = solve(n, 2)
        if d is None: continue
        # output solution (= shortest length string)
        s = min(d.keys())
        for a in sorted(d[s]):
          printf("n={n}: lawn area = {a:.3f} m^2 [string length = {s:.3f} m]")
        break  # we only need the first garden
      

      Solution: The area of the lawn is: 7 square metres.

      Considering gardens where the central lawn area has sides of 4 different lengths.

      For a garden less than 4m × 4m this is not possible.

      For a 4m × 4m garden there are essentially 2 different layouts (where the central area has sides of different length):

      The lengths of string, and area of lawn are:

      A: string = √5 + √10 + √13 + √ 8 ≈ 11.832 m; lawn = 8.5 m²
      B: string = √2 + √13 + √5 + √18 ≈ 11.498 m; lawn = 7.0 m²

      So layout B uses the least amount of string, and so gives the answer to the puzzle.

      For a garden greater than 4m × 4m there are more than 2 different layouts, which give more than 2 different lengths of string.

      Like

  • Unknown's avatar

    Jim Randell 8:36 am on 11 September 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 119: Weights 

    From The Sunday Times, 7th July 1963 [link]

    In the time of the Great Caliph a large annual tax was one day levied on shopkeepers for each weight used in their shops. The ingenious Al Gebra contrived to use very few weights but he often had weights in both his scale pans. The exchequer hit back by making it compulsory to make every weighing by using two weights only, one in each pan. Al now contrived with 20 weights to weigh up to 118 lb. in 1 lb. steps. Using 1 lb. as the least weight he found various ways of doing this. “But”, he said, “I’m getting old and I’m going to have the lightest possible set”.

    Which set was this?

    [teaser119]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 11 September 2022 Permalink | Reply

      This puzzle was published with a note that the previous weeks puzzle, Teaser 118 (also involving finding a set of weights), had solicited “no correct solution”.

      However, as we found, the published solution for Teaser 118 is incorrect.

      In this puzzle we are also asked to find a set of weights, and the originally published solution is also incorrect (although a correction was later made with one optimal set of weights, although it turns out this set is not unique).


      If the lightest weight is 1 lb, then the heaviest weight we need is 119 lb (to create a difference of 119 − 1 = 118), and the remaining 18 weights can fit in between.

      This is equivalent to constructing a sparse ruler [@wikipedia] of length 118, with 20 marks.

      And as we want to cover all differences 1 … 118, the ruler must be complete.

      I started by writing a program to find complete sparse rulers, and then adapted it to produce rulers with a minimal total sum for the set of marks, and from these we construct sets of weights to solve the puzzle.

      For a set of 20 weights weighing values 1 … 118 the program takes 29m28s to find the 5 optimal solutions (although the solutions are found much faster than this (in 5.4s), the rest of the time is spent completing the search to ensure there are no better solutions).

      from enigma import (irange, append, diff, empty, reverse, Accumulator, arg, printf)
      
      # generate sparse rulers
      # L = length
      # M = number of marks
      # ms = existing marks (as a set)
      # k = number of remaining marks
      # xs = distances not yet satisfied (as an ordered list)
      # zl = left zone
      # zr = right zone
      # r = Accumulator (on the sum of the marks)
      def rulers(L, M, ms, k, xs, zl, zr, r):
        if k == 0:
          if not xs:
            ms = sorted(ms)
            t = sum(ms)
            # is the mirror image better?
            if L * M < 2 * t:
              ms = reverse(L - x for x in ms)
              t = L * M - t
            # record the value
            r.accumulate_data(t, ms)
            if r.value == t: printf("[{ms} -> {t}]")
        else:
          # perform some early termination checks:
          # are there too many unsatisfied differences remaining?
          if xs and len(xs) > (k * (k - 1)) // 2 + k * (M - k): return
          # is max distance too big?
          if xs and xs[-1] > max(zr, L - zl): return
          # will the additional marks exceed the current best?
          if r.value and sum(ms) + sum(irange(zl, zl + k - 1)) > r.value: return
          # can we fit k marks in the gaps?
          if zr - zl + 1 < k: return
      
          # extend left zone?
          if not (zl > L - zr):
            # try with mark at zl
            ds = set(abs(m - zl) for m in ms)
            rulers(L, M, append(ms, zl), k - 1, diff(xs, ds), zl + 1, zr, r)
            # try without mark at zl
            rulers(L, M, ms, k, xs, zl + 1, zr, r)
          else:
            # try without mark at zr
            rulers(L, M, ms, k, xs, zl, zr - 1, r)
            # try with mark at zr
            ds = set(abs(m - zr) for m in ms)
            rulers(L, M, append(ms, zr), k - 1, diff(xs, ds), zl, zr - 1, r)
      
      # generate complete rulers
      complete_rulers = lambda L, M, r: rulers(L, M, {0, L}, M - 2, list(irange(1, L - 1)), 1, L - 1, r)
      
      # ruler length and marks
      L = arg(118, 0, int)
      M = arg(20, 1, int)
      printf("[L={L} M={M}]")
      
      # generate complete rulers with length L, and M marks, and recorded minimal total
      r = Accumulator(fn=min, collect=1)
      complete_rulers(L, M, r)
      
      # use optimal rulers
      printf("best = {r.value}")
      for ss in r.data:
        printf("-> ruler = {ss}")
        # make the weights
        ws = list(x + 1 for x in ss)
        printf("-> weights = {ws}; total = {t}", t=sum(ws))
      

      Solution: There are five possible sets of weights, each set weighs 712 lb:

      size = 20: sum = 712
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 23, 35, 48, 60, 73, 84, 96, 108, 119)
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 23, 35, 49, 59, 73, 84, 96, 108, 119)
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 23, 36, 47, 60, 72, 85, 96, 108, 119)
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 23, 36, 48, 60, 71, 85, 96, 108, 119)
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 23, 37, 47, 59, 72, 85, 96, 108, 119)

      The originally published solution (with Teaser 120) is:

      size = 20: sum = 759
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 21, 32, 43, 54, 65, 76, 87, 98, 109, 119)

      which is not optimal.

      But this was later revised (with Teaser 122) to:

      size = 20: sum = 712
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 23, 36, 47, 60, 72, 85, 96, 108, 119)

      which is the third of the optimal sets I give above.


      The puzzle says the tax is levied for each weight, so it may be preferable to use fewer weights.

      Each pair of weights corresponds to potential actual value, and C(15, 2) = 105, does not give enough pairs. But C(16, 2) = 120 does. So it is potentially possible that a set of 16 weights could be used to achieve our goal of weighing each of the weights 1 … 118.

      I checked sets of 16, 17, 18 (that include a 1 lb weight), but didn’t find any viable sets.

      But for a set of 19 weights I found:

      size = 19: sum = 901
      (1, 2, 3, 6, 7, 9, 12, 13, 18, 31, 44, 57, 70, 83, 96, 103, 110, 117, 119)

      This is heavier than the optimal solution sets with 20 weights (sum = 712), but if the tax per weight was high it may be preferable.

      However, by using more weights we can come up with a lighter set:

      Here are the optimal sets of size 21 to 25:

      size = 21: sum = 629
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 29, 45, 60, 76, 90, 105, 119)
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 29, 46, 59, 76, 90, 105, 119)

      size = 22: sum = 634
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 31, 42, 57, 73, 88, 104, 119)

      size = 23: sum = 609
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 35, 49, 67, 84, 102, 119)

      size = 24: sum = 615
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 29, 47, 65, 83, 101, 119)

      size = 25: sum = 605
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 39, 59, 79, 99, 119)
      (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 41, 58, 78, 99, 119)

      after which the total weight starts going up again.

      So the lightest set of weights is one of the sets of size 25.

      But presumably, the intricacies of the tax system have led Al Gebra to conclude that the extra weight for an optimal set of 20 weights is offset by the difference in tax on the lighter set of 25 weights.

      Like

    • Frits's avatar

      Frits 2:08 pm on 11 September 2022 Permalink | Reply

      The numbers 20 and 118 were probably chosen as it leads to a solution which it is relatively easy to check for completeness.

       
      in this case we can formulate L + 1 as (n + 1) * n + n - 1 where n = M / 2
      
           1-n       n+1 - 2n   2n+2 - 3n+1   (n-1)n+n-1 - n*n+n-2    
      1,2,3,..., n ,   2n+1,       3n+2, ....... ,  n*n+n-1,      (n+1)n+n-1  
      
      We can already start with minimum n*(n+1)//2 + ((n+2)*(n+1)//2 - 1)*n + n*(n+1)//2 - 1 = M**3 / 16 + 5*M**2 / 8 + M / 2 - 1 = 759 ( originally published solution)
      

      Like

    • Frits's avatar

      Frits 12:19 pm on 12 September 2022 Permalink | Reply

      This program runs in 11.5 seconds with PyPY (with inc set to 3).

      I am not sure if we can rely on the weights always to contain number 1 so we have to see this program as a way to get a low total weight.

        
      from itertools import combinations
      
      M = 20
      L = 118
      
      # can we make number 1,...,N with numbers from <seq>
      def check(seq):
        s = set()
        for c1, c2 in combinations(seq, 2):
          d = c2 - c1
          if 0 < d < L + 1 and d not in s:     
            s.add(d)
            if len(s) == L: 
              return True
        return False
        
      # pick one value from each entry of a <k>-dimensional list <ns>
      def pickOneFromEach(k, ns, s=[]):
        if k == 0:
          yield s
        else:
          for n in ns[k - 1]:
            yield from pickOneFromEach(k - 1, ns, s + [n])
        
      n = 10
      M = 20
      break_next = 0
      
      # assume the first sequence of consecutive weights is 1,2,3, ..,m
      # then the last 2 weight must be L + 1 - m, L + 1
      # we have to choose M - m - 2 weights between m and L + 1 - m
      best = 99999
      inc = 3           # allowed deviance from calculated middle value
      for m in range(17, 0, -1):
        print()
        print("suppose the first consecutive weights are", list(range(1, m + 1)))
        print("then the last 2 weight must be", [L + 1 - m, L + 1])
        print("we have to choose", M - m - 2, "weights between", m, "and", 
              L + 1 - m, "with", M - m - 1, "intervals")
        
        # calculate groups of allowed values per missing weight
        ws = [[m + i + x * (L + 1 - 2 * m ) // (M - m - 1) 
                  for i in range(-inc, inc + 1)] for x in range(1, M - m - 1)]
        print("choose weights from", *ws)
        
        for p in pickOneFromEach(M - m - 2, ws):
          # rebuild full set of weights
          ns = list(range(1, m + 1)) + sorted(p) + [L + 1 - m, L + 1]
          if sum(ns) > best: continue
          # check if we can make all numbers 1 - 118
          if not check(ns): continue
            
          best = sum(ns)
          print(ns, "with total weight", sum(ns))
       
        if break_next: break
          
        if best < 99999:
          # we assume that a lower <m> results in a higher total weight
          # perform one extra run with a lower <m>  anyway
          break_next = 1   
      

      Like

      • Jim Randell's avatar

        Jim Randell 5:07 pm on 12 September 2022 Permalink | Reply

        We are told in the puzzle text that the lowest weight is 1lb.

        Even if we weren’t told, given any set of weights all that matters is the set of differences. So all the weights can be reduced (or increased) by the same amount to give another set.

        Specifically if we reduce the lowest weight to 1 we will get the minimal possible set based on the original set.

        In fact we can reduce the lowest weight to 0, and then we end up with the corresponding ruler.

        Like

  • Unknown's avatar

    Jim Randell 4:46 pm on 9 September 2022 Permalink | Reply
    Tags:   

    Teaser 3129: Bounce count 

    From The Sunday Times, 11th September 2022 [link] [link]

    At the local arcade, Claire and David played an air hockey game, using a square table with small pockets at each corner, on which a very small puck can travel 1m left-right and 1m up-down between the perimeter walls. Projecting the puck from a corner, players earn a token for each bounce off a wall, until the puck drops into a pocket.

    In their game, one puck travelled 1m further overall than its left-right distance (and the other, travelled 2m further). Claire’s three-digit number of tokens was a cube, larger than David’s number which was triangular (1 + 2 + 3 + …). Picking up an extra token, they found they could split their collection into two piles, one consisting of a cube number of tokens and the other a square.

    How many tokens did they end up with?

    I’ve modified the wording slightly to remove a typo and improve clarity.

    [teaser3129]

     
    • Jim Randell's avatar

      Jim Randell 5:23 pm on 9 September 2022 Permalink | Reply

      With this kind of puzzle it is easier to reflect the table, rather than reflecting the puck. (Assuming the puck bounces in a well behaved fashion). See: Teaser 1917.

      If a play has x bounces off the left/right sides, and y bounces of the top/bottom sides, then in order for the play to be viable, (x + 1) and (y + 1) must be coprime, and:

      score = x + y
      left/right distance = x + 1
      top/bottom distance = y + 1
      distance travelled, d = hypot(x + 1, y + 1)

      It seems we need to find scores C (a cube) and D (a triangular number) such that (C + D + 1) can be expressed as the sum of a cube and a square.

      This Python program runs in 71ms. (Internal run time is 14.6ms).

      Run: [ @replit ]

      from enigma import (
        coprime_pairs, is_square, sq, is_cube,
        is_triangular, cproduct, powers, inf, printf
      )
      
      # generate (x, y, z) values, where z is d - x
      def generate():
        # consider coprime pairs
        for (a, b) in coprime_pairs(200):
          d = is_square(sq(a) + sq(b))
          if d is None: continue
          for (x, y) in [(a, b), (b, a)]:
            z = d - x
            if z in {1, 2}:
              yield (x - 1, y - 1, z)
      
      # find possible values for C and D
      (Cs, Ds) = (list(), list())
      for (x, y, z) in generate():
        t = x + y
        if 99 < t < 1000 and is_cube(t):
          Cs.append((x, y, z, t))
        if t < 1000 and is_triangular(t):
          Ds.append((x, y, z, t))
      
      # can total t be decomposed into a square and a cube?
      def is_sq_cb(t):
        for c in powers(1, inf, 3):
          s = t - c
          if s < 1: return False
          if is_square(s): return True
      
      # choose values for C and D
      for ((Cx, Cy, Cz, Ct), (Dx, Dy, Dz, Dt)) in cproduct([Cs, Ds]):
        T = Ct + Dt + 1
        # Cz/Dz are different; Ct > Dt; T is a square + a cube
        if Cz != Dz and Dt < Ct and is_sq_cb(T):
          # output solution
          printf("T={T} [C=(x={Cx} y={Cy} z={Cz} t={Ct}); D=(x={Dx} y={Dy} z={Dz} t={Dt})]")
      

      Solution: They ended up with a total of 171 tokens.

      C won 125 (= 5^3) tokens on her go. The puck made 111 left/right bounces and 14 up/down bounces. The left/right distance travelled was 112m and the total distance travelled was 113m. So C’s overall distance was 1m more than the total left/right distance.

      D won 45 (= tri(9)) tokens on his go. The puck made 34 left/right bounces and 11 up/down bounces. The left/right distance travelled was 35m and the total distance travelled was 37m. So D’s overall distance was 2m more than the total left/right distance.

      Combining their tokens with 1 extra token give 125 + 45 + 1 = 171 tokens in total.

      And 171 = 144 + 27 = 12^2 + 3^3.

      Like

      • Jim Randell's avatar

        Jim Randell 9:33 pm on 9 September 2022 Permalink | Reply

        Faster (and a bit shorter) to use [[ pythagorean_triples() ]] to generate the (x, y, z) values. And we don’t need to check the coprime condition if we just look at primitive pythagorean triples.

        This Python program runs in 54ms. (Internal run time is 579µs).

        Run: [ @replit ]

        from enigma import (
          pythagorean_triples, is_square, is_cube, is_triangular,
          cproduct, powers, inf, ifirst, lt, printf
        )
        
        # generate (x, y, z) values, where z is d - x
        def generate():
          for (a, b, c) in pythagorean_triples(1001, primitive=1):
            z = c - b
            if z in {1, 2}:
              yield (b - 1, a - 1, z)
        
        # find possible values for C and D
        (Cs, Ds) = (list(), list())
        for (x, y, z) in generate():
          t = x + y
          if 99 < t < 1000 and is_cube(t):
            Cs.append((x, y, z, t))
          if t < 1000 and is_triangular(t):
            Ds.append((x, y, z, t))
        
        # can total t be decomposed into a square and a cube?
        def is_sq_cb(t):
          return any(is_square(t - c) for c in ifirst(powers(1, inf, 3), count=lt(t)))
        
        # choose values for C and D
        for ((Cx, Cy, Cz, Ct), (Dx, Dy, Dz, Dt)) in cproduct([Cs, Ds]):
          T = Ct + Dt + 1
          # Cz/Dz are different; Ct > Dt; T is a square + a cube
          if Cz != Dz and Dt < Ct and is_sq_cb(T):
            # output solution
            printf("T={T} [C=(x={Cx} y={Cy} z={Cz} t={Ct}); D=(x={Dx} y={Dy} z={Dz} t={Dt})]")
        

        Like

        • Frits's avatar

          Frits 9:48 am on 10 September 2022 Permalink | Reply

          @Jim, how do you guarantee one puck travelled 1m and the other 2m?

          Like

        • Frits's avatar

          Frits 9:54 am on 10 September 2022 Permalink | Reply

          Can’t both C and D not have a difference of 1 m? or both 2m?

          Like

          • Jim Randell's avatar

            Jim Randell 9:55 am on 10 September 2022 Permalink | Reply

            No. Line 30 ensures the z values are different.

            Like

            • Frits's avatar

              Frits 10:00 am on 10 September 2022 Permalink | Reply

              @you are right, I only saw line 10. I may have overlooked it as I use z as the hypotenuse.

              Like

    • Frits's avatar

      Frits 8:31 pm on 9 September 2022 Permalink | Reply

         
      from itertools import product
      from enigma import is_square, is_cube, is_triangular
      
      # can number <n> be decomposed into a square and a cube?
      def check(n):
        for i in range(int(n**(1/3)) + 1):
          if is_square(n - i**3): 
            return True
        return False  
      
      g1 = []
      g2 = []
      # travelled distance is hypothenuse z
      # for one person one side is z - 1, for the other person it is z - 2
      for z in range(1, 1003):
        # store sides of a right triangle (x, y, z) if x + y - 2 < 1000 
        # z**2 - (z - 1)**2 = 2z - 1
        if (rt := is_square(2 * z - 1)) and (z - 1 + rt - 2) < 1000:
          g1.append((z, z - 1, rt))
        # z**2 - (z - 2)**2 = 4(z - 1) 
        if (rt := is_square(4 * (z - 1))) and (z - 2 + rt - 2) < 1000:
          g2.append((z, z - 2, rt))  
      
      # check if number of earned tokens x + y - 2 is cube or triangular
      g1 = [(x + y - 2, (c, t), (z, x, y)) for z, x, y in g1 
            if ((c := is_cube(x + y - 2)) and x + y - 2 > 99) or 
              (t := is_triangular(x + y - 2))]    
      g2 = [(x + y - 2, (c, t), (z, x, y)) for z, x, y in g2 
            if ((c := is_cube(x + y - 2)) and x + y - 2 > 99) or 
              (t := is_triangular(x + y - 2))]     
      
      # check all combinations
      for p1, p2 in product(g1, g2):
        # there should be at least one cube and one triangular number
        if any(p1[1][i] == p2[1][i] == None for i in range(2)): continue
        # cube should be higher than triangular number
        if p1[0] == p2[0]: continue
        if p1[0] > p2[0] and p1[1][0] == None: continue
        if p2[0] > p1[0] and p2[1][0] == None: continue
        
        # can we arrange all their tokens plus one into a cube and a square?
        if check(p1[0] + p2[0] + 1):
          print("answer:", p1[0], "and", p2[0])
      

      Like

      • Frits's avatar

        Frits 11:31 am on 10 September 2022 Permalink | Reply

        Easier to read.

           
        from enigma import is_square, is_cube, is_triangular, ceil
        
        # can number <n> be decomposed into a square and a cube?
        def check(n):
          for i in range(1, ceil(n**(1/3))):
            if is_square(n - i**3): 
              return True
          return False  
        
        # travelled distance is hypothenuse z of a right triangle
        # for one person one side is z - 1, for the other person it is z - 2
        
        cubes, tris = [], []
        for z in range(1, 1003):
          # 1m or 2m further
          for f in range(1, 3):
            # calculate other side (besides z and z - f)
            other = is_square(2 * f * z - f**2)          # z**2 - (z - f)**2
            if not other: continue
            # for a right triangle (x, y, z) we earn x + y - 2 tokens
            tkns = z - f + other - 2
            if is_cube(tkns) and tkns > 99:
              cubes.append((tkns, f, (z - f, other))) 
            if is_triangular(tkns):
              tris.append((tkns, f, (z - f, other))) 
        
        # check all combinations
        for c in cubes:
          for t in tris:
            # cube larger than triangular and using both 1m further and 2m further
            if t[0] >= c[0] or t[1] == c[1]: continue
            # can we arrange all their tokens plus one into a cube and a square?
            if check(c[0] + t[0] + 1):
              print("answer:", c[0], "and", t[0])  
        

        Like

        • Jim Randell's avatar

          Jim Randell 3:38 pm on 10 September 2022 Permalink | Reply

          @Frits: Yes, I think that is much clearer.

          Although I don’t see a check to ensure that the left/right and up/down distances are coprime. This is necessary for a valid path. If this is not the case the puck will hit a pocket before it reaches the end of the path.

          Like

          • Frits's avatar

            Frits 9:14 pm on 10 September 2022 Permalink | Reply

            You are right, I had to verify the coprime thing with the following graph.
            If both distances share a factor then there is at least one point on the hypothenuse (besides the end points) where both x and y are whole numbers (and thus the puck drops prematurely in a pocket).

             
              |               f(x) = b - (x * b) / a   
            b-|               suppose b and a share factor k, k > 1
              |\              so a = k * a2, b = k * b2
              | \
              |  \            f(x) = b - (x * b2 / k) / (a2 / k) 
              |   \           if x = a2 then f(x) = b - b2 (integer)
              |    \
              +---------
                    a
            

            Like

  • Unknown's avatar

    Jim Randell 9:19 am on 8 September 2022 Permalink | Reply
    Tags:   

    Teaser 2740: X times 

    From The Sunday Times, 29th March 2015 [link] [link]

    In this long multiplication sum, I am multiplying a three-figure number by itself. Throughout the workings one particular digit has been replaced by X wherever it occurs: all other digits have been replaced by a question mark.

    What is the three-figure number being squared?

    [teaser2740]

     
    • Jim Randell's avatar

      Jim Randell 9:21 am on 8 September 2022 Permalink | Reply

      The intermediate multiplications are presented in the opposite order to how I was taught long multiplication. But from the layout of the columns the intent is obvious.

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

      The following run file executes in 73ms. (Internal runtime of the generated program is 277µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      #       a X b
      #  *    a X b
      #   ---------
      #   c d e      = aXb * a
      #   f g X X    = aXb * X
      #     h i j k  = aXb * b
      #   ---------
      #   X m n p q  = aXb * aXb
      
      SubstitutedExpression
      
      # added symbols are different from X
      --distinct="aX,bX,cX,dX,eX,fX,gX,hX,iX,jX,kX,mX,nX,pX,qX"
      
      # the multiplication sum
      "{aXb} * {aXb} = {Xmnpq}"
      "{aXb} * {a} = {cde}"
      "{aXb} * {X} = {fgXX}"
      "{aXb} * {b} = {hijk}"
      
      --answer="{aXb}"
      

      Solution: The number being squared is: 286.

      Note that you will find the answer even if you don’t check that the question marks are all different from X (we can use [[ --distinct="" ]] to show this), but without this check the solution is not complete.

      Like

    • GeoffR's avatar

      GeoffR 1:33 pm on 8 September 2022 Permalink | Reply

      
      from itertools import permutations
      
      for p1 in permutations('1234567890', 3):
          a, X, b = p1
          aXb = int(a + X + b)
          # check leading digit of multiplication result
          res = aXb * aXb
          if res //10000 != int(X):continue
          
          # 1st line of multiplication
          line1 = int(a) * aXb * 100
          if X in set(str(line1)):continue
          
          # 2nd line of multiplication
          line2 = int(X) * aXb * 10
          if line2 //100 % 10 != int(X):continue
          if line2//10 % 10 != int(X):continue
          
          # 3rd line of multiplication
          line3 = int(b) * aXb
          if X in set(str(line3)):continue
          print(f"The three-figure number being squared is {aXb}.")
      
      # The three-figure number being squared is 286.
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 8:50 am on 6 September 2022 Permalink | Reply
    Tags:   

    Teaser 2752: Best before 

    From The Sunday Times, 21st June 2015 [link] [link]

    Peter likes to note “pandigital” times, such as 15.46, 29/03/78, that use all ten digits. Here the five individual numbers (15, 46, 29, 3 and 78) have a product that is divisible by the perfect square 36, and they also have a sum that is two more than a perfect square.

    He has been watching for pandigital times ever since and remembers one where the product of the five numbers was not divisible by any perfect square (apart from 1): this has never happened since! He is also looking out for a pandigital time where the sum of the five numbers is a perfect square:

    (a) When was that last “non-square” pandigital time?
    (b) When will that first “square-sum” pandigital time be?

    [teaser2752]

     
    • Jim Randell's avatar

      Jim Randell 8:51 am on 6 September 2022 Permalink | Reply

      We generate possible pandigital times, and then check to find the last (before the puzzle was set) with a square-free product, and the first (after the puzzle was set) with a perfect square sum.

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

      Run: [ @replit ]

      from datetime import datetime
      from enigma import (
        irange, choose, implies, nconcat, catch,
        multiply, is_square_free, is_square, printf
      )
      
      # find pandigital (y, m, d, H, M) values
      def generate():
        # possible digits
        digits = set(irange(0, 9))
      
        # selection functions
        fns = [
          # month first digit; is 0-1
          lambda m1: m1 < 2,
          # hour first digit; is 0-2
          lambda m1, H1: H1 < 3,
          # day first digit; is 0-3
          lambda m1, H1, d1: d1 < 4,
          # minute first digit; is 0-5
          lambda m1, H1, d1, M1: M1 < 6,
          # month second digit; m2 = 1 -> 0, 2
          lambda m1, H1, d1, M1, m2: implies(m1 == 1, m2 < 3),
          # hour second digit; H1 = 2 -> 0, 1, 2
          lambda m1, H1, d1, M1, m2, H2: implies(H1 == 2, H2 < 3),
          # day second digit; d1 = 3 -> 0, 1
          lambda m1, H1, d1, M1, m2, H2, d2: implies(d1 == 3, d2 < 2),
          # remaining digits (M2, y1, y2) = any
          None, None, None
        ]
      
        # assign digits
        for (m1, H1, d1, M1, m2, H2, d2, M2, y1, y2) in choose(digits, fns, distinct=1):
          (y, m, d, H, M) = (nconcat(*x) for x in [(y1, y2), (m1, m2), (d1, d2), (H1, H2), (M1, M2)])
          y += (2000 if y < 78 else 1900)
          t = catch(datetime, y, m, d, H, M)
          if t is not None:
            yield (t, (H, M, d, m, y % 100))
      
      # date of the puzzle
      now = datetime(2015, 6, 21)
      
      # record answers
      a = b = None
      
      # look for pandigital times
      for (t, ns) in generate():
        # (a) latest time before now with a square-free product
        if t < now and (a is None or t > a) and is_square_free(multiply(ns)):
          a = t
        # (b) earliest time after now with a perfect square sum
        if t > now and (b is None or t < b) and is_square(sum(ns)):
          b = t
      
      # output solution
      fmt = lambda t: t.strftime("%H:%M, %d/%m/%y")
      printf("(a) {a}", a=fmt(a))
      printf("(b) {b}", b=fmt(b))
      

      Solution: (a) 15:43, 26/07/89; (b) 18:59, 27/06/34.

      Like

    • Frits's avatar

      Frits 6:06 pm on 6 September 2022 Permalink | Reply

      Two programs:

         
      from itertools import permutations
      
      # small numbers which are square free
      nums = [n for n in range(1, 32) if n % 11 and all(n % y for y in [4, 9, 25])]
      days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      mx_dt = 0
      
      # check day/month/hour permutations
      for d, m, h in permutations(nums, 3):
        if m > 12 or h > 24: continue
        
        s = "".join(str(x).zfill(2) for x in (d, m, h))
        if len(set(s)) != 6: continue          # different digits
        
        p1 = d * m * h                         # product
        # not divisible by any perfect square
        if not all(p1 % (n * n) 
          for n in [2] + list(range(3, int(p1**.5) + 1, 2))): continue
        
        # check day number
        if d > 28 and m == 2:
          if year % 4 == 0 and (year % 100 or year % 400 == 0):
            days[1] = 29
          else:
            days[1] = 28
        if d > days[m - 1]:  continue  
        
        rest = [int(x) for x in range(9, -1, -1) if str(x) not in s]
        mdh = str(m).zfill(2) + str(d).zfill(2) + str(h).zfill(2)
        
        # check year/minutes permutations
        for p in permutations(rest):
          if p[2] > 5: continue                # minutes < 60
          
          year = 10 * p[0] + p[1]
          if 15 < year < 78: continue          # year between 1978 and 2015
          mins = 10 * p[2] + p[3]
          
          # built date string
          dt = int(("19" if year > 77 else "20") + str(year) +  mdh + str(mins))
                   
          if dt < mx_dt: continue              # skip if earlier year than maximum
          
          # not divisible by any perfect square for new numbers ...
          p2 = mins * year
          if not all(p2 % (n * n) 
                     for n in [2] + list(range(3, int(p2**.5) + 1, 2))): continue
          # ... and for all five 2-digit numbers 
          p2 *= p1 
          if not all(p2 % (n * n) 
                     for n in [2] + list(range(3, int(p2**.5) + 1, 2))): continue
         
          mx_dt = dt                           # new maximum
          break                                # as p is decreasing
          
      s = str(mx_dt) 
      print(f"last 'non-square' pandigital time: "
            f"{s[6:8]}/{s[4:6]}/{s[:4]} {s[8:10]}:{s[10:]}") 
      

      and

        
      days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      
      # pick one value from each entry of a <k>-dimensional list <ns>
      # so that all digits in the <k> values are different
      def pickOneFromEach(k, ns, s=[], dgts=set()):
        if k == 0:
          yield s
        else:
          for n in ns[k - 1]:
            sn = str(n).zfill(2)
            if all(x not in dgts for x in sn):
              yield from pickOneFromEach(k - 1, ns, s + [sn], dgts | set(sn))
      
      # months, days and hours have to use the digits 0, 1 and 2 
      # months 10 and 12 are invalid as they use two of the digits 0, 1 and 2
      # and leave no room for day and hour so month < 10
      ys = list(n for n in range(34, 100) 
                if n % 11 and all(y not in str(n) for y in "012"))
      ms = list(range(1, 10))
      ds = list(n for n in range(13, 32) if n % 11 and n % 10)
      hs = list(n for n in range(24) if n % 11 and n % 10)
      mis = list(n for n in range(34, 60) 
                 if n % 11 and all(y not in str(n) for y in "012"))
      
      rev_lst = [mis, hs, ds, ms, ys]
      
      for p in pickOneFromEach(5, rev_lst):
        s = "".join(p)
        # sum has to be a perfect square
        if sum([int(x) for x in p]) not in {144, 169, 196}: continue
        
        # check day number
        m, d = (int(p[1]), int(p[2]))
        if d > 28 and m == 2:
          if year % 4 == 0 and (year % 100 or year % 400 == 0):
            days[1] = 29
          else:
            days[1] = 28
        if d > days[m - 1]:  continue  
        
        print(f" first 'square-sum' pandigital time: "
              f"{p[2]}/{p[1]}/{p[0]} {p[3]}:{p[4]}")  
        break       # we have found the first date 
      

      Like

  • Unknown's avatar

    Jim Randell 12:47 pm on 4 September 2022 Permalink | Reply
    Tags: by: A. R. Legard   

    Brain-Teaser 118: [Ceremonial weights] 

    From The Sunday Times, 30th June 1963 [link]

    “On our last expedition to the interior”, said the famous explorer, “we came across a tribe who had an interesting kind of Harvest Festival, in which every male member of the tribe had to contribute a levy of grain into the communal tribal store. Their unit of weight was roughly the same as our pound avoirdupois, and each tribesman had to contribute one pound of grain for every year of his age”.

    “The contributions were weighed on the tribe’s ceremonial scales, using a set of seven ceremonial weights. Each of these weighed an integral number of pounds, and it was an essential part of the ritual that not more than three of them should be used for each weighing, though they need not all be in the same pan”.

    “We were told that if ever a tribesman lived to such an age that his contribution could no longer be weighed by using three weights only, the levy of grain would terminate for ever. And in the previous year, one old man had died only a few months short of attaining this critical age, greatly to the relief of the headman of the tribe”.

    “The scientist with our expedition confirmed that the weights had been selected so that the critical age was the maximum possible”.

    What was the age of the old man when he died?

    And what were the weights of the seven ceremonial weights?

    This puzzle was originally published with no title.

    [teaser118]

     
    • Jim Randell's avatar

      Jim Randell 12:50 pm on 4 September 2022 Permalink | Reply

      I had a look at this puzzle, as according to the published answer: “No correct solution [was] received”.

      But the published solution does not seem to be the best answer to the puzzle.

      The following program considers sets of weights by increasing total weight. I assumed that all the weights were different (as this seems like an obvious way to increase the number of different combinations we can make, but I don’t want to make too many assumptions, as that might be what tripped up the original setter. Duplicate weights are useful for lower values of the total weight, but not as the total weight increases).

      However it is not quick. I left it running overnight, and it checked sets with a total weight of up to 350 lbs.

      from enigma import (Accumulator, irange, inf, decompose, subsets, peek, printf)
      
      # find the first unreachable weight using weights <ws>
      def unreachable(ws):
        # find possible weights using a single weight
        vs = set(ws)
        # using 2 weights
        for (a, b) in subsets(ws, size=2, select="C"):
          vs.add(b + a)
          vs.add(b - a)
        # using 3 weights
        for (a, b, c) in subsets(ws, size=3, select="C"):
          vs.add(c + b + a)
          vs.add(c + b - a)
          vs.add(c - b + a)
          vs.add(abs(c - b - a))
        # find the first unreachable weight
        return peek(v for v in irange(1, inf) if v not in vs)
      
      # accumulate maximum first unreachable weight
      r = Accumulator(fn=max, collect=1)
      
      # consider possible totals for the 7 weights
      for t in irange(28, 350):
        # find values for the 7 weights (all different)
        for ws in decompose(t, 7, increasing=1, sep=1, min_v=1):
          # find the first unreachble age
          v = unreachable(ws)
          # overall best
          r.accumulate_data(v, ws)
          if v == r.value: printf("[t={t}: {v} -> {ws}]")
      
      # output best found in range
      printf("unreachable weight = {r.value}")
      for ws in r.data:
        printf("-> {ws}; total = {t}", t=sum(ws))
      

      (You might want to reduce the range at line 24 if you run this program. e.g. To find the improved solutions you might want to consider t = 238 … 244, this takes about 40 minutes).

      The published solution was that the man was 120 years old when he died (i.e. the first unreachable weight was 121 lbs).

      And the seven weights were: 1, 3, 9, 14, 40, 70, 103 lb (total = 240 lb).

      However this is not the only set of weights that can allow every amount between 1 and 120 lb to be weighed using up to 3 weights, for example:

      (1, 3, 7, 12, 42, 75, 100); total = 240; weighs (1 … 120)
      (3, 5, 15, 16, 33, 75, 99); total = 246; weighs (1 … 120)
      (2, 5, 6, 15, 43, 74, 103); total = 248; weighs (1 … 120)

      So the published solution is not unique.

      However, there are combinations of weights that allow us to weigh greater contiguous amounts:

      (2, 4, 7, 22, 35, 78, 90); total = 238; weighs (1 … 121)
      (1, 3, 10, 15, 38, 72, 103); total = 242; weighs (1 … 121)
      (1, 3, 7, 12, 43, 76, 102); total = 244; weighs (1 … 122)

      The first of these has a lower total weight than the published solution.

      And the final set is the best I have found and gives the following solution to the puzzle:

      Solution: The old man was 122 years old when he died. The weights were: 1, 3, 7, 12, 43, 76, 102 lb.

      With this set the first unreachable weight is 123 lb, and as this is set is the only set that can weigh 1 … 122 lb the solution is unique.

      So it is possible that The Sunday Times may have received correct solutions, it is just that they didn’t match the setters incorrect solution. I checked subsequent Teaser puzzles, but was not able to find an apology or correction.


      All the sets that allow weights of (1 … 119) to be made lie between total = (213 … 272), so having checked all totals up to 350 it seems reasonable to assume that this solution may be optimal.

      Here is a plot of the first unreachable weight (y) vs. total weight (x). We see that the maximum is achieved at x = 244, and as the total weight increases the first unreachable weight is tending down.


      See also: Pages 5-7 of Optima 65 (May 2001) [ PDF ] [ PDF ], which reaches the same conclusion, using a considerable amount of analysis and also computing time. (I found this reference by searching the web for the improved answer found by my program).

      Like

      • Jim Randell's avatar

        Jim Randell 2:56 pm on 4 September 2022 Permalink | Reply

        I think I have convinced myself that there is no use having a weight greater than (roughly) 2t/3, otherwise we create a gap at (roughly) t/3.

        So we can provide the call to [[ decompose() ]] with an argument of max_v=divf(2 * t + 2, 3).

        Which will allow the code to run a little faster.

        Like

    • Hugh+Casement's avatar

      Hugh+Casement 5:03 pm on 4 September 2022 Permalink | Reply

      Hard to believe that a man in what is presumably a village somewhere in Africa (where in the past even 50 counted as very old) could reach an age of 120 or so.

      Would we find a more reasonable solution if there were only six weights in the set?

      Like

      • Jim Randell's avatar

        Jim Randell 5:15 pm on 4 September 2022 Permalink | Reply

        @Hugh: For 3 out of 6 weights I found the best set is:

        (1, 3, 17, 22, 51, 61)

        which can weigh 1 … 84.

        Like

        • Hugh+Casement's avatar

          Hugh+Casement 6:51 pm on 4 September 2022 Permalink | Reply

          Thanks for the swift reply, Jim.
          That looks a much more reasonable solution
          — I mean a much more reasonable puzzle.

          Like

    • Frits's avatar

      Frits 11:42 pm on 31 May 2024 Permalink | Reply

      Same concept but more efficient (if possible to run with PyPy).
      I also tried decompose() with N-2 and N-3 but that was not as efficient.

      For 8 weights the best set looks to be:

      (1, 3, 9, 14, 21, 59, 108, 148)

      which can weigh 1 … 172.

      from itertools import combinations
      from sys import argv
      
      N = 7 if len(argv) != 2 else int(argv[1])
      
      tri = lambda n: (n * (n + 1)) // 2
      
      # decompose number <t> into <k> increasing numbers (minimum <mn>)
      # so that sum(<k> numbers) equals <t>
      def decompose(t, k=6, mn=1, s=tuple()):
        if k == 1:
          if s[-1] < t:
            yield s + (t, )
        else:
          # can we make <t - n> in <k - 1> decompositions?
          for n in range(mn if not s else s[-1] + 1, (t - k * (k - 3) // 2) // k):
            yield from decompose(t - n, k - 1, mn, s + (n, ))
        
      def solve(ws, opt):
        c2 = set()
        # find possible weights using 2 weights
        for (a, b) in combinations(ws, 2):
          c2.update([b + a, b - a])
        # possible weights using 1 or 2 weights
        vs = set(ws) | c2   
        one_or_two = vs.copy()
        
        # using 3 weights
        for (a, b, c) in combinations(ws, 3):
          vs.update([c + b + a, c + b - a, c - b + a, abs(c - b - a)])
        
        # number of new numbers to consider
        extra = 50 if opt < 95 else 10
        # unreachable numbersfor all combinations in <ws>
        missing = [i for i in range(1, opt + 10) if i not in vs]
        
        # find an additional higher weight that reaches a lot of unreachable numbers
        higher_ws = set(range(ws[-1] + 1, max(ws[-1] + extra, opt + extra)))
        
        for m in missing:
          for element in higher_ws: break  # get any element from set
          # which remaining numbers greater than ws[-1] can reach numbers 1...m
          higher_ws &= {m} | {abs(x * i + m) for i in one_or_two for x in [-1, 1]}
          
          if not higher_ws: 
            return (m, element)  # number <m> is unreachable
        
        # all missing numbers could be reached, try again with more missing numbers
        return solve(ws, opt + 50)
         
      mx, seq = 0, []
      # consider possible totals for the first <N - 1> weights
      for t in range(tri(N - 1), 200):  # adapt for different N or a bigger range
        # find values for the <N - 1> weights (all different)
        for ws in decompose(t, N - 1):
          # find the first unreachable age
          v, k = solve(ws, mx - 1 if mx else t)
          # overall best
          if v > mx: 
            seq = ws + (k, )
            print(f"[total first {N - 1} weights={t}: {v} -> {seq}]")
            mx = v
            
      # output best found in range
      print(f"unreachable weight = {mx}")
      print(f"-> {seq}")     
      

      Like

  • Unknown's avatar

    Jim Randell 4:29 pm on 2 September 2022 Permalink | Reply
    Tags:   

    Teaser 3128: Tetrahedral towers 

    From The Sunday Times, 4th September 2022 [link] [link]

    I have a large number (but fewer than 2000) of identical spherical bonbons, arranged exactly as a tetrahedral tower, having the same number of bonbons along each of the six edges of the tower, with each bonbon above the triangular base resting on just three bonbons in the tier immediately below.

    I apportion all my bonbons between all my grandchildren, who have different ages in years, not less than 5, so that each grandchild can exactly arrange his or her share as a smaller tetrahedral tower, having the same number of tiers as his or her age in years.

    The number of my grandchildren is the largest possible in these circumstances.

    How many tiers were in my original tower? And how old in years are the eldest and youngest of my grandchildren?

    [teaser3128]

     
    • Jim Randell's avatar

      Jim Randell 4:46 pm on 2 September 2022 Permalink | Reply

      This Python program constructs the tetrahedral numbers we are interested in, and then looks for subsets that also sum to a tetrahedral number.

      It runs in 178ms.

      Run: [ @replit ]

      from enigma import (irange, inf, tri, reverse, subsets, Accumulator, printf)
      
      # compute tetrahedral numbers, n >=5, tetra[n] < 2000
      tetra = dict()
      t = 0
      for n in irange(1, inf):
        t += tri(n)
        if not (t < 2000): break
        if n < 5: continue
        tetra[n] = t
      rev = reverse(tetra)
      
      # consider possible ages of grandchildren
      r = Accumulator(fn=max, collect=1)
      for ns in subsets(sorted(tetra.keys()), min_size=2):
        t = sum(tetra[n] for n in ns)
        n = rev.get(t)
        if n:
          r.accumulate_data(len(ns), (ns, n))
      
      # output solution
      printf("{r.value} grandchildren")
      for (ns, n) in r.data:
        printf("-> ages = {ns}; tetra[{n}] = {t}", t=tetra[n])
      

      Solution: The original tower had 19 tiers. The eldest grandchild is 12 years old. The youngest grandchild is 5 years old.

      In total there are tetra[19] = 1330 bonbons.

      There are 8 grandchildren. Their ages are: 5, 6, 7, 8, 9, 10, 11, 12.

      And:

      tetra[5] + tetra[6] + tetra[7] + tetra[8] + tetra[9] + tetra[10] + tetra[11] + tetra[12]
      = 35 + 56 + 84 + 120 + 165 + 220 + 286 + 364
      = 1330
      = tetra[19]


      There are possible sets of grandchildren ages of size: 2, 3, 4, 5, 6, 7, 8:

      2: (8, 14) → 15

      3: (5, 6, 12) → 13
      3: (11, 12, 15) → 19

      4: (5, 6, 9, 14) → 16
      4: (5, 8, 11, 19) → 21
      4: (5, 11, 12, 13) → 18
      4: (6, 8, 13, 18) → 21
      4: (6, 9, 10, 19) → 21
      4: (8, 9, 11, 17) → 20
      4: (8, 11, 12, 14) → 19

      5: (5, 6, 7, 9, 10) → 14
      5: (5, 7, 11, 13, 15) → 20
      5: (6, 7, 10, 12, 16) → 20

      6: (5, 6, 7, 8, 9, 10) → 15
      6: (5, 6, 7, 8, 9, 15) → 18
      6: (5, 6, 7, 10, 14, 16) → 21
      6: (5, 7, 8, 11, 13, 14) → 20
      6: (6, 7, 9, 10, 13, 14) → 20
      6: (6, 7, 9, 11, 12, 16) → 21
      6: (6, 9, 10, 11, 12, 15) → 21
      6: (7, 8, 9, 10, 11, 13) → 19

      7: (6, 8, 9, 10, 11, 12, 14) → 21

      8: (5, 6, 7, 8, 9, 10, 11, 12) → 19

      The largest number of grandchildren (8) has only one corresponding set, and this gives the answer to the puzzle.

      Like

      • Jim Randell's avatar

        Jim Randell 10:33 pm on 2 September 2022 Permalink | Reply

        Or a bit longer, but faster.

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

        Run: [ @replit ]

        from enigma import (irange, inf, tri, reverse, append, Accumulator, printf)
        
        # compute tetrahedral numbers, n >=5, tetra[n] < 2000
        tetra = dict()
        t = mx = 0
        for n in irange(1, inf):
          t += tri(n)
          if not (t < 2000): break
          if n < 5: continue
          tetra[n] = t
          mx = t
        rev = reverse(tetra)
        
        # make sums of tetras
        sums = {(): 0}
        for k in sorted(tetra.keys()):
          v = tetra[k]
          xs = dict((append(ns, k), t + v) for (ns, t) in sums.items() if not (t + v > mx))
          sums.update(xs)
        
        # look for sums that are themselves tetras
        r = Accumulator(fn=max, collect=1)
        for (ns, t) in sums.items():
          n = rev.get(t)
          if n:
            r.accumulate_data(len(ns), (ns, n))
        
        # output solution
        printf("{r.value} grandchildren")
        for (ns, n) in r.data:
          printf("-> ages = {ns}; tetra[{n}] = {t}", t=tetra[n])
        

        Like

    • Robert Brown's avatar

      Robert Brown 5:21 pm on 2 September 2022 Permalink | Reply

      I found this:

      How many tiers were in my original tower, and how old in years are the eldest and youngest of my grandchildren?

      Like

    • GeoffR's avatar

      GeoffR 10:33 pm on 2 September 2022 Permalink | Reply

      # Full tetrahedral list below 2000, starting at 5th number
      # Ref: http://oeis.org/A000292 - assume youngest age = 5
      
      # List starting from 5th tetrahedral number
      Tet_Nums = [35, 56, 84, 120, 165, 220, 286, 364, 455, 560,
      680, 816, 969, 1140, 1330, 1540, 1771]
      
      # Dictionary to reference tetrahedral numbers and ages
      ages = range(5, 22)
      TN = dict(zip(Tet_Nums, ages))
      
      # Iterate from both ends of tetrahedral numbers list
      # Largest possible number needed from end of list
      for a in reversed(Tet_Nums):
        nsum = 0
        L =[]  # list of grandchildren's tetrahedral numbers
        for b in (Tet_Nums):
          nsum += b
          L.append(b) 
          if nsum > a:
            continue
          if nsum == a:
            print('Tiers in original tower = ', TN[nsum], "no.")
            for x in L:
              print('Grandchild\'s Age =', TN[x], "years")
            exit()   # single solution only needed
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 3:15 pm on 4 September 2022 Permalink | Reply

        @GeoffR: It looks like this code looks for sets of consecutive ages starting from 5. Which is somewhat more specific than the puzzle text requires.

        Like

    • GeoffR's avatar

      GeoffR 5:03 pm on 4 September 2022 Permalink | Reply

      @Jim: Yes, you are correct..

      Like

  • Unknown's avatar

    Jim Randell 9:23 am on 1 September 2022 Permalink | Reply
    Tags:   

    Teaser 2732: Prime meat 

    From The Sunday Times, 1st February 2015 [link] [link]

    Mark has recently converted from vegetarianism. John sent him a coded message consisting of a list of prime numbers. Mark found that by systematically replacing each digit by a letter the list became the message:

    EAT BEEF AT TIMES
    IT IS A PRIME MEAT

    What number became PRIME?

    [teaser2732]

     
    • Jim Randell's avatar

      Jim Randell 9:29 am on 1 September 2022 Permalink | Reply

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

      The following Python program executes in 58ms. (Internal runtime of the generated program is 1.8ms).

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, sprintf)
      
      words = "EAT BEEF AT TIMES IT IS A PRIME MEAT"
      
      SubstitutedExpression(
        list(sprintf("is_prime({w})") for w in words.split()),
        answer="PRIME",
        template=words,
      ).run()
      

      Solution: PRIME = 80429.

      Like

    • GeoffR's avatar

      GeoffR 11:53 am on 1 September 2022 Permalink | Reply

      This solution gets the answer, but is a bit slow – approx 1.5 sec.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) 
      ((i < x) -> (x mod i > 0));
      
      var 0..9:E; var 0..9:A; var 0..9:T; var 0..9:B;
      var 0..9:F; var 0..9:I; var 0..9:M; var 0..9:S;
      var 0..9:P; var 0..9:R;
      
      constraint E > 0 /\ B > 0 /\I > 0 /\ P > 0
      /\ T > 0 /\ A > 0 /\ M > 0;
      
      constraint all_different ([E, A, T, B, F, I, M, S, P, R]);
      
      var 100..999:EAT = 100*E + 10*A + T;
      var 1000..9999:BEEF = 1000*B + 110*E + F;
      var 10..99:AT = 10*A + T;
      var 10..99:IS = 10*I + S;
      var 10..99:IT = 10*I + T;
      var 10000..99999:TIMES = 10000*T + 1000*I + 100*M + 10*E + S;
      var 10000..99999:PRIME = 10000*P + 1000*R + 100*I + 10*M + E;
      var 1000..9999:MEAT = 1000*M + 100*E + 10*A + T;
      
      constraint sum([is_prime(A), is_prime(EAT), is_prime(BEEF),
      is_prime(AT), is_prime(TIMES), is_prime(IT), is_prime(IS),
      is_prime(PRIME), is_prime(MEAT)]) == 9;
      
      solve satisfy;
      output ["PRIME = " ++ show(PRIME) ++
      "\n" ++ "[E, A, T, B, F, I, M, S, P, R] = " 
      ++ show( [E, A, T, B, F, I, M, S, P, R] ) ];
      
      % PRIME = 80429
      % ----------
      %  E, A, T, B, F, I, M, S, P, R] = 
      % [9, 5, 3, 6, 1, 4, 2, 7, 8, 0]
      % ==========
      

      Like

    • GeoffR's avatar

      GeoffR 1:56 pm on 1 September 2022 Permalink | Reply

      from itertools import permutations
      from enigma import is_prime
      
      for p1 in permutations('1234567890', 5):
          A, E, I, T, S = p1
          if '0' in (A, E, I, T):continue
          if not(is_prime(int(A))):continue
          AT = int(A + T)
          if not (is_prime(AT)):continue
          IS = int(I + S)
          if not (is_prime(IS)):continue
          IT = int(I + T)
          if not (is_prime(IT)):continue
          EAT = int(E + A + T)
          if not (is_prime(EAT)):continue
          q = set('1234567890').difference([A, E, I, T, S])
          for p2 in permutations(q):
              B, F, M, P, R = p2
              if '0' in (B, M, P):continue
              BEEF = int(B + E + E + F)
              if not (is_prime(BEEF)):continue
              TIMES = int(T + I + M + E + S)
              if not (is_prime(TIMES)):continue
              MEAT = int(M + E + A + T)
              if not (is_prime(MEAT)):continue
              PRIME = int( P + R + I + M + E)
              if is_prime(PRIME):
                  print(f"PRIME = {PRIME}")
      
      # PRIME = 80429
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:31 am on 30 August 2022 Permalink | Reply
    Tags:   

    Teaser 2743: Line-up 

    From The Sunday Times, 19th April 2015 [link] [link]

    I have arranged the numbers from 1 to 27 in a 3-by-3-by-3 cubical array. I have noticed that the nine numbers making up one of the faces of the cube are all primes.

    Also, I have searched through the array and written down the sum of any three numbers that are in a straight line. I have then calculated the grand total of all those line-sums. It turns out that the grand total is itself a perfect cube!

    What is that grand total?

    [teaser2743]

     
    • Jim Randell's avatar

      Jim Randell 9:32 am on 30 August 2022 Permalink | Reply

      Considering a standard 3×3×3 Rubik’s cube, we can think of is as consisting of 27 cubelets:

      8 corner pieces (with 3 colours)
      12 edge pieces (with 2 colours)
      6 middle pieces (with 1 colour)
      1 core piece (with 0 colours)

      And if we consider the number of lines each type of cubelet appears in we have:

      corner = 7 lines
      edge = 4 lines
      middle = 5 lines
      core = 13 lines

      The grand total then consists of:

      7× (8 corner pieces)
      4× (12 edge pieces)
      5× (6 centre pieces )
      13× (1 core piece)

      And as long as we distribute the numbers amongst the same type of piece any arrangement will have the same grand total. (Although to confirm with the layout specified in the puzzle you have to keep the primes all together on one face).

      There are only nine primes between 1 and 27 (= 2, 3, 5, 7, 11, 13, 17, 19, 23) and these are arranged on one of the faces, so we have 4 corners, 4 edges, and 1 centre that are prime.

      So, we just need to split the primes into sets of size (4, 4, 1) with sums (p1, p2, p3), and the remaining 18 non-primes into sets of size (4, 8, 5, 1) with sums (n1, n2, n3, n4), such that:

      T = 7(p1 + n1) + 4(p2 + n2) + 5(p3 + n3) + 13 n4 [= a perfect cube]

      This can be written as:

      T = 4(p1 + p2 + p3 + n1 + n2 + n3 + n4) + 3(p1 + n1) + (p3 + n3) + 9 n4

      and we know that: (p1 + p2 + p3 + n1 + n2 + n3 + n4) = tri(27), so:

      T = 4 tri(27) + 3(p1 + n1) + (p3 + n3) + 9 n4

      So the maximum possible value for T is 2363, giving a maximum possible cube of 13³ = 2197.

      And the minimum possible value for T is 1731, giving a minimum possible cube of 13³ = 2197.

      So, the only possibility for a grand total that is a cube is 13³ = 2197.

      Solution: The grand total is 2197.


      But now we need to show there is at least one solution.

      If we do find a solution we can permute the numbers amongst their own groups without changing the grand total to give a class with (4! × 4! × 1!) × (4! × 8! × 5! × 1!) = 66886041600 solutions. (Which we can divide by 8 to account for reflections/rotations).

      This Python program chooses a non-prime core value, and partitions the primes into sets of size (4, 1) and the remaining non-primes into sets of size (4, 5), and in each case the contribution to the grand total is recorded. We then look for pairs of contributions that can make a grand total that is a perfect cube. (There are many of these, so we stop when we have found a single example for a particular cube).

      It runs in 13s and finds there are many possible core values that lead to viable solutions (and each of these has many partitions sums that lead to many solutions).

      Run: [ @replit ]

      from enigma import (irange, primes, subsets, intc, intf, cbrt, cb, printf)
      
      # partition set <s> into subsets with sizes in <ns>
      def partition(s, ns, ss=[]):
        if not ns:
          yield ss
        elif len(ns) == 1:
          for x in subsets(s, size=ns[0]):
            yield ss + [x]
        else:
          for x in subsets(s, size=ns[0]):
            yield from partition(s.difference(x), ns[1:], ss + [x])
      
      # primes
      ps = set(primes.between(1, 27))
      
      # non-primes
      nps = set(irange(1, 27)).difference(ps)
      
      # total of primes and non-primes (= tri(27))
      t = sum(ps) + sum(nps)
      
      # record grand totals
      Ts = set()
      
      # split the primes into sets of size (4, 1) and record the contribution to the total
      pss = set(3 * sum(p1) + sum(p3) for (p1, p3) in partition(ps, (4, 1)))
      printf("[{n} prime partitions]", n=len(pss))
      
      # choose the core value (non-prime)
      for v in nps:
        printf("[core={v}]")
      
        # split the remaining non-primes into sets of size (4, 5) and record the contribution to the total
        nss = set(3 * sum(n1) + sum(n3) for (n1, n3) in partition(nps.difference({v}), (4, 5)))
        printf("[{n} non-prime partitions]", n=len(nss))
      
        # find possible cubes
        t0 = 4 * t + 9 * v
        cubes = set(cb(x) for x in irange(intc(cbrt(t0 + min(pss) + min(nss))), intf(cbrt(t0 + max(pss) + max(nss)))))
        printf("[possible cubes = {cubes}]", cubes=sorted(cubes))
      
        # find pairs from pss and nss that give a cube grand total
        for T in cubes:
          for p in pss:
            n = T - t0 - p
            if n > 0 and n in nss:
              printf("[p={p} n={n} -> T={T}]")
              Ts.add(T)
              break  # we only need one example for this T
      
      # output solution
      printf("grand total = {Ts}")
      

      There are many, many ways to place the numbers on the cube.

      Here is one (this has 8 as the core value – the smallest possible, but it can be any non-prime value between 8 and 27):

      In this solution the different types of cubelet are (prime + non-prime values):

      8 corners = (7, 17, 19, 23) + (24, 25, 26, 27); sum = 168
      12 edges = (2, 3, 5, 11) + (1, 4, 6, 9, 10, 12, 14, 16); sum = 93
      6 middles = (13) + (15, 18, 20, 21, 22); sum = 109
      1 core = () + (8); sum = 8

      And so the grand total is:

      7×168 + 4×93 + 5×109 + 13×8 = 2197 = 13³

      Like

  • Unknown's avatar

    Jim Randell 8:49 am on 28 August 2022 Permalink | Reply
    Tags: by: R. K. Brown   

    Brain-Teaser 906: Truthful salesman 

    From The Sunday Times, 2nd December 1979 [link]

    Seated happily one Sunday morning in my local pub, solving the Brain-teaser, I was accosted by the slightly inebriated resident bore. He said: “I have a puzzle for you which concerns seven people who were either engineers or salesmen and, of course, salesmen always tell the truth and engineers always lie. I will make four statements from which you must deduce how many salesmen there are:

    1. B and E are salesmen;
    2. A and C have different occupations;
    3. D says that G is an engineer;
    4. A says that B declares that C insists that D asserts that E affirms that F states that G is a salesman”.

    I said: “That’s a very old one”, and told him the answer. Grinning, he then replied, “Yes, that would be the correct answer if all the four statements were true, but it is not the correct answer because I intentionally made one of the four statements false. Further, if were to tell you how many salesmen there were under these new circumstances, you would be able to tell me which statement was false”.

    “Ah”, I said, “that’s a much more interesting problem”. I thought for a moment and indeed I was able to tell him not only which statement was false but also how many salesmen there really were.

    Which statement was false and how many salesmen were there really?

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

    [teaser906]

     
    • Jim Randell's avatar

      Jim Randell 8:50 am on 28 August 2022 Permalink | Reply

      This Python program runs in 54ms. (Internal runtime is 233µs).

      Run: [ @replit ]

      from collections import defaultdict
      from enigma import (subsets, peek, printf)
      
      # salesman or engineer?
      jobs = (Sales, Eng) = (True, False)
      
      # does X make statements s?
      check = lambda X, s: (X == s)
      
      # map number of salesmen to index of false statement
      m = defaultdict(set)
      
      # choose jobs for A - G
      for (A, B, C, D, E, F, G) in subsets(jobs, size=7, select="M"):
      
        # evaluate the statements:
        ss = [
          # 1. "B and E are salesmen"
          (B == E == Sales),
          # 2. "A and C have different occupations"
          (A != C),
          # 3. "D says that G is an engineer"
          check(D, G == Eng),
          # 4. "A says that B declares that C insists that D asserts that E affirms that F states that G is a salesman
          check(A, check(B, check(C, check(D, check(E, check(F, G == Sales)))))),
        ]
      
        # exactly one statement is false
        if ss.count(False) == 1:
          m[[A, B, C, D, E, F, G].count(Sales)].add(ss.index(False))
      
      # look for unique values
      for (k, vs) in m.items():
        if len(vs) == 1:
          v = peek(vs) + 1
          printf("{k} salesmen -> statement {v} is false")
      

      Solution: The false statement is number 4. And there are 4 salesmen.

      And there are 4 possible arrangements of salesmen:

      A, B, D, E
      A, B, E, G
      B, C, D, E
      B, C, E, G


      If all statements are true then there are 5 salesmen. The possible arrangements are:

      A, B, D, E, F
      A, B, E, F, G
      B, C, D, E, F
      B, C, E, F, G

      Like

    • John Crabtree's avatar

      John Crabtree 3:19 am on 30 August 2022 Permalink | Reply

      I was introduced to the original puzzle in Paul Nahin’s book, “The Logician and Engineer”, Princeton University Press, 2013, pp 58-59 and 65-66. He gave these references:
      i) D.M. McCallum and J.B. Smith, “Mechanized Reasoning: Logical Computers and their Design”, Electronic Engineering (a UK magazine), April 1951, pp 126-133 (page nos. not given by Nahin)
      ii) C. E. Shannon, “Computers and Automata”, Proc. of the Institute of Radio Engineers (U.S.A.), Oct 1953, pp 1234-1241. Nahin notes that there was a typo in Shannon’s representation of McCallum and Smith’s problem. I believe that it does not affect the nature of, or the solution to, the problem.

      A asserts B may be written as A XNOR B = 1, ie both true or both false.
      St. 4, if true, may be written as A XNOR (B XNOR (C XNOR (D XNOR (E XNOR (F XNOR G))))) = 1
      And so A XNOR B XNOR C XNOR D XNOR E XNOR F XNOR G = 1
      X XNOR Y XNOR Z = 1 is equivalent to X XOR Y XOR Z = 1
      And so St. 4, if true, may be written as
      A XOR B XOR C XOR D XOR E XOR F XOR G = 1
      This is familiar to electrical engineers as the expression for a half-adder circuit ie binary addition ignoring the carry. And so if St. 4 is true, there are an odd number of salesman, and if it is false there are an even number of salesmen. (This method is not given by Nahin).

      If all of the statements are true, there are 5 salesmen including F. If St. 1 is false, there are 3 salesmen. If Sts. 2 or 3 are false, there can be 3 or 5 salesmen.

      And so St. 4 is false and there are 4 salesmen (with F being an engineer).

      Like

  • Unknown's avatar

    Jim Randell 6:56 pm on 26 August 2022 Permalink | Reply
    Tags:   

    Teaser 3127: Putting in a shift 

    From The Sunday Times, 28th August 2022 [link] [link]

    Next month’s four week rota for Monday to Friday dinner duties starting on Monday 1st is covered by five teachers each having the following duty allocations. Ann, Ben and Ed each have four, Celia six and Dave has two. Strangely, all the prime number dates (1 is not a prime) are covered by Ben, Celia and Dave, while the others are covered by Ann, Celia and Ed. After working a duty, nobody works on either of the following two shifts, so anyone working on a Friday will not work on the following Monday or Tuesday. Celia has no duties on Mondays while Ben and Ed have none on Wednesdays.

    In date order, who is on duty from Monday to Friday of the first week?

    [teaser3127]

     
    • Jim Randell's avatar

      Jim Randell 7:32 pm on 26 August 2022 Permalink | Reply

      This Python program runs in 56ms. (Internal run time is 2.8ms).

      Run: [ @replit ]

      from enigma import (enigma, irange, multiset, append, chunk, join, printf)
      
      # dates to allocate (1 - 26 without Sat (6) or Sun (0))
      dates = list(n for n in irange(1, 26) if n % 7 not in {0, 6})
      
      # counts for each worker
      count = dict(A=4, B=4, C=6, D=2, E=4)
      
      # primes
      primes = set(enigma.primes.between(1, 26))
      
      # allocate candidates to a list of dates
      def solve(ds, i=0, vs=()):
        if i == len(ds):
          # check prime numbers are covered by B, C, D
          if set(v for (d, v) in zip(ds, vs) if d in primes) != set('BCD'): return
          # and non-primes are covered by A, C, E
          if set(v for (d, v) in zip(ds, vs) if d not in primes) != set('ACE'): return
          # valid solution
          yield vs
        else:
          # consider the next date
          k = ds[i]
          # day of week (Mon = 1 ... Fri = 5)
          w = k % 7
          # consider candidates for next shift
          for v in ('BCD' if k in primes else 'ACE'):
            if w == 1:
              if v == 'C': continue
            elif w == 3:
              if v in 'BE': continue
            if v not in vs[-2:] and vs.count(v) < count[v]:
              # solve for the rest
              yield from solve(ds, i + 1, append(vs, v))
      
      # format a week
      fmt = lambda x: join(x, sep=" ", enc="()")
      
      # find possible rotas and collect solutions (= rota for first week)
      ss = multiset()
      for vs in solve(dates):
        weeks = list(chunk(vs, 5))
        printf("[{weeks}]", weeks=join((fmt(x) for x in weeks), sep=" "))
        ss.add(weeks[0])
      
      # output solutions
      for (ks, n) in ss.most_common():
        printf("{ks} [{n} solutions]", ks=fmt(ks))
      

      Solution: The rota for the first week is (Mon – Fri): Ann, Ben, Dave, Celia, Ben.

      The complete rota is:

      Week 1: A B D C B
      Week 2: E C A B C
      Week 3: A E D C B; or: E A D C B
      Week 4: E C A E C


      The prime days (of which there are 7) are covered by B, C, D (which means all A and E’s days must be non-prime).

      Similarly the non-prime days are covered by A, C, E (which means all B and D’s days must be prime).

      So between them B (4 days) and D (2 days) must cover 6 of the 7 prime days, so C must have 1 prime (and 5 non-prime) days.

      We can use this analysis to simplify the program slightly, although the run time is already very quick.

      Like

    • Frits's avatar

      Frits 9:50 pm on 26 August 2022 Permalink | Reply

      Faster when run with PyPy.

         
      from enigma import SubstitutedExpression
      
      # days  (1 - 5, 8 - 12, 15 - 19, 22, 26)
      days = list(n for n in range(1, 27) if n % 7 not in {0, 6})
      
      # prime days up to 26
      P = {3, 5, 7}
      P = {2, 3, 5} | {x for x in range(9, 26, 2) if x in days and all(x % p for p in P)}
      
      # daynum: day --> day index
      d_ind = {d: i for i, d in enumerate(days)}
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 27):
        vs = set()
        if d == 0 or d not in days:
          vs.update('ABCDEFGHIJKLMNOPQRST')
          
        if d in P: 
          vs.update('AFGHERST')  # Ann and Ed
        else:
          vs.update('BIJKDQ')    # Ben and Dave
        
        if d % 7 == 1: 
          vs.update('CLMNOP')    # Celia not on Monday  
        if d % 7 == 3:
          vs.update('BIJKERST')  # Ben and Ed not on Wednesday 
         
        d2i[d] = vs
      
      # check gap of at least 2 work days
      def check(seq):
        for i, s in enumerate(seq[1:], start=1):
          if d_ind[s] - d_ind[seq[i - 1]] < 3: return False
        return True  
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "check([B, I, J, K])",
          "check([D, Q])",
          "check([C, L, M, N, O, P])",
          "check([A, F, G, H])",
          "check([E, R, S, T])",
        ],
        answer="(A, F, G, H, B, I, J, K, C, L, M, N, O, P, D, Q, E, R, S, T)",
        base=27,
        d2i=d2i,
        env=dict(check=check),
        reorder=0,
        denest=1,     # use denest=1 if not run with PyPy
        verbose=0,    # use 256 to see the generated code
      )
      
      order = "AAAABBBBCCCCCCDDEEEE"
      # collect answers
      sols = set()
      for (_, ans) in p.solve():
        mix = sorted(zip(ans, order))
        #print(f"{', '.join(x[1] for x in mix)}")
        sols.add(', '.join(x[1] for i, x in enumerate(mix) if i < 5))
      
      # print answers
      for s in sols:  
        print(s)  
      

      Like

    • Hugh+Casement's avatar

      Hugh+Casement 7:04 am on 27 August 2022 Permalink | Reply

      Next month’s rota?? It was the 1st of August that was a Monday this year.
      It seems the puzzles editor doesn’t read the puzzles.

      Like

    • Frits's avatar

      Frits 1:06 pm on 27 August 2022 Permalink | Reply

      Similar to my previous version but faster.
      I experienced some order problems with a set for P so I decided to use a tuple.

         
      from itertools import combinations
      
      # select items from <a> that do not occur in <b>
      diff = lambda a, b: tuple(x for x in a if x not in b)
      
      # days  (1 - 5, 8 - 12, 15 - 19, 22, 26)
      days = [n for n in range(1, 27) if n % 7 not in {0, 6}]
      
      # don't use a set for P as a set is an unordered collection in CPython
      # (results differ depending on the CPython release)
      
      # primes work days up to 26
      P = (3, 5)
      P = (2, 3, 5) + tuple(x for x in range(7, 27, 2) 
                       if x in days and all(x % p for p in P))
                       
      NP = diff(days, P)
      
      # day --> day index
      d_ind = {d: i for i, d in enumerate(days)}
      
      # check for a gap between shifts of at least 2 work days
      def check(seq):
        for i in range(len(seq) - 1):
          if d_ind[seq[i + 1]] - d_ind[seq[i]] < 3: return False
        return True  
        
      sols = set()
      for b4 in combinations(P, 4):
        if not check(b4): continue              # check gaps between shifts 
        # Ben not on Wednesday
        if any(x for x in b4 if x % 7 == 3): continue
        for d2 in combinations(diff(P, b4), 2):
          if not check(d2): continue            # check gaps between shifts 
          c1 = diff(P, b4 + d2)
          # Celia not on Monday
          if c1[0] % 7 == 1: continue
      
          for c5 in combinations(NP, 5):
            # Celia not on Monday
            if any(x for x in c5 if x % 7 == 1): continue
            c6 = tuple(sorted(c5 + c1))
            if not check(c6): continue        # check gaps between shifts 
              
            for e4 in combinations(diff(NP, c5), 4):
              if not check(e4): continue          # check gaps between shifts 
              # Ed not on Wednesday
              if any(x for x in e4 if x % 7 == 3): continue
              
              a4 = diff(NP, e4 + c5)
              if not check(a4): continue        # check gaps between shifts 
              
              mix = sorted(str(y).zfill(2) + "ABCDE"[i]
                    for i, x in enumerate([a4, b4, c6, d2, e4]) for y in x)
              sols.add(tuple(x[2] for x in mix[:5]))    
           
      # print answers
      for s in sols:  
        print("answer: ", *s) 
      

      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