Updates from October, 2022 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:00 pm on 7 October 2022 Permalink | Reply
    Tags:   

    Teaser 3133: Primary road network 

    From The Sunday Times, 9th October 2022 [link] [link]

    I was recently studying a large map that showed all the towns and major roads in a country. Every town had at least one road leading to it and every road led from one town to another. The roads only met at towns and all joined together to make a network with lots of blank areas in between, which I happily coloured in, needing just four different colours.

    I counted up the number of areas (excluding the area around the outside of the network) and the number of towns, and discovered that both numbers were prime. Also, when I took these two numbers with the number of roads, the three numbers together used every digit from 0 to 9 precisely once.

    In increasing order, what were the three numbers?

    [teaser3133]

     
    • Jim Randell's avatar

      Jim Randell 4:31 pm on 7 October 2022 Permalink | Reply

      This is an exercise in the properties of planar graphs [@wikipedia].

      We can use Euler’s formula:

      V – E + F = 2

      where:

      V = the number of vertices in graph (= the number of towns on the map)
      E = the number of edges in the graph (= the number of roads on the map)
      F = the number of enclosed regions in the graph (including the region around the graph)
      F = (the number of enclosed/coloured regions on the map) + 1

      This Python program runs in 93ms. (Internal run time is 38.6ms).

      Run: [ @replit ]

      from enigma import (primes, distinct_chars, ordered, printf)
      
      # consider V = the number of towns, it is prime
      for V in primes.between(3, 1000):
        if distinct_chars(V) is None: continue
      
        # consider A = the number of enclosed areas, it is also prime
        for A in primes.between(5, 2 * V - 5):
          if distinct_chars(V, A) is None: continue
      
          # calculate E = the number of roads
          E = V + A - 1
          if E > 3 * V - 6: continue
          if distinct_chars(V, A, E) != 10: continue
      
          # output solution
          ns = ordered(V, A, E)
          printf("{ns} [V={V} A={A} E={E}]")
      

      Solution: The three numbers are: 607, 829, 1435.


      We can construct a viable map as follows:

      Consider the following diagram:

      If the central area is an odd n-gon, coloured with the first colour, then it is surrounded by n regions, and as n is odd we require an additional 3 colours to colour these. So at least 4 colours are required, and the 4-colour theorem tells that we don’t need more than 4 colours.

      And together the central and surrounding areas contribute:

      2n vertices
      3n edges
      (n + 1) areas

      And adding p petals (we may add multiple layers of elongated petals if necessary), adds:

      0 vertices
      p edges
      p areas

      And a stalk of length s adds:

      s vertices
      s edges
      0 areas

      So using: n = 413, p = 193, s = 3, gives a graph with:

      826 + 0 + 3 = 829 vertices
      1239 + 193 + 3 = 1435 edges
      414 + 193 + 0 = 607 areas

      Which provides a viable map.

      Like

      • Jim Randell's avatar

        Jim Randell 8:33 am on 8 October 2022 Permalink | Reply

        Using the formula:

        E = p + q − 1

        where p and q are primes, and (E, p, q) is pandigital, we see that E must be 4-digits, so p and q can only be (2, 4) or (3, 3) digits.

        And without further planarity checks there is only one candidate solution, which we can find using the [[ SubstitutedExpression() ]] solver from the enigma.py library.

        The following Python program runs in 83ms. (Internal runtime is 28.4ms)

        Run: [ @replit ]

        from enigma import (SubstitutedExpression, sprintf as f, printf)
        
        # symbols for the digits
        (terms, result) = ("ABCDEF", "GHIJ")
        # consider (2,4) and (3, 3) digit terms
        for i in (2, 3):
          (x, y) = (terms[:i], terms[i:])
          exprs = [ f("is_prime({x})"), f("is_prime({y})"), f("{x} + {y} - 1 = {result}") ]
          if len(x) == len(y): exprs.append(f("{x} < {y}", x=x[0], y=y[0]))
          p = SubstitutedExpression(exprs, answer=f("({x}, {y}, {result})"))
          for ans in p.answers(verbose=0):
            printf("{ans}")
        

        Like

    • Frits's avatar

      Frits 12:31 am on 8 October 2022 Permalink | Reply

      This program considers up to 9999 number of towns.
      I stopped programming when the run time got under 10ms.

      NB The final P4 list is always logically empty, I didn’t remove the code to process 4-digit number of towns .

        
      from itertools import combinations
      
      # formula E = V + A - 1
      
      # V = the number of towns
      # A = the number of enclosed areas
      # E = the number of roads
      sols = set()
      
      P = [3, 5, 7]
      P += [x for x in range(11, 33, 2) if all(x % p for p in P)]
      P += [x for x in range(33, 1000, 2) if all(x % p for p in P)]
      
      # if V has 3 digits then E must be 1xxx
      # E must have at least 4 digits so V + 2V - 5 - 1 > 999 --> 3V > 1005
      # 3-digit primes
      P3 = [p for p in P if p > 335 and 
                           len(s := str(p)) == len(set(s)) and
                          '1' not in s]
      
      # first process 3-digit number of towns
      for V, A in combinations(P3, 2):
        if V + A < 1024: continue
        E = V + A - 1
        if len(set(str(1000000 * E + 1000 * A + V))) != 10: continue
        sols.add(tuple(sorted([V, A, E])))
        
      P = [3, 5, 7]
      P += [x for x in range(11, 100, 2) if all(x % p for p in P)]
      
      # if V has 4 digits then the 2nd digit of V, E must be resp 9, 0
      # if A ends in 1 then V and E will end in the same digit
      
      # 2-digit primes
      P2 = [p for p in P if p > 9 and 
                            all(y not in (s := str(p)) for y in "019") and
                            len(s) == len(set(s))]
                            
      # 4-digit primes
      P4 = [x for x in range(1001, 10000, 2) if all(x % p for p in P)]
      
      # if V has 4 digits the 2nd digit must be a nine (and may not use a zero)
      # otherwise E will use some of the same digits
      # if V ends in 1 then A and E will end in the same digit
      P4 = [p for p in P4 if (s := str(p))[1] == '9' and 
                             '0' not in s and
                             len(s) == len(set(s)) and
                             s[-1] != '1']
                             
      # numbers in P2 and P4 always end in 3 or 7  (1, 5, and 9 are not allowed)
      # so E must end in 9
      P2 = [p for p in P2 if '9' not in (s := str(p)) and p not in {37, 73}]
      P4 = [p for p in P4 if '9' not in (s := str(p)) and
                              all(y not in s[:-1] for y in "37")]
        
      # process 4-digit number of towns  
      for V in P4:
        # if V has 4 digits (x9yz) then A must be at least 101 - yz
        for A in [p for p in P2 if p >= 101 - V % 100]:
          E = V + A - 1
          if len(set(str(1000000 * E + 1000 * A + V))) != 10: continue
          sols.add(tuple(sorted([V, A, E])))
          
      # print solutions
      for s in sols:    
        print(s)
      

      Like

    • Hugh Casement's avatar

      Hugh Casement 6:32 am on 9 October 2022 Permalink | Reply

      I too had the idea of using Euler’s formula. At first I thought that a town with only a single road leading to it would spoil things, but then realized that they cancel out.

      Most unlikely that there would be a four-digit number of towns and only a two-digit number of regions between the roads, or vice versa. In any case, three plus three quickly yields a solution.
      I’ll spare you my program code, but Basic’s ability to handle strings was useful in determining whether all ten digits are present.

      Like

    • GeoffR's avatar

      GeoffR 11:44 am on 9 October 2022 Permalink | Reply

      I based my program on (3/3) and (2/4) digit splits for towns/areas, with 4 digits for roads.

      from enigma import nsplit, is_prime
       
      # form lists of 2, 3 and 4 digit primes
      # with non-repeating digits to check digit splits
      pr2 = [n for n in range(11, 99) if is_prime(n) \
             and len(set(str(n))) == 2]
      pr3 = [n for n in range(100, 999) if is_prime(n) \
             and len(set(str(n))) == 3]
      pr4 = [n for n in range(1000, 9999) if is_prime(n) \
             and len(set(str(n))) == 4 ]
       
      # check if (town/area) digits split is (3, 3)
      for town in pr3:
        A, B, C = nsplit(town)
        for area in pr3:
          if area == town:continue
          D, E, F = nsplit(area)
          if len(set((A, B, C, D, E, F))) != 6:continue
          roads = town + area - 1
          if roads in pr4: continue
          if roads < 1000 or roads > 9999:continue
          R1, R2, R3, R4 = nsplit(roads)
          if len(set((A,B,C,D,E,F,R1,R2,R3,R4))) == 10:
            if town < area:
              print(f"1.The three numbers were {town}, {area} and {roads}.")
       
      # check if (town/area) digits split is (2,4)
      for town2 in pr2:
        a, b = nsplit(town2)
        for area2 in pr4:
          c, d, e, f = nsplit(area2)
          if len(set((a, b, c, d, e, f))) == 6:
            roads2 = town2 + area2 - 1
            if roads2 < 1000 or roads2 > 9999: continue
            R5, R6, R7, R8 = nsplit(roads2)
            if len(set((a,b,c,d,e,f,R5,R6,R7,R8))) == 10:
              print(f"2.The three numbers were {town2}, {area2} and {roads2}.")
      

      Like

  • Unknown's avatar

    Jim Randell 7:56 am on 6 October 2022 Permalink | Reply
    Tags:   

    Teaser 2750: Granny’s birthdays 

    From The Sunday Times, 7th June 2015 [link] [link]

    At Granny’s birthday this year she was telling us some surprising things about some past birthdays. She told us that each year she writes down the date of her birthday (in the eight-digit form dd/mm/yyyy) and her age in years.

    On two occasions in her lifetime it has turned out that this has involved writing each of the digits 0 to 9 exactly once. The first of these occasions was in 1974.

    What is Granny’s date of birth (in the eight-digit form)?

    Note that in order to solve this puzzle it is important to be aware of the date it was originally set.

    All 200 puzzles included in the book The Sunday Times Brain Teasers Book 1 (2019) are now available on S2T2.

    [teaser2750]

     
    • Jim Randell's avatar

      Jim Randell 8:00 am on 6 October 2022 Permalink | Reply

      The puzzle was originally set on 7th June 2015, and mentions Granny’s birthday “this year” as having already happened. So her birthday must be earlier in the year than 7th June.

      This Python program considers dates in 1974 up to 6th June, if the date includes 8 different digits, then the remaining 2 digits indicate Granny’s age (in some order), and we can then look for other years.

      The program runs in 59ms. (Internal runtime is 4.3ms)

      Run: [ @replit ]

      from datetime import (date, timedelta)
      from enigma import (irange, repeat, nsplit, union, subsets, nconcat, printf)
      
      digits = set(irange(0, 9))
      
      # consider dates earlier than this
      end = date(1974, 6, 7)
      
      # consider dates in 1974
      inc = lambda x, i=timedelta(days=1): x + i
      for d in repeat(inc, date(1974, 1, 1)):
        if not (d < end): break
        # remaining digits
        ds4 = union([nsplit(d.month, 2), nsplit(d.day, 2)])
        ds = digits.difference(nsplit(d.year, 4), ds4)
        if len(ds) != 2: continue
        # construct possible ages
        for ss in subsets(ds, size=2, select='P'):
          age = nconcat(ss)
          year = 1974 - age
          # collect "special" years
          years = list()
          for bd in irange(10, 99):
            y = year + bd
            if y > 2015: break
            if not digits.difference(nsplit(y, 4), ds4, nsplit(bd, 2)):
              years.append(y)
          if len(years) == 2 and years[0] == 1974:
            # output solution
            printf("{d} -> {years}", d=date(year, d.month, d.day).strftime("%d/%m/%Y"))
      

      Solution: Granny’s date of birth is: 26/05/1936.

      So Granny is 38 in 1974 → (26/05/1974, 38).

      And she is 47 in 1983 → (26/05/1983, 47).

      In 2015 (when the puzzle was set) she was 79.

      If we consider all dates in 1974 then there is a further solution of: 25/06/1936.

      So, the puzzle only has a unique solution if posed between 27th May and 24th June. A fact that was not mentioned when the puzzle was included in the book The Sunday Times Brain Teasers 1 (2019).

      Like

  • Unknown's avatar

    Jim Randell 8:11 am on 4 October 2022 Permalink | Reply
    Tags:   

    Teaser 2751: Red-handed 

    From The Sunday Times, 14th June 2015 [link] [link]

    I removed an even number of red cards from a standard pack and I then divided the remaining cards into two piles. I drew a card at random from the first pile and it was black (there was a whole-numbered percentage chance of this happening). I then placed that black card in the second pile, shuffled them, and chose a card at random from that pile. It was red (and the percentage chance of this happening was exactly the same as that earlier percentage).

    How many red cards had I removed from the pack?

    [teaser2751]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 4 October 2022 Permalink | Reply

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

      Run: [ @replit ]

      from enigma import (irange, cproduct, div, printf)
      
      # start with 26 red, 26 black
      R = B = 26
      
      # remove an even (non-zero) number of red cards from the pack
      for k in irange(2, R, step=2):
      
        # pile 1 has r1 reds and b1 blacks (at least 1 black)
        for (r1, b1) in cproduct([irange(0, R - k), irange(1, B)]):
      
          # chance of drawing a black is a whole number percentage
          pb = div(100 * b1, r1 + b1)
          if pb is None: continue
      
          # a black card is moved to pile 2 which now has r2 reds and b2 blacks
          (r2, b2) = (R - k - r1, B - b1 + 1)
          # chance of a red card is also pb percent
          pr = div(100 * r2, r2 + b2)
          if pr is None or pr != pb: continue
      
          # output solution
          printf("k={k}: p1={r1}r + {b1}b -> pb={pb}%; p2={r2}r + {b2}b -> pr={pr}%")
      

      Solution: 8 red cards were removed from the pack.

      From the 18 red and 26 black cards the two piles can be made in two ways:

      pile 1: 6 red + 24 black; P(black) = 24/30 = 80%
      (1 black card is moved to pile 2)
      pile 2: 12 red + 3 black; P(red) = 12/15 = 80%

      pile 1: 12 red + 3 black; P(black) = 3/15 = 20%
      (1 black card is moved to pile 2)
      pile 2: 6 red + 24 black; P(red) = 6/30 = 20%

      Like

  • Unknown's avatar

    Jim Randell 8:36 am on 2 October 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 188: Family birthdays 

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

    I wrote to an American friend on the 3rd February 1964, and told him of the coincidence of our family birthdays. My wife and I, our three sons, and our four grandsons all have our birthdays on the same day of the week every year, though all our birthdays are different. When I wrote, I used the usual English form of the date — 3/2/64 — and I gave all our birthdays in that form also.

    My third son received a cable from my friend on his birthday in 1964, but later I was surprised to get a cable from him myself on my eldest son’s birthday. Next my eldest grandson received a cable on his right birthday, and I realised that we were all going to receive cables, but that my friend was, quite reasonably, reading the dates in the American form, i.e. he assumed that the letter had been sent on 2nd March 1964.

    However, I did not write to put him right, and my wife was the next person to get a cable; this was not on her birthday.

    What was the day and date of her birthday in 1964?

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

    [teaser188]

     
    • Jim Randell's avatar

      Jim Randell 8:37 am on 2 October 2022 Permalink | Reply

      The birthdays are on the same day of the week each year, so they cannot straddle 29th February.

      But in any case the letter was sent on 3/2, which was misinterpreted as 2nd March, so the birthdays must be after that date.

      This Python program runs in 57ms. (Internal run time is 434µs).

      Run: [ @replit ]

      from datetime import (date, timedelta)
      from enigma import (defaultdict, repeat, catch, subsets, printf)
      
      # reverse the day/month of a date
      rev = lambda d: catch(date, d.year, d.day, d.month)
      
      # is a date reversible?
      is_rev = lambda d: rev(d) == d
      
      # group days by day of the week
      g = defaultdict(list)
      # look for dates in 1964 that can be misinterpreted as US style dates
      inc = lambda x, i=timedelta(days=1): x + i
      for d in repeat(inc, date(1964, 1, 1)):
        if d.year > 1964: break
        # try US format
        d_ = rev(d)
        if not d_: continue
        # must be the same day of the week as the original date
        w = d.isoweekday()
        if d_.isoweekday() == w:
          g[w].append(d)
      
      # consider the day of the week
      for k in g.keys():
        # we need 9 dates the come after 2nd March
        for ds in subsets((d for d in g[k] if d > date(1964, 3, 2)), size=9):
          # the first birthday is the correct date (third son)
          if not is_rev(ds[0]): continue
          # the second birthday is not the correct date (eldest son, sent to setter)
          if is_rev(ds[1]): continue
          # the third birthday is the correct date (eldest grandson)
          if not is_rev(ds[2]): continue
          # the fourth birthday is not the correct date (sent to wife)
          if is_rev(ds[3]): continue
          # so the wife's birthday is ...
          d = rev(ds[3])
          # output solution
          printf("{d}", d=d.strftime("%A, %d %B %Y"))
      

      Solution: Her birthday is on: Saturday, 7th November 1964.

      There is only one set of dates that works:

      4/4 = 4th April, third son
      9/5 = 9th May, eldest son; misinterpreted as 5th September (the setter’s birthday)
      6/6 = 6th June, eldest grandson
      11/7 = 11th July; misinterpreted at 7th November (the setter’s wife’s birthday)
      8/8 = 8th August
      5/9 = 5th September, setter; misinterpreted at 9th May
      10/10 = 10th October
      7/11 = 7th November, setter’s wife; misinterpreted as 11th July
      12/12 = 12th December

      Like

  • Unknown's avatar

    Jim Randell 4:35 pm on 30 September 2022 Permalink | Reply
    Tags:   

    Teaser 3132: Bilateral symmetry 

    From The Sunday Times, 2nd October 2022 [link] [link]

    My son, at a loose end after A-levels, asked me for a mental challenge. As we’d been discussing palindromes, I suggested he try to find an arrangement of the digits 1 to 9 with the multiplication symbol “×” to give a palindrome as the answer, and he came up with:

    29678 × 1453 = 43122134.

    I said: “Now try to find the smallest such palindromic product starting in 4, where the last digit of the smallest number is still 3”. He found that smallest product, and, interestingly, if he added the two smaller numbers instead of multiplying them, then added 100, he also got a palindrome.

    What is the smallest product?

    [teaser3132]

     
    • Jim Randell's avatar

      Jim Randell 4:55 pm on 30 September 2022 Permalink | Reply

      Using the [[ SubstitutedExpression() ]] solver from the enigma.py library.

      This Python program runs in 98ms.

      from enigma import (
        Accumulator, SubstitutedExpression, irange,
        is_npalindrome as is_npal, sprintf as f, printf
      )
      
      # find the smallest product
      r = Accumulator(fn=min, collect=1)
      
      # symbols to use
      syms = "ABCDEFGHI"
      
      n = len(syms)
      for i in irange(1, n // 2):
      
        # construct the alphametic puzzle; X < Y
        (X, Y) = (syms[:i], syms[i:])
        (x, y) = (X[-1], Y[-1])
        # X * Y is palindromic and starts (and ends) in the digit 4
        exprs = [ f("is_npal({X} * {Y})"), f("({x} * {y}) % 10 = 4") ]
        if len(X) == len(Y): exprs.append(f("{X[0]} < {Y[0]}"))
        p = SubstitutedExpression(
          exprs,
          digits=irange(1, 9),  # digits are 1-9
          s2d={ x: 3 },  # final digit of X is 3
          answer=f("({X}, {Y})"),
          env=dict(is_npal=is_npal),
        )
        # solve it
        for (s, (x, y)) in p.solve(verbose=0):
          z = x * y
          printf("[{x} * {y} = {z}]")
          r.accumulate_data(z, (x, y))
      
      # output solution
      printf("min product = {r.value}")
      for (x, y) in r.data:
        v = x + y + 100
        printf("  = {x} * {y}; {x} + {y} + 100 = {v} [{s}palindrome]", s=('' if is_npal(v) else 'not '))
      

      Solution: The smallest product is 40699604.

      Ignoring the final palindromic addition check, there are 3 candidate palindromic products that meet the criteria (in decreasing order)

      424393424 = 7163 × 59248
      43122134 = 1453 × 29678
      40699604 = 23 × 1769548

      The final one gives the answer to the puzzle, and is also the only one where the sum of the multiplicands and 100 is also palindromic.

      There are also two further palindromic products where the larger of the multiplicands ends in the digit 3:

      449757944 = 56219743 × 8
      49900994 = 167453 × 298

      Like

      • Frits's avatar

        Frits 10:22 am on 1 October 2022 Permalink | Reply

        @Jim,

        I expected “for i in irange(5, n – 1):” ( where the last digit of the smallest number is still 3)

        Like

    • GeoffR's avatar

      GeoffR 10:10 am on 3 October 2022 Permalink | Reply

      The only way I could find a MiniZinc solution was to assume that one of the multipliers was two digits long, so this is not a rigorous solution. Although I coded requirements for the second palindrome, as stated in the teaser, I found it was not strictly necessary to find the smallest palindromic product.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % re-using Frits' toNum function
      function var int: toNum(array[int] of var int: a) =
           let { int: len = length(a) }in
               sum(i in 1..len) (
               ceil(pow(int2float(10), int2float(len-i))) * a[i]
                );
                
      % Digits for the two numbers being multiplied together
      % which are AB and CDEFGHI
      var 1..9:A; var 1..9:B; var 1..9:C; var 1..9:D; var 1..9:E; 
      var 1..9:F; var 1..9:G; var 1..9:H; var 1..9:I;
      
      constraint all_different ([A,B,C,D,E,F,G,H,I]);
      
      var 10..99:AB;
      var 1000000.. 9999999:CDEFGHI;
      
      % last digits of the two numbers 
      constraint B == 3 /\ I == 8;
      
      % abcdefgh - main product
      var 1..9:a; var 0..9:b; var 0..9:c; var 0..9:d;
      var 0..9:e; var 0..9:f; var 0..9:g; var 1..9:h;
      
      % mnopqrst - 2nd palindrome
      var 1..9:m; var 0..9:n; var 0..9:o; var 0..9:p;
      var 0..9:q; var 0..9:r; var 0..9:s; var 1..9:t;
      
      % Two palindromes
      var 40000000..45000000:abcdefgh;
      var 1000000..2000000:mnopqrs;
      
      % Two numbers being multiplied together
      constraint AB == toNum([A,B]);
      constraint CDEFGHI == toNum([C,D,E,F,G,H,I]);
      
      % Main and 2nd palindromes
      constraint abcdefgh == toNum([a,b,c,d,e,f,g,h]);
      constraint mnopqrs == toNum([m,n,o,p,q,r,s]); 
      
      % check main product is palindromic
      constraint (a == 4 /\ h == 4) /\ b == g /\ c == f /\ d == e;
      
      % main palindromic product
      constraint CDEFGHI * AB == abcdefgh;
      
      % form 2nd palindrome 
      constraint mnopqrs == CDEFGHI + AB + 100;
      constraint m = s /\ n == r /\ o == q;
      
      % find smallest palindromic product
      solve minimize(abcdefgh);
      
      output["Smallest palindrome = " ++ show(abcdefgh) ++ "\n" ++
      "Sum is: " ++show(CDEFGHI) ++ " * " ++ show(AB) ++ " = " ++ 
       show(abcdefgh) ++ "\n2nd palindrome = "  ++ show(mnopqrs)];
      
      

      Like

    • NigelR's avatar

      NigelR 11:28 am on 3 October 2022 Permalink | Reply

      Shorter but not quicker! The palin lambda proves I was listening last week, though!!

      from itertools import combinations as comb, permutations as perm
      #test whether number 'num' is palindromic
      palin = lambda num:sum(str(num)[n]!=str(num)[-n-1] for n in range(len(str(num))//2)) == 0
      digs=['1','2','4','5','6','7','8','9']
      res=999999999
      #work through possible values for 'left' of two smaller numbers
      for i in range(1,8):
          for x in comb(digs,i):
              for y in perm(x):
                  l = int("".join(y))
                  #work through possible values for 'right' of two smaller numbers (ending in 3)
                  for v in perm([w for w in digs if w not in y]):
                      r=int("".join(v))*10+3
                      if r>l:continue   #number ending in 3 is smallest number
                      prod=l*r
                      #test for output conditions
                      if str(prod)[0]!='4':continue
                      if not palin(prod):continue
                      if not palin(l+r+100):continue
                      if prod<res:res=prod
      print('Smallest valid product is:', res)
      

      Like

      • NigelR's avatar

        NigelR 11:36 am on 3 October 2022 Permalink | Reply

        Given that the number ending in ‘3’ is the smaller of the two numbers, I could have made line 7
        ‘for i in range(5,8)’, which shaves a bit of time off.

        Like

      • Frits's avatar

        Frits 3:19 pm on 3 October 2022 Permalink | Reply

        Easier: palin = lambda num: (s := str(num)) == s[::-1]

        The “for x .. for y ..” can be replaced by “for y in perm(digs, i):”

        Like

        • NigelR's avatar

          NigelR 12:31 pm on 4 October 2022 Permalink | Reply

          Thanks Frits . Much neater – and I now know how to assign a variable within an expression. Onwards and upwards!

          Like

      • Frits's avatar

        Frits 10:57 pm on 3 October 2022 Permalink | Reply

        I spent some time in making a generic version of GeoffR’s Minizinc program.

        As the numFirst and numAsOf functions do not work with “var int” parameters I had to call them several times.

        Using the fact that the first digit of the largest number must be a 1 (as p1 + p2 plus 100 is palindromic and has to end in 1) didn’t help to bring the run time under one second.

         
        % A Solution in MiniZinc
        include "globals.mzn";
        
        % return <n>-th digit of number <a> with length<len>
        function var int: nthDigit(var int: a, var int: len, var int: n) =
          ((a mod pows[len + 2 - n]) div pows[len + 1 - n]);                        
                       
        % return number of the first <len> digits        
        function var int: numFirst(array[int] of var int: a, var int: len) =
          sum(i in 1..len) (pows[len + 1 - i] * a[i]);            
        
        % return number as of the <b>-th digit                        
        function var int: numAsOf(array[int] of var int: a, var int: b) =
          let { int: len = length(a) }in
               sum(i in b..len) (pows[len + 1 - i] * a[i]);  
        
        % count how many digits have a correct mirror digit              
        function var int: palin(var int: a, var int: len, var int: b) =
          sum(i in 1..b) (
              nthDigit(a, len, i) == nthDigit(a, len, len + 1 - i)
          );        
        
        % digits used in the two numbers
        var 1..9: a; var 1..9: b; var 1..9: c; var 1..9: d;
        var 1..9: e; var 1..9: f; var 1..9: g; var 1..9: h; var 1..9: i;
        
        % make a tables of powers of 10
        set of int: pows = {pow(10, j) | j in 0..8};
        
        constraint i == 8;  % largest number has to end in 8
        
        constraint all_different ([a, b, c, d, e, f, g, h, i]);
        
        var 1..9999: p1;           % smallest number
        var 1..99999999: p2;       % largest number
        var 1..999999999: prod;    % product
        var 8..9: L;               % length palindrome
        var 1..4: L1;              % length smallest number
        
        % set smallest number to p1
        constraint p1 == numFirst([a, b, c, d, e, f, g, h, i], L1);
        % set largest number to p2          
        constraint p2 == numAsOf([a, b, c, d, e, f, g, h, i], L1 + 1);
        
        constraint p1 mod 10 == 3;    % last digit of smallest number is 3
        
        % first digit of product must be a 4  (needed for performance)
        constraint nthDigit(prod, L, 1) == 4;
                   
        constraint prod == p1 * p2;
        
        % set length variable L
        constraint (prod  > 99999999 /\ L == 9) \/
                   (prod <= 99999999 /\ L == 8);
        
        % product should be a palindrome
        constraint palin(prod, L, 4) == 4;
        
        % find smallest palindromic product
        solve minimize(prod);
         
        output["Smallest palindrome = " ++ show(prod)];
        

        Like

    • GeoffR's avatar

      GeoffR 9:24 am on 4 October 2022 Permalink | Reply

      @Frits:
      An excellent full programming solution in MiniZinc, with some new functions.

      Like

    • GeoffR's avatar

      GeoffR 8:50 am on 20 October 2022 Permalink | Reply

      # last digits of a = 3 and for b = 8 with a < b
      for a in range(13, 100, 10):  # UB assumed value
          # check 8-digit products up to teaser stated product
          for b in range(100008, 43122134 // a, 10):
              prod1 = a * b
              if prod1 < 40000004:continue
              
              # we need all digits in range 1..9 in prod1
              all_dig = str(a) + str(b)
              if '0' in all_dig:continue
              if len(set(all_dig)) != 9:continue
              
              # check product is a palindrome
              if str(prod1) == str(prod1)[::-1]:
                  # 2nd palindrome condition
                  pal2 = a + b + 100
                  if str(pal2) == str(pal2)[::-1]:
                      print(f"Sum is {a} * {b} = {prod1}")
      
      # Sum is 23 * 1769548 = 40699604
      
      
      
      

      Like

  • 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).

      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).

      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

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