Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:02 am on 10 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 150: Paving the way 

    From The Sunday Times, 23rd February 1964 [link]

    I have eight paving stones whose dimensions are an exact number of inches. Their areas are 504, 420, 324, 306, 270, 130, 117 and 112 square inches. Four of these are red and four green. There should be a ninth stone coloured yellow and square so that all nine stones can be fitted together to form a square in such a way that the red stones completely enclose the other five and the green stones completely enclose the yellow one.

    What are the dimensions of the red stones?

    [teaser150]

     
    • Jim Randell's avatar

      Jim Randell 8:03 am on 10 July 2022 Permalink | Reply

      The tiles that are mentioned have a total area of 2183.

      If the missing central tile has an area of x², and the entire collection has an area of y² then we have:

      y² = x² + 2183
      y² − x² = 2183
      (y − x)(y + x) = 2183

      So we can calculate possible values for x and y by examining the divisors of 2183.

      The Python program find values for x and y, and then chooses 4 green tiles, along with the x by x yellow square, which can fit together to form a square. And then checks that this square, along with the the remaining (green) tiles can fit together to form a y by y square.

      We use the rectpack.py module to fit the tiles into squares.

      The program runs in 169ms.

      Run: [ @replit ]

      from enigma import (divisors_pairs, div, subsets, is_square, cproduct, printf)
      import rectpack
      
      # areas of tiles we are given
      areas = {504, 420, 324, 306, 270, 130, 117, 112}
      
      # record possible tiles
      dps = dict((x, list(divisors_pairs(x))) for x in areas)
      
      # select tiles by area <x>, with max dimension not exceeding <m>
      tiles = lambda x, m: ((a, b) for (a, b) in dps[x] if not (b > m))
      
      # pack rectangles <rs> and an <s> by <s> square into a <t> by <t> square
      def pack(rs, s, t):
        # make the list of rectangles
        rs_ = [(s, s)]
        rs_.extend(rs)
        # pack them into a t by t square
        for ps in rectpack.pack(t, t, rs_):
          # check the square is not on the edge
          if not any(
            w == h == s and x > 0 and y > 0 and x + w < t and y + h < t
            for (x, y, w, h) in ps
          ): continue
          return ps  # we only need one packing
      
      # consider divisors of the total area
      for (a, b) in divisors_pairs(sum(areas)):
        # calculate x and y
        (x, y) = (div(b - a, 2), div(a + b, 2))
        if x is None or y is None or y < x + 4: continue
        # choose 4 green tiles to go around a central x by x square
        for green in subsets(areas, size=4):
          z = is_square(x * x + sum(green))
          if z is None: continue
          # consider dimensions of green tiles
          for gs in cproduct(tiles(x, z) for x in green):
            # pack them into a z by z square
            ps1 = pack(gs, x, z)
            if ps1 is None: continue
            # consider dimensions of red tiles
            red = areas.difference(green)
            for rs in cproduct(tiles(x, y) for x in red):
              # pack them into a y by y square
              ps2 = pack(rs, z, y)
              if ps2 is None: continue
              # output packings
              printf("red {red} around {z} x {z} green in {y} x {y}\n-> {ps2}")
              printf("green {green} around {x} x {x} yellow in {z} x {z}\n-> {ps1}")
              printf()
      

      Solution: The dimensions of the red stones (in inches) are: 3×39, 6×45, 9×36, 12×42.

      Here is a diagram of one possible layout:

      Like

      • Frits's avatar

        Frits 1:01 pm on 10 July 2022 Permalink | Reply

        @Jim, it is not stated that the yellow and green tiles form a square.

        Like

        • Jim Randell's avatar

          Jim Randell 1:11 pm on 10 July 2022 Permalink | Reply

          @Frits: Is there a way the red stones can enclose the green ones without forming a square?

          Like

        • Jim Randell's avatar

          Jim Randell 1:14 pm on 10 July 2022 Permalink | Reply

          I suppose it could be a non-square rectangle. But luckily it is possible to do it with a square.

          Like

          • Frits's avatar

            Frits 1:18 pm on 10 July 2022 Permalink | Reply

            So there might be another solution…

            Like

          • Jim Randell's avatar

            Jim Randell 1:26 pm on 10 July 2022 Permalink | Reply

            I made some tweaks to my program, and it didn’t find any solutions with a non-square rectangle. But I’ll have a closer look.

            Like

          • Jim Randell's avatar

            Jim Randell 2:53 pm on 10 July 2022 Permalink | Reply

            This is my program adapted to consider packing the green and yellow tiles into a (possibly non-square) rectangle.

            It doesn’t find any additional solutions.

            Run: [ @replit ]

            from enigma import (divisors_pairs, div, subsets, cproduct, printf)
            import rectpack
            
            # areas of tiles we are given
            areas = {504, 420, 324, 306, 270, 130, 117, 112}
            
            # record possible tiles
            dps = dict((x, list(divisors_pairs(x))) for x in areas)
            
            # select tiles by area <x>, with max dimension not exceeding <m>
            tiles = lambda x, m: ((a, b) for (a, b) in dps[x] if not(b > m))
            
            # pack rectangles <rs> and rectangle <s> into a bounding rectangle <t>
            def pack(rs, s, t):
              (a, b) = t
              # make the list of rectangles
              rs_ = [s]
              rs_.extend(rs)
              # pack them into a the rectangle
              for ps in rectpack.pack(t[0], t[1], rs_):
                # check that s is not on the edge
                if not any(
                  x > 0 and y > 0 and x + w < a and y + h < b and {w, h} == set(s)
                  for (x, y, w, h) in ps
                ): continue
                return ps  # we only need one packing
            
            # consider divisors of the total area
            for (a, b) in divisors_pairs(sum(areas)):
              # calculate x and y
              (x, y) = (div(b - a, 2), div(a + b, 2))
              if x is None or y is None or y < x + 4: continue
              # choose 4 green tiles to go around the central x by x square
              for green in subsets(areas, size=4):
                for (a, b) in divisors_pairs(x * x + sum(green)):
                  if a < x + 2 or y < b + 2: continue
                  # consider dimensions of green tiles
                  for gs in cproduct(tiles(x, b) for x in green):
                    # pack them into an a by b rectangle
                    ps1 = pack(gs, (x, x), (a, b))
                    if ps1 is None: continue
                    # consider dimensions of red tiles
                    red = areas.difference(green)
                    for rs in cproduct(tiles(x, y) for x in red):
                      # pack them into a y by y square
                      ps2 = pack(rs, (a, b), (y, y))
                      if ps2:
                        # output packings
                        printf("red {red} around {a} x {b} green in {y} x {y}\n-> {ps2}")
                        printf("green {green} around {x} x {x} yellow in {a} x {b}\n-> {ps1}")
                        printf()
            

            Like

    • Frits's avatar

      Frits 5:02 pm on 10 July 2022 Permalink | Reply

      One piece of logic is not coded, it turns out it is not needed.

         
      from enigma import divisors_pairs
      from itertools import product
       
      # find three other red pieces which can form the outer ring
      def check(a, b, d):
        # combinations of length and width of fourth red piece
        lst = [[[big_square_side - x, (big_square_side - y) * x] 
                for x in d[big_square_side - y] if (big_square_side - x) in d] 
               for y in [a, b]]
        
        # check every combination of possible length and width
        for c, d in product(lst[0], lst[1]):
          # has the fourth piece a known area
          if c[0] * d[0] not in d_red: continue
      
          # do all four pieces have a different area
          if len(set([c[0] * d[0], a * b, c[1], d[1]])) != 4: continue
          # return all four pieces
          return tuple(sorted([tuple(sorted([a, b])), 
                 tuple(sorted([big_square_side - a, big_square_side - c[0]])), 
                 tuple(sorted([big_square_side - b, big_square_side - d[0]])), 
                 tuple(sorted([c[0], d[0]]))]))
         
        return tuple()          
            
      # areas of tiles we are given
      areas = sorted([504, 420, 324, 306, 270, 130, 117, 112])
       
      # record possible tiles
      d_tiles = dict((x, list(divisors_pairs(x))) for x in areas)
      
      # possible areas for the big square
      sqs = {n * n for n in range(1, areas[-4] + 1)}
      
      area_greenred = sum(areas)
      
      # candidates for yellow area
      area_yellow_cands = [sq for sq in sqs if (sq + area_greenred) in sqs]
      
      # check all candidates for yellow areas 
      for area in area_yellow_cands:
        big_square_side = int((area + area_greenred)**.5)
        yellow = int(area**.5)
        area_yellow = area
        
        # adjust possible red tiles to drop tiles with a big length
        d_red = {k: [x for x in vs if x[0] <= big_square_side and 
                                      x[1] <= big_square_side] 
                    for k, vs in d_tiles.items()}
        
        d_side = dict()     # dictionary per side
        for k, vs in d_red.items():
          for x in vs:
            d_side[x[0]] = d_side.get(x[0], set()) | {x[1]}
            d_side[x[1]] = d_side.get(x[1], set()) | {x[0]}
      
        if big_square_side in d_side:
          # valid situation but not coded!
          continue
        
        # as each piece now is shorter than the side of the big square
        # we need to have a form so that 2 sides of different pieces are equal to
        # the side of the big square, forms should be similar to this one:
        #
        #   +----------+--+
        #   |          |  |
        #   +----+-----+  |
        #   |    |     |  |
        #   |    +-----+--+
        #   |    |        |
        #   +----+--------+
        
        # adjust dictionary to keep only sides <x> for which complementary side 
        # <big_square_side> - <k> also exists
        d_side = {k: vs for k, vs in d_side.items() 
                        if big_square_side - k in d_side}
        # also adjust possible red tiles 
        d_red = {k: [x for x in vs if big_square_side - x[0] in d_side and 
                                      big_square_side - x[1] in d_side] 
                    for k, vs in d_red.items()}
       
        sol = set()
        for k, vs in d_red.items():
          # pick a first red piece
          for (le, wi) in vs:
            # find three other red pieces which can form the outer ring
            chk = check(le, wi, d_side)
            if chk:
              if chk not in sol:
                print("answer:", *chk)
              sol.add(chk)
      

      Like

  • Unknown's avatar

    Jim Randell 4:50 pm on 8 July 2022 Permalink | Reply
    Tags:   

    Teaser 3120: Bus stop blues 

    From The Sunday Times, 10th July 2022 [link] [link]

    While waiting for buses, I often look out for interesting number plates on passing cars. From 2001 the UK registration plate format has been 2 letters + a 2-digit number + 3 more letters, the digits being last two of the year of registration with 50 added after six months (for example in 2011, the possible numbers were 11 and 61). I spotted one recently with its five letters in alphabetical order, all different and with no vowels. Looking more closely, I saw that if their numerical positions in the alphabet (A = 1, B = 2 etc.) were substituted for the 5 letters, their sum plus 1 was the 2-digit number and the sum of their reciprocals was equal to 1.

    Find the 7-character registration.

    [teaser3120]

     
    • Jim Randell's avatar

      Jim Randell 4:59 pm on 8 July 2022 Permalink | Reply

      I used the [[ reciprocals() ]] function from the enigma.py library (originally written for Enigma 348).

      This Python program runs in 63ms. (Internal run time is 174µs).

      Run: [ @replit ]

      from enigma import (reciprocals, printf)
      
      # letters (1-indexed)
      letters = "?ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      
      # vowels: A=1, E=5, I=9, O=15, U=21
      vowels = set(letters.index(x) for x in "AEIOU")
      
      # find 5 reciprocals that sum to 1
      for ns in reciprocals(5, M=26, g=1):
        # no vowels allowed
        if vowels.intersection(ns): continue
        # calculate the year
        y = sum(ns) + 1
        if not (1 < y < 23 or 50 < y < 72): continue
        # output the registration
        (A, B, X, Y, Z) = (letters[n] for n in ns)
        printf("reg = [{A}{B} {y:02d} {X}{Y}{Z}]")
      

      Solution: The registration is: BD 51 HLX.

      Note that the “vowels” restriction is not necessary if the restriction on the year number being within [02, 22] or [51, 71] is observed.

      Like

    • Frits's avatar

      Frits 6:39 pm on 8 July 2022 Permalink | Reply

         
      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # its five letters converted to numerical positions
          # AB, CD, EF, GH, IJ
          "1 < AB < 23",
          "AB < CD < 24",
          "CD < EF < 25",
          "EF < GH < 26",
          "GH < IJ < 27",
          
          # no vowels allowed, A=1, E=5, I=9, O=15, U=21
          "all(x not in {1, 5, 9, 15, 21} for x in [AB, CD, EF, GH, IJ])",
          # ranges for the sum
          "(AB + CD + EF + GH + IJ) < 22 or \
           49 < (AB + CD + EF + GH + IJ) < 72",
          # the sum of their reciprocals was equal to 1
          "1 / AB + 1 / CD + 1 / EF + 1 / GH + 1 / IJ == 1",
        ],
        answer="AB, CD, EF, GH, IJ",
        d2i="",
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        reg = chr(ord('A') + ans[0] - 1) + \
              chr(ord('A') + ans[1] - 1) + " "
        reg += str(sum(ans) + 1) + " "
        reg += chr(ord('A') + ans[2] - 1) + \
               chr(ord('A') + ans[3] - 1) + \
               chr(ord('A') + ans[4] - 1)
        print("registration =", reg)
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:38 pm on 8 July 2022 Permalink | Reply

        Or we can use base 27 to make things a bit neater:

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # allow digits 1-26
        --base=27
        # but exclude vowels (1, 5, 9, 15, 21)
        --digits="1-26,!1,!5,!9,!15,!21"
        
        # numbers are in increasing order
        "A < B" "B < X" "X < Y" "Y < Z"
        
        # reciprocals sum to 1
        "fraction(1, A,  1, B,  1, X,  1, Y,  1, Z) == (1, 1)"
        
        # check year
        --code="year = lambda n: (2 <= n <= 22) or (51 <= n <= 71)"
        "year(A + B + X + Y + Z + 1)"
        
        # output the registration
        --code="l = lambda x: int2base(x, base=27, digits='?ABCDEFGHIJKLMNOPQRSTUVWXYZ')"
        --code="n = lambda x: int2base(x, width=2)"
        --code="reg = lambda A, B, X, Y, Z: sprintf('{A}{B} {y} {X}{Y}{Z}', A=l(A), B=l(B), X=l(X), Y=l(Y), Z=l(Z), y=n(A + B + X + Y + Z + 1))"
        --answer="reg(A, B, X, Y, Z)"
        --template=""
        

        Like

    • GeoffR's avatar

      GeoffR 8:27 pm on 8 July 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Omitting vowel values for A,E,I,O,U
      % Used to translate numerical letters in output(v,w,x,y,z) to letters
      int:B == 2; int:C == 3; int:D == 4; int:F == 6; int:G == 7;
      int:H == 8; int:J == 10; int:K == 11; int:L == 12; int:M == 13;
      int:N == 14; int:P == 16; int:Q == 17; int:R == 18; int:S == 19;
      int:T == 20; int:V == 22; int:W == 23; int:X == 24; int:Y == 25;
      int:Z == 26;
      
      % number plate format is v w num x y z  (num is 2 digits)
      var 2..26:v; var 2..26:w; var 2..26:x;
      var 2..26:y; var 2..26:z;
      
      % last 2 digits in number plate - range = 2001 to 2022 + 50
      var 01..72: num;
      
      set of int: letters = {B,C,D,F,G,H,J,K,L,M,N,P,Q,R,S,T,V,W,X,Y,Z};
      
      % Five number plate letters
      constraint v in letters /\ w in letters /\ x in letters
      /\ y in letters /\ z in letters;
      
      % five letters in number plate are in alphabetical order
      constraint w > v /\ x > w /\ y > x /\ z > y;
      
      % Reciprocals of letter values sum to 1
      constraint z * 1/v + z * 1/w + z * 1/x + z * 1/y + z/z == z;
      
      % Two middle digits are the sum of letter values plus 1
      constraint v + w + x + y + z + 1 == num;
      
      solve satisfy;
      
      output [ "Number plate = " ++ show( [v, w, num, x, y, z ] ) ];
      % uses translation table to convert numbers to letters
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 3:39 pm on 10 July 2022 Permalink | Reply

      
      from itertools import combinations
      import string
      
      # Dictionary mapping numbers to upper case letters
      DN = dict(zip(range(1, 27), string.ascii_uppercase))
      
      # omit values for vowels A, E, I, O, U
      vowels = {1, 5, 9, 15, 21}
      
      for C1 in combinations(set(range(1, 27)).difference(vowels), 5):
        a, b, c, d, e = C1
        #  five letters are in alphabetical order
        if a < b < c < d < e:
          # the sum of their reciprocals was equal to 1
          # i.e. 1/a + 1/b + 1/c + 1/d + 1/e == 1:
          if b*c*d*e + a*c*d*e + a*b*d*e + a*b*c*e + a*b*c*d == a*b*c*d*e:
            
            # last two digits of the year
            year2d = a + b + c + d + e + 1
            if year2d <= 22 or (51 <= year2d <= 72):
              print('Reg. No. = ', DN[a], DN[b], year2d, DN[c], DN[d], DN[e])
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:53 am on 7 July 2022 Permalink | Reply
    Tags:   

    Teaser 2723: St Andrew’s day 

    From The Sunday Times, 30th November 2014 [link] [link]

    Bearing in mind today’s date, I have written down two numbers in code, with different letters being used consistently to replace different digits. The addition of the two numbers is shown below, appropriately leading to today’s date as a six- figure number.

    What is the value of SEND?

    [teaser2723]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 7 July 2022 Permalink | Reply

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

      The following run file is straightforward, but not especially quick. It runs in 710ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "{SAINT} + {ANDREW} = 301114"
      
      --answer="{SEND}"
      

      One way we can improve the execution time is to split the sum into multiple partial sums consisting of columns from the original sum.

      This process is automated by the [[ SubstitutedExpression.split_sum ]] helper method, which can now be called from a run file.

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

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "{SAINT} + {ANDREW} = {301114}"
      
      --literal="0134"
      --distinct="ADEINRSTW"
      --answer="{SEND}"
      

      Solution: SEND = 3568.

      The solver finds 4 solutions to the sum as (I, R) = (1, 9) and (T, W) = (0, 4), in some order.

      Like

    • GeoffR's avatar

      GeoffR 11:18 am on 7 July 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:S; var 1..9:A; var 0..9:I;
      var 0..9:N; var 0..9:T; var 0..9:D;
      var 0..9:R; var 0..9:E; var 0..9:W;
      
      constraint all_different([S,A,I,N,T,D,R,E,W]);
      
      var 10000..99999: SAINT = 10000*S + 1000*A + 100*I + 10*N + T;
      var 100000..999999: ANDREW = 100000*A + 10000*N + 1000*D+ 100*R + 10*E + W;
      
      var 1000..9999:SEND = 1000*S + 100*E + 10*N + D;
      
      constraint SAINT + ANDREW == 301114;
      
      solve satisfy;
      output["SEND = " ++ show(SEND) ];
      
      % SEND = 3568
      % ----------
      % ==========
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 2:37 pm on 7 July 2022 Permalink | Reply

      A solution simulating the normal column addition process, with carry digits.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      %    S A I N T
      %  A N D R E W
      %-------------
      %  3 0 1 1 1 4
      %-------------
      
      var 1..9:S; var 1..9:A; var 0..9:I;
      var 0..9:N; var 0..9:T; var 0..9:D;
      var 0..9:R; var 0..9:E; var 0..9:W;
      
      constraint all_different([S,A,I,N,T,D,R,E,W]);
      
      % column carry digits for adding columns
      var 0..1: c1; var 0..1: c2; var 0..1: c3; var 0..1: c4; var 0..1: c5; 
      
      % carry digits start from RH end of sum
      constraint (T + W) mod 10 == 4 /\ (T + W) div 10 == c1;
      
      constraint (N + E + c1) mod 10 == 1 /\ (N + E + c1) div 10 == c2;
      
      constraint (I + R + c2) mod 10 == 1 /\ (I + R + c2) div 10 == c3;
      
      constraint (A + D + c3) mod 10 == 1 /\ (A + D + c3) div 10 == c4;
      
      constraint (S + N + c4) mod 10 == 0 /\ (S + N + c4) div 10 == c5;
      
       constraint A + c5 == 3;
       
      solve satisfy;
      
      output ["SEND = \(S)\(E)\(N)\(D)" 
      ++  ", SAINT = \(S)\(A)\(I)\(N)\(T)"
      ++  ", ANDREW = \(A)\(N)\(D)\(R)\(E)\(W)" ];
      
      % SEND = 3568, SAINT = 32964, ANDREW = 268150
      % ----------
      % SEND = 3568, SAINT = 32960, ANDREW = 268154
      % ----------
      % SEND = 3568, SAINT = 32164, ANDREW = 268950
      % ----------
      % SEND = 3568, SAINT = 32160, ANDREW = 268954
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:32 am on 5 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 731: A little list 

    From The Sunday Times, 20th July 1975 [link]

    (1) In this list there is a true statement and a false statement whose numbers add up to give the number of a false statement.

    (2) Either statement 4 is false or there are three  consecutive true statements.

    (3) The number of the last false statement subtracted from the product of the numbers of the first and last true statements gives the number of a statement which is false.

    (4) The number of even-numbered true statements equals the number of false statements.

    (5) One of the first and last statements is true and the other is false.

    (6) When I first sent this problem to the editor, thanks to a typing error no sixth statement was included. However the answer to the following question was the same then as it is now:

    Which statements are false?

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

    [teaser724]

     
    • Jim Randell's avatar

      Jim Randell 10:33 am on 5 July 2022 Permalink | Reply

      This Python program runs in 62ms. (Internal runtime is 680µs).

      Run: [ @replit ]

      from enigma import (subsets, filter2, irange, first, product, tuples, icount, printf)
      
      # solve the puzzle with k (= 5 or 6) statements, considering only the first 5 statements
      def solve(k):
      
        # consider the puzzle with k statements
        for ss in subsets((True, False), size=k, select="M"):
          # check the first 5 statements
          (s1, s2, s3, s4, s5) = first(ss, 5)
          (ts, fs) = filter2((lambda n: ss[n - 1]), irange(1, k))
      
          # 1: "in this list there is a true statement and a false statement
          # whose numbers add up to give the number of a false statement"
          if not (s1 == any(x + y in fs for (x, y) in product(ts, fs))): continue
      
          # 2: "either statement 4 is false, or there are three consecutive
          # true statements"
          if not (s2 == ((s4 is False) != (any(all(xs) for xs in tuples(ss, 3))))): continue
      
          # 3: "the number of the last false statement subtracted from the
          # product of the numbers of the first and last true statements gives
          # the number of a statement which is false"
          if not (s3 == (ts and fs and ts[0] * ts[-1] - fs[-1] in fs)): continue
      
          # 4: "the number of even numbered true statements equals the number
          # of false statements"
          if not (s4 == (icount(n for n in ts if n % 2 == 0) == len(fs))): continue
      
          # 5: "one of the first and last statements is true and the other is false"
          if not (s5 == (s1 != ss[-1])): continue
      
          # return the numbers of the false statements
          yield fs
      
      # look for solutions for the first 5 statements of the 6 statement
      # version of the puzzle, grouped by the value of statement 6
      (fs6t, fs6f) = filter2((lambda fs: 6 not in fs), solve(6))
      
      # if statement 6 is false, we don't have to worry about the 5 statement version
      printf("false statements = {fs6f}")
      
      # if statement 6 is true, the solution(s) must be the same as the 5 statement version
      fs5 = list(solve(5))
      if fs5 == fs6t:
        printf("false statements = {fs6t}")
      

      Solution: Statements 3, 4, 6 are false.

      So statements 1, 2, 5 are true.

      1. (true) “in this list there is a true statement and a false statement whose numbers add up to give the number of a false statement”.
      There are two: 1 + 3 = 4, 2 + 4 = 6.

      2. (true) “either statement 4 is false, or there are three consecutive true statements”.
      4 is false; there are not three consecutive true statements.

      3. (false) “the number of the last false statement subtracted from the product of the numbers of the first and last true statements gives the number of a statement which is false”.
      1×5 − 6 = −1, is not the number of a statement.

      4. (false) “the number of even numbered true statements equals the number of false statements”.
      There is 1 even numbered true statement, and there are 3 false statements.

      5. (true) “one of the first and last statements is true and the other is false”.
      1 is true; 6 is false.

      6. (false) “When I first sent this problem to the editor, thanks to a typing error no sixth statement was included. However the answer to the following question was the same then as it is now: Which statements are false”.
      The first part could be false (there was no error). But if it was true there are no solutions given only the first 5 statements, so the answer to the puzzle could not be the same if the 6th statement was included.

      Like

  • Unknown's avatar

    Jim Randell 12:49 pm on 3 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 120: Rugby results 

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

    At a certain stage in our all-against-all Rugby football competition the table of results read as follows. There had been no matches in which neither side had scored at all:

    What was the result of the match between A and B?

    Note: This problem was set when a try was worth 3 points, a penalty goal 3 points and a converted try 5 points. Match points are on the usual basis of 2 for a win and 1 for a draw.

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

    [teaser120]

     
    • Jim Randell's avatar

      Jim Randell 12:50 pm on 3 July 2022 Permalink | Reply

      There are certain scores which are not possible to make from combinations of 3 and 5 points.

      This Python program uses the [[ express() ]] function from the enigma.py library to determine what they are. We then use the [[ Football() ]] helper class to determine possible match outcomes and scorelines.

      It runs in 89ms.

      Run: [ @replit ]

      from enigma import (express, irange, peek, Football, digit_map, cache)
      
      # find scores that cannot be made from multiples of 3 and 5
      invalid = set(n for n in irange(0, 18) if not peek(express(n, [3, 5]), default=None))
      
      # scoring system (2 points for a win, 1 for a draw)
      football = Football(points=dict(w=2, d=1))
      
      # digits used in the table (10=A, 13=D, 15=F, 16=G, 18=I)
      d = digit_map(0, 18)
      
      # the table, goals for/against
      (table, gf, ga) = (dict(played="34432", points="55420"), "DGF53", "8AI88")
      
      # check for a valid score (and cache as we go along)
      @cache
      def valid(s):
        # unplayed?
        if s is None: return True
        # otherwise:
        # (0, 0) is not allowed
        if s == (0, 0): return False
        # reject invalid scores
        if invalid.intersection(s): return False
        # looks OK
        return True
      
      # find possible match outcomes
      for (ms, _) in football.substituted_table(table, d=d):
        # find possible scorelines
        for ss in football.substituted_table_goals(gf, ga, ms, d=d, valid=valid):
          # output solution
          football.output_matches(ms, ss, teams="ABCDE")
      

      Solution: The result of the A vs B match was a 5-3 win for A.

      The full results are:

      A vs B = 5-3
      A vs C = 5-5
      A vs D = 3-0
      A vs E = (unplayed)
      B vs C = 5-5
      B vs D = 5-0
      B vs E = 3-0
      C vs D = 0-5
      C vs E = 5-3
      D vs E = (unplayed)

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 1 July 2022 Permalink | Reply
    Tags:   

    Teaser 3119: Hidden powers 

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

    My grandson is studying “History since the Battle of Hastings”. I made him a game, which consisted of a row of nine cards, each with a different non-zero digit on it. Throw a standard die, note the number of spots displayed, count that number of places along the row and pause there. Throw the die again, move the corresponding number of places further along and pause again. Repeat this until you come off the end of the row, noting the digit or digits you paused on and put these together in the same order, to produce a number.

    Keeping the cards in the same order I asked my grandson to try to produce a square or cube or higher power. He eventually discovered that the lowest possible such number was equal to the number of one of the years that he had been studying.

    What is the order of the nine digits along the row?

    [teaser3119]

     
    • Jim Randell's avatar

      Jim Randell 5:54 pm on 1 July 2022 Permalink | Reply

      It took me a little while to understand how this puzzle is supposed to work.

      And despite my initial misgivings there is only one possible arrangement of digits that can lead to the situation described.

      The following Python program isn’t very quick (it tries all possible arrangements of digits), but it does find the required arrangement.

      The code to generate powers is adapted from Teaser 683 and Enigma 1777.

      It runs in 669ms.

      Run: [ @replit ]

      from enigma import (Primes, nsplit, subsets, irange, tuples, printf)
      
      # generate powers (x^y) in numerical order, x >= 0, y >= 2,
      def powers():
        # powers less than 2^2
        yield 0
        yield 1
        # powers from 2^2 upwards
        base = { 2: 2 }
        power = { 2: 4 }
        maxp = 2
        primes = Primes(35, expandable=1).generate(maxp + 1)
        while True:
          # find the next power
          n = min(power.values())
          yield n
          # what powers are involved
          ps = list(p for (p, v) in power.items() if v == n)
          # increase the powers
          for p in ps:
            base[p] += 1
            power[p] = pow(base[p], p)
          # do we need to add in a new power?
          if maxp in ps:
            maxp = next(primes)
            base[maxp] = 2
            power[maxp] = pow(2, maxp)
      
      # collect candidate powers
      pows = list()
      for n in powers():
        if n > 2022: break
        # split the power into digits
        ds = nsplit(n)
        # no 0 digits, or repeated digits
        if 0 in ds or len(set(ds)) != len(ds): continue
        pows.append((n, ds))
      
      # check powers that can be made with an arrangement of digits
      def play(ss):
        # find powers that can be made
        ns = list()
        for (n, ds) in pows:
          # find indices of the digits
          js = list(ss.index(d) for d in ds)
          # check entry and exit
          if js[0] > 5 or js[-1] < 3: continue
          js.insert(0, -1)
          # and corresponding dice throws
          ts = list(y - x for (x, y) in tuples(js, 2))
          if min(ts) < 1 or max(ts) > 6: continue
          # check the power is permissible
          if n < 1066: return
          ns.append(n)
        # return the list of powers
        return ns
      
      # consider possible arrangements of digits
      for ss in subsets(irange(1, 9), size=len, select="P"):
        ns = play(ss)
        if ns:
          printf("{ss} -> {ns}")
      

      Solution: The order of the digits is: 1, 9, 8, 7, 5, 2, 3, 4, 6.

      And the smallest (and onliest) power which can be made by rolling the die is 1936, with rolls of (1, 1, 5, 2, *).


      Although the puzzle talks about the lowest power that can be generated, it turns out it is the only (non-trivial) power that can be generated with the required arrangement of cards.

      The puzzle will continue to work in the future, until the year 4913 (= 17³) is incorporated into the history book.

      Like

      • Jim Randell's avatar

        Jim Randell 10:13 am on 2 July 2022 Permalink | Reply

        Here’s a faster (but slightly longer) program. Instead of just trying all possible arrangements of digits, it recursively constructs arrangements that cannot give powers less than 1066, and then finds which greater powers can be made.

        It runs in 174ms.

        Run: [ @replit ]

        from enigma import (Primes, nsplit, subsets, tuples, diff, update, printf)
        
        # generate powers (x^y) in numerical order, x >= 0, y >= 2,
        def powers():
          # powers less than 2^2
          yield 0
          yield 1
          # powers from 2^2 upwards
          base = { 2: 2 }
          power = { 2: 4 }
          maxp = 2
          primes = Primes(35, expandable=1).generate(maxp + 1)
          while True:
            # find the next power
            n = min(power.values())
            yield n
            # what powers are involved
            ps = list(p for (p, v) in power.items() if v == n)
            # increase the powers
            for p in ps:
              base[p] += 1
              power[p] = pow(base[p], p)
            # do we need to add in a new power?
            if maxp in ps:
              maxp = next(primes)
              base[maxp] = 2
              power[maxp] = pow(2, maxp)
        
        # collect candidate powers (disallowed and allowed)
        (xpows, pows) = (list(), list())
        for n in powers():
          if n > 2022: break
          # split the power into digits
          ds = nsplit(n)
          # no 0 digits, or repeated digits
          if 0 in ds or len(set(ds)) != len(ds): continue
          (xpows if n < 1066 else pows).append((n, ds))
        
        # check if digit sequence <ds> can be made from arrangement <ns>
        def play(ns, ds):
          # find indices of the digits
          js = list(ns.index(d) for d in ds)
          # check entry and exit
          if js[0] > 5 or js[-1] < 3: return False
          # check corresponding dice throws (1-6)
          js.insert(0, -1)
          return all(0 < y - x < 7 for (x, y) in tuples(js, 2))
        
        # generate arrangements <ns> that don't allow sequences <xs> to be made
        # ns = digit arrangement
        # xs = disallowed sequences
        # ss = allocated digits
        # js = indices of empty slots
        def solve(ns, xs, ss=set(), js=None):
          if not xs:
            yield ns
          else:
            # find the shortest sequences with the fewest missing digits
            fn = lambda ds: (len(set(ds).difference(ss)), len(ds))
            ds = min(xs, key=fn)
            # choose placements for the missing digits
            ms = diff(ds, ss)
            if not js: js = set(j for (j, n) in enumerate(ns) if n is None)
            for ks in subsets(js, size=len(ms), select="P"):
              ns_ = update(ns, ks, ms)
              if not play(ns_, ds):
                # solve the rest
                yield from solve(ns_, xs.difference([ds]), ss.union(ms), js.difference(ks))
        
        # find layouts that do not permit sequences from xpows
        for ns in solve([None] * 9, set(ds for (_, ds) in xpows)):
          # look for permissible powers
          ps = list(n for (n, ds) in pows if play(ns, ds))
          if ps:
            printf("{ns} -> {ps}")
        

        Liked by 1 person

    • Frits's avatar

      Frits 11:28 pm on 1 July 2022 Permalink | Reply

      Reusing my code of Teaser 683 and using Jim’s early performance enhancement (also considering number 1 as a power).

         
      from itertools import permutations
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if mult_gcd(list(oc)) == 1:
          return False
        return True
       
      # calculate greatest common divisor of multiple numbers  
      def mult_gcd(li):
        if len(li) == 1:
          return li[0]
       
        for i in range(len(li) - 1):
          a = li[i]
          b = li[i+1]
          while b:
            (a, b) = (b, a % b)
           
          if a == 1: return a
           
          li[i+1] = a
        return a  
       
      # generate powers, stop if you encounter one less than 1067    
      def solve(k, s, pw=0, ns=[], rs=set()):
        if k > 0:
          # year must be before 2023
          if k == 1 and pw > 202:
            return []
          ls = len(s)
          for i in range(6):
            # check if you come off the end of the row
            if i > ls - 1: break
            p = 10 * pw + s[i]
            if p in pows and ls - i < 7:
              return [p]
            
            # recurse if worthwhile
            if p in powstart:  
              res = solve(k - 1, s[i + 1:], p, ns + [i + 1], rs)  
              if len(res) == 1:
                rs.add(res[0])
                if res[0] < 1067:
                  return []  # no solution
          
          # if we didn't encounter a low power return list of powers
          return list(rs)    
          
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      powstart = {int(str(p)[:i]) for p in pows for i in range(1, 4)}
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
      
        r = sorted(solve(4, perm, rs=set()))
        if r and r[0] > 1066:
          print(perm, "number", r[0])
      

      Like

      • Frits's avatar

        Frits 9:29 am on 2 July 2022 Permalink | Reply

        Python 3.9 introduced multiple arguments version of math.gcd so math.gcd() can be used instead of mult_gcd().

        Like

    • Frits's avatar

      Frits 2:50 pm on 2 July 2022 Permalink | Reply

      Just as with Jim ‘s third program only calling solve() for 49 permutations.

      Doing the checks for 2-digit and 3-digit powers in a general way is a bit slower.
      Also storing digit positions in a dictionary to minimize index() calls is a bit slower.

         
      from itertools import permutations
      from math import gcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if gcd(*oc) == 1:
          return False
        return True
       
      # generate powers, stop if you encounter one less than 1067    
      def solve(k, s, pw=0, ns=[], rs=set()):
        if k > 0:
          # year must be before 2023
          if k == 1 and pw > 202:
            return []
          ls = len(s)
          for i in range(6):
            # check if you come off the end of the row
            if i > ls - 1: break
            p = 10 * pw + s[i]
            if p in pows and ls - i < 7:
              return [p]
            
            # recurse if worthwhile
            if p in powstart:  
              res = solve(k - 1, s[i + 1:], p, ns + [i + 1], rs)  
              if len(res) == 1:
                rs.add(res[0])
                if res[0] < 1067:
                  return []  # no solution
          
          # if we didn't encounter a low power return list of powers
          return list(rs)    
          
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      powstart = {int(str(p)[:i]) for p in pows for i in range(1, 4)}
      
      # 2-digit powers
      pows2 = [(x // 10, x % 10) for x in pows if 9 < x < 100]
      
      # 3-digit powers
      pows3 = [(x // 100, (x // 10) % 10, x % 10) for x in pows if 99 < x < 1000]
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
        
        # check 2-digit powers
        for a, b in pows2:
          posa = perm.index(a)
          posb = perm.index(b)
          # can we make the power in 2 jumps and then come off the end of the row?
          if 0 < (posb - posa) < 7 and posa < 6 and posb > 2:
            break
        else:  # no break   
          # check 3-digit powers
          for a, b, c in pows3:
            posa = perm.index(a)
            posb = perm.index(b)
            posc = perm.index(c)
            # can we make the power in 3 jumps and 
            # then come off the end of the row?
            if not (0 < (posb - posa) < 7 and 0 < (posc - posb) < 7): continue
            if posa < 6 or posc > 2:
              break    
          else:  # no break   
            # no 2- or 3-digit power can be formed
            r = sorted(solve(4, perm, rs=set()))
            if r and r[0] > 1066:
              print(perm, "number", r[0])
      

      Like

    • Frits's avatar

      Frits 4:04 pm on 2 July 2022 Permalink | Reply

      Without recursion.

         
      from itertools import permutations
      from enigma import mgcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if mgcd(*oc) == 1:
          return False
        return True
       
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      # powers grouped by number of digits
      pws = [[[int(x) for x in str(p)] for p in pows 
               if 10**i <= p < 10**(i + 1)] for i in range(1, 4)]
      
      # do powers exists >= 2000 ?
      exists2xxx = any(x for x in pws[2] if x[0] == 2)
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
        
        for ps in pws[:-1]:
          for p in ps:
            pos = [perm.index(i) for i in p]
            # first digit reachable and then come off the end of the row?
            if not(pos[0] < 6 and pos[-1] > 2): continue
            # can we make the power in <len(pos)> jumps ...
            if any(y - x not in {1, 2, 3, 4, 5, 6} for x, y in zip(pos, pos[1:])):
              continue
              
            # we have found a valid 2- or 3-digit power
            break
          else: # no break  
            continue
            
          break   # break if break occurred    
        else:  # no break
          if not exists2xxx:
            # check position of 1
            if perm.index(1) > 5: continue
          
          # check 4-digit powers
          for p in sorted(pws[2]):
            pos = [perm.index(i) for i in p]
            # it is enough to only check for increasing numbers
            if pos != sorted(pos): continue
            
            print(perm, "number", "".join(str(x) for x in p))
            break
      

      Like

    • Frits's avatar

      Frits 5:48 pm on 8 July 2022 Permalink | Reply

         
      from itertools import permutations
      from functools import reduce
      from math import gcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        if n == 1: return [1]
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if gcd(*oc) == 1:
          return False
        return True
      
      # group type
      gtype = lambda i: "first" if i == 0 else "middle" if i == 1 else "last"
              
      # collect all candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and 
              len(s:= str(i)) == len(set(s)) and "0" not in s} 
      
      # powers grouped by number of digits
      pws = [set(str(p) for p in pows 
               if 10**i <= p < 10**(i + 1)) for i in range(4)]
               
      # return possible arrangements of digits in group <grp> for 2-digit powers
      def arrangements(grp):
        return [p for p in permutations(grp) if all(fdgt + sdgt not in pws[1] 
                for fdgt, sdgt in [(p[0], p[1]), (p[0], p[2]), (p[1], p[2])])
               ]
      
      # return all numbers per group that don't occur in other groups
      def calc_known(grp):
        return [[x for x in grp[i] 
                if all(x not in t for t in 
                      [y for j, y in enumerate(grp) if j != i])] 
                for i in range(3)]
       
      # for all combinations in <lst> check if a 3-digit power can be made
      # with a digit in the last group
      def check_last_group(grp, lst):
        forbidden = [set() for _ in range(len(lst))]
        for i, n in enumerate(lst):
          # for all combinations in <lst> 
          for x in [(n[0] + n[1]), (n[0] + n[2]), (n[1] + n[2])]:
            for p in pws[2]:
              if x in {p[:-1]}:
                # x + p[-1] is 3-digit power so p[-1] is forbidden in last group
                forbidden[i].add(p[-1])
              
        # return numbers in the last group which are forbidden for all <lst> entries
        return reduce((lambda x, y: x & y), map(set, forbidden))
      
      # maintain groups if all numbers in a group are known
      def maintain_groups(grp, fixed):
        for i, fix in enumerate(fixed):
          # all numbers in a group are known?
          if len(fix) == 3:
            # remove other numbers from this group
            for n in [x for x in grp[i] if x not in fix]:
              print(f"{gtype(i)} group must be {','.join(fix)}:"
                    f" remove {n} from {gtype(i)} group")
            grp[i] = set(fix)
                  
      def print_groups(g):
        print("----- groups:", ",".join(sorted(g[0])), "--", 
                               ",".join(sorted(g[1])), "--", 
                               ",".join(sorted(g[2])), "-----")
      
      # maintain and print groups  
      def update(g):
        known = calc_known(g)
        maintain_groups(g, known)
        print_groups(g)
        return known
      
      
      print("-------- START -----------")
      
      print("split the nine digits in three groups of three digits")
      # list <g> contains candidate digits for each group
      g = [set("123456789"), set("123456789"), set("123456789")]
      print_groups(g)
      
      print(f"removing 1 from last group"
            f" as we need a 4-digit power 1xxx as an answer")
      g[2] -= {'1'}             # we need a 4-digit power
      
      print("removing", ", ".join(sorted(pws[0])), 
            "from middle group to prevent single digit powers")
      g[1] -= set(pws[0])       
      
      for x in calc_known(g)[0]:
        # can we can make a 2-digit power with <x>?
        for p in [p2 for p2 in pws[1] if p2[0] == x]:
          print("removing", p[1], "from middle group to prevent power", p)
          g[1].remove(p[1])         
      
      print_groups(g)
      
      # check middle group
      if len(g[1]) == 4:
        # see what happens if we remove <m> from middle group 
        for m in g[1]:
          middle = [x for x in g[1] if x != m]
          for m2 in middle:
            # if combination is a 2-digit power <m> must occur in middle group
            if m2 + m in pws[1]: 
              g[2] -= {m}
            if m + m2 in pws[1]: 
              g[0] -= {m}
         
          # get arrangements from <middle> without a 2-digit power
          lst = arrangements(middle)
          
          if len(lst) == 1:
            # we have only one arrangement for slots 3, 4 and 5 
            # can we can make a 3-digit power with this arrangement?
            arr = lst[0]
            for a in [(arr[0] + arr[1]), (arr[0] + arr[2]), (arr[1] + arr[2])]:
              # 3-digit powers that can be made from <a>
              tdp = [p3 for p3 in pws[2] if a in p3[:-1]]
              for p in tdp:
                # last digit of 3-digit power may not be used yet
                if p[-1] in (arr + ('1', )): continue
                # removing m has lead to a valid 3-digit power 
                print(f"removing {m} from the middle group can lead to"
                      f" power {p}, remove {m} from first and last group")
                g[0] -= {m}
                g[2] -= {m}
               
        # maintain and print groups
        known = update(g)
        
        # for every 3-digit power check if two of it's digits have to occur in two 
        # groups, if so we can remove the other digit from the other group
        first = 1
        for p in pws[2]:
          unknown = [i for i in range(3) if p[i] not in known[i]]  
          if len(unknown) != 1: continue
          
          i = unknown[0]
          if first: 
            print("known digits per group:", known)
            first = 0
          
          print(f"digits {' and '.join(p[j] for j in range(3) if j != i)}"
                f" of power {p} each have to occur in a different specific group:"
                f" remove {p[i]} from {gtype(i)} group")
          g[i] -= {p[i]}
        
        update(g)
        
        # get numbers in the last group which make a 3-digit power for all
        # valid combinations in the middle group
        for s in check_last_group(g, arrangements(g[1])):
          print(f"all valid combinations of middle group combined with the number"
                f" {s} lead to 3-digit power: remove {s} from last group")
          g[2] -= {s}
               
        update(g)
        print()
        
        # consider possible arrangements of digits
        for p0 in permutations(g[0]):
          for p1 in arrangements(g[1]):
            for p2 in permutations(g[2]):
              perm = p0 + p1 + p2
              # check 2-digit and 3-digit powers
              for ps in pws[1:-1]:
                for p in ps:
                  # the index positions of its digits in the row of cards
                  pos = [perm.index(i) for i in p]
                  # ensure that its first digit is reachable and that its
                  # last digit can be the final digit (with a throw of six)
                  if not(pos[0] < 6 and pos[-1] > 2): 
                    continue
                  # can we make the power in intervals of  1 - 6 jumps?
                  if any(y - x not in set(range(1, 7))
                        for x, y in zip(pos, pos[1:])):
                    continue
      
                  # we have found a valid 2-digit or 3-digit power
                  break
                else: 
                  continue
      
                break   # break again if break occurred    
              else:  
                # check 4-digit powers
                for p in sorted(pws[3]):
                  pos = [perm.index(i) for i in p]
                  
                  # checking for the correct digit order is sufficient because
                  # the highest gap between digits is five empty slots
                  if pos != sorted(pos): continue
                  
                  print(perm, "number", p)
                  break  # only print the lowest 4-digit power
      

      Like

  • Unknown's avatar

    Jim Randell 9:59 am on 30 June 2022 Permalink | Reply
    Tags:   

    Teaser 2720: Better tetra 

    From The Sunday Times, 9th November 2014 [link] [link]

    I have four tetrahedral dice. Each has four identical triangular faces and on each face of each die is one of the numbers 1, 2, 3 or 4 (repeats being allowed on a die). No die uses the same four numbers and, for each die, the sum of its four numbers is ten.

    I play a game with my friend. My friend chooses a die first and then I choose one of the remaining three dice. We each throw our chosen die and score the face-down number: sometimes it’s a draw, otherwise the higher number wins. I can choose my die so that I am always more likely to win.

    So my friend changes the rules. We now throw our chosen die twice, add the two numbers, and the higher total wins.

    Now he knows that there is one die he can choose that makes him more likely to win the game.

    What are the four numbers on the winning dice?

    The wording for this puzzle has been modified from the originally published version.

    [teaser2720]

     
    • Jim Randell's avatar

      Jim Randell 10:00 am on 30 June 2022 Permalink | Reply

      (See: [@youtube]).

      We assume that the dice are chosen in such a way that each player is aware of which die has been chosen. (So, for example, they may be different colours and arranged on the table, but not be hidden a bag and chosen by putting your hand in and selecting one at random).

      When we compare 2 dice, we say that one is “better” than the other, if it is more like to win a game against the other die.

      There are 5 possible dice (i.e. 5 possible decompositions of 10 into the numbers 1, 2, 3, 4), but the set only contains 4 of these.

      It turns out there is only one possible set of 4, where whatever die A chooses B can always choose a better die.

      And from this set we can look to see if there is die which is better when played against each of the other dice.

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

      Run: [ @replit ]

      from enigma import (decompose, subsets, multiset, product, printf)
      
      # return wins for X and Y in a game with k throws?
      def play(X, Y, k):
        # generate scores for X and Y
        (sX, sY) = (multiset.from_seq(sum(ss) for ss in subsets(ns, size=k, select="M")) for ns in (X, Y))
        # count wins for X, wins for Y
        wX = wY = 0
        for ((x, nx), (y, ny)) in product(sX.items(), sY.items()):
          if x > y:
            wX += nx * ny
          elif y > x:
            wY += nx * ny
        return (wX, wY)
      
      # is X better than Y in a game with k throws?
      def better(X, Y, k):
        (wX, wY) = play(X, Y, k)
        return wX > wY
      
      # generate possible dice
      ns = list(decompose(10, 4, increasing=1, sep=0, min_v=1, max_v=4))
      
      # choose a set of 4 dice, and solve the puzzle
      for ds in subsets(ns, size=4):
      
        # with 1 throw, whatever die A chooses, B can always choose a better die
        if not all(any(better(B, A, 1) for B in ds if B != A) for A in ds): continue
        printf("dice = {ds}")
      
        # with 2 throws, A can choose a die that is better than the others
        for A in ds:
          if not all(better(A, B, 2) for B in ds if B != A): continue
          printf("-> A = {A}")
      

      Solution: The best die in the second game is: (1, 3, 3, 3).


      The four dice are:

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

      And we have:

      (1, 1, 4, 4) is beaten by (2, 2, 2, 4)
      (2, 2, 2, 4) is beaten by (2, 2, 3, 3) and (1, 3, 3, 3)
      (2, 2, 3, 3) is beaten by (1, 3, 3, 3)
      (1, 3, 3, 3) is beaten by (1, 4, 4, 4)

      So whichever die is chosen first, there is always a choice from the other three that is better.

      Like

    • Frits's avatar

      Frits 5:54 pm on 30 June 2022 Permalink | Reply

      Apparently we can also compare lists with the less than operator in Python.

         
      from itertools import product
      from enigma import SubstitutedExpression
      
      # number of times x wins from y if die is thrown once
      play1 = lambda x, y: sum([p > q for p, q in product(x, y)])
      
      # number of times x wins from y if die is thrown twice
      def play2(x, y):
        # add the score of two throws
        x2 = [p + q for p, q in product(x, x)]
        y2 = [p + q for p, q in product(y, y)]
        
        return sum([p > q for p, q in product(x2, y2)])
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
         "A <= B <= C",
         "10 - A - B - C = D",
         "C <= D",
         
         "E <= F <= G",
         "10 - E - F - G = H",
         "G <= H",
         "(A, B, C, D) < (E, F, G, H)",
         
         "I <= J <= K",
         "10 - I - J - K = L",
         "K <= L",
         "(E, F, G, H) < (I, J, K, L)",
         
         "M <= N <= O",
         "10 - M - N - O = P",
         "O <= P",
         "(I, J, K, L) < (M, N, O, P)",
         
         # with 1 throw, whatever die my friend chooses (x), 
         # I can always choose a better die (y), resulting in 4 wins
         "sum([any(play1(y, x) > play1(x, y) \
               for y in [(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)] if y != x) \
               for x in [(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)]]) == 4",
        ],
        answer="(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)",
        env=dict(play1=play1),
        distinct="",
        digits=range(1, 5),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # check if my friend can win with the die thrown twice
      for _, s in p.solve():
        for s1 in s:
          if all(play2(s1, s2) > play2(s2, s1) for s2 in s if s1 != s2):
            print("answer:", s1)
      

      Like

  • Unknown's avatar

    Jim Randell 9:54 am on 28 June 2022 Permalink | Reply
    Tags: by: M C Breach   

    Brain-Teaser 724: Counting sheep … 

    From The Sunday Times, 1st June 1975 [link]

    In a primitive eastern country a shepherd was counting his
    sheep into four separate pens. In the first were 75 sheep, so he wrote in his records:

    In the second were 255 and he wrote:

    In the third were 183 and he wrote:

    After counting the sheep in the fourth pen he wrote:

    How many sheep were in the fourth pen?

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

    [teaser724]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 28 June 2022 Permalink | Reply

      I think this puzzle is a bit open ended.

      I called the symbols A (for the triangle), D (for the box), and X (for the loop).

      For all we know DXA, ADX, XAD could be words that happen to mean 75, 255, 183.

      Or it could be some system with strange rules (like roman numerals, for instance).

      So, I made some assumptions:

      I assumed each symbol stands for a different non-negative integer value (but the same value whenever it appears). And as the same symbols appear in a different order for each of the given values I assumed position is important (as it is in our decimal system).

      My first thought was maybe it was just a substituted base system where each digit is represented by one of the symbols.

      For 255 to be represented by 3-digits the base must be 7 – 15.

      For 75 to be represented by 3-digits the base must be: 5 – 8.

      So we can look at values 75, 255, 183 in base 7 and 8:

      base 7: 75 → “135”; 255 → “513”; 183 → “351”
      base 8: 75 → “113”; 255 → “377”; 183 → “267”

      Only for the case of base 7 do the same 3 digits appear in different orders.

      And in this case the substitution: A=5, D=1, X=3, gives us:

      DAX = 153 [base 7] = 87 [base 10]

      So: 87 is certainly a possible answer (and it is the published answer).


      But there are other solutions where the positions represent different values, but not necessarily increasing powers of some base.

      For instance instead of the positions standing for (49, 7, 1) and the symbols being A=5, D=1, X=3, we swap them over to give:

      positions = (1, 5, 3); A=7, D=49, X=1
      DXA = 49×1 + 1×5 + 7×3 = 75
      ADX = 7×1 + 49×5 + 1×3 = 255
      XAD = 1×1 + 7×5 + 49×3 = 183
      DAX = 49×1 + 7×5 + 1×3 = 87

      But we can also use decreasing position values:

      positions = (39, 11, 7); A=6, D=0, X=3
      DXA = 0×39 + 3×11 + 6×7 = 75
      ADX = 6×39 + 0×11 + 3×7 = 255
      XAD = 3×39 + 6×11 + 0×7 = 183
      DAX = 0×39 + 6×11 + 3×7 = 87

      Note: this combination of positions is able to express all numbers greater than 59.

      positions = (117, 33, 21); A=2, D=0, X=1 (Note: symbols are 0, 1, 2):
      DXA = 0×117 + 1×33 + 2×21 = 75
      ADX = 2×117 + 0×33 + 1×21 = 255
      XAD = 1×117 + 2×33 + 0×21 = 183
      DAX = 0×117 + 2×33 + 1×21 = 87

      Note: this combination of positions can only be used to express multiples of 3.

      Or mixed positions:

      positions = (7, 31, 19); A=1, D=8, X=0
      DXA = 8×7 + 0×31 + 1×19 = 75
      ADX = 1×7 + 8×31 + 0×19 = 255
      XAD = 0×7 + 1×31 + 8×19 = 183
      DAX = 8×7 + 1×31 + 0×19 = 87

      Note: this combination of positions is able to express all numbers greater than 86.

      And we can also find viable answers of 159 or 267.

      positions = (31, 19, 7); A=8, D=0, X=1
      DXA = 0×31 + 1×19 + 8×7 = 75
      ADX = 8×31 + 0×19 + 1×7 = 255
      XAD = 1×31 + 8×19 + 0×7 = 183
      DAX = 0×31 + 8×19 + 1×7 = 159

      positions = (33, 21, 117); A=0, D=1, X=2 (Note: symbols are 0, 1, 2)
      DXA = 1×33 + 2×21 + 0×117 = 75
      ADX = 0×33 + 1×21 + 2×117 = 255
      XAD = 2×33 + 0×21 + 1×117 = 183
      DAX = 1×33 + 0×21 + 2×117 = 267

      These values can also be found by permuting the (49, 7, 1) positions of the base 7 representation.

      Like

      • Jim Randell's avatar

        Jim Randell 11:13 pm on 28 June 2022 Permalink | Reply

        Here is a simple program using the [[ SubstitutedExpression ]] solver from the enigma.py library to solve the puzzle as a standard positional base system (which is how the puzzle is intended).

        It runs in 65ms.

        from enigma import (SubstitutedExpression, map2str, printf)
        
        # consider bases 7 and 8
        for b in [7, 8]:
          # create the alphametic puzzle
          p = SubstitutedExpression(
            [ "DXA == 75", "ADX == 255", "XAD == 183" ],
            base=b,
            answer="DAX",
            verbose=0,
          )
          # solve it
          for (s, ans) in p.solve():
            # output solution
            printf("DAX={ans} [b={b}; {s}]", s=map2str(s, enc=""))
        

        Like

    • GeoffR's avatar

      GeoffR 10:06 am on 29 June 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      % test for numerical bases 7 and 8 only, as Jim's analysis
      % i.e. symbols A (for the triangle), D (for the box), and X (for the loop)
      
      var 1..7:D; var 1..7:A; var 1..7:X;
      
      constraint  % base 7
      (D < 7 /\ A < 7 /\ X < 7 
      /\ (7 * 7 * D + 7 * X + A == 75) 
      /\ (7 * 7 * A + 7 * D + X == 255) 
      /\ (7 * 7 * X + 7 * A + D == 183))
      \/  % base 8
      (  (8 * 8 * D + 8 * X + A == 75) 
      /\ (8 * 8 * A + 8 * D + X == 255)
      /\ (8 * 8 * X + 8 * A + D == 183));
      
      solve satisfy;
      
      output ["[D, A, X ] = " ++ show([D, A, X ]) ];
      
      % [D, A, X ] = [1, 5, 3]  
      % ----------
      % ==========
      % DXA = 135, ADX = 513, XAD = 351, DAX = 153 (base 7)
      % DAX (base 7) = 1*7*7 + 5*7 + 3 = 87 ( base 10)
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:55 am on 26 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 101: Midsummer birthday madness 

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

    Midsummer Day, 1962 (June 24) was my youngest grandson John’s first birthday, and I was then able to claim that my nine grandchildren were aged 0, 1, 2, 3, 4, 5, 6, 7, and 8 years old (neglecting, of course, the odd days). They were all born in June, and if they are arranged in birthday order through the month the following facts are true:

    John is the middle grandchild;

    The sum of the dates of the last four is an exact multiple of the sum of the dates of the first four;

    The sum of the ages of the last four is two-thirds of the sum of the ages of the first four;

    The sum of the years of birth of the first three is equal to the sum of the years of birth of the last three;

    The intervals between birthdays are 0, 1, 2, 3, 4, 5, 6, and 7 days, but not in that order;

    Also:

    My eldest son’s two daughters are exactly two years apart;

    The twins were born four hours apart;

    Two children are as old as their dates of birth.

    What was the date of birth of the grandchild born in 1954?

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

    There are now 700 Teaser puzzles available on the site.

    [teaser101]

     
    • Jim Randell's avatar

      Jim Randell 11:56 am on 26 June 2022 Permalink | Reply

      The ages are all different, so the twins must be born either side of midnight on the 24th, so John is one of the twins, and the other must have a birthday of the 25th. John is the youngest grandson, so his (younger) twin must be a girl.

      This Python program starts with the 24th in the middle of the order, and then chooses an ordering for the intervals to give the remaining dates. It then assigns the ages, and checks all the required conditions apply.

      It runs in 161ms.

      Run: [ @replit ]

      from enigma import (subsets, div, printf)
      
      # the middle birthday is on the 24th, and the separation between
      # birthdays is (0, 1, 2, 3, 4, 5, 6, 7), in some order
      for ss in subsets((0, 1, 2, 3, 4, 5, 6, 7), size=len, select="P"):
        # construct the dates
        d5 = 24
        (d4, d6) = (d5 - ss[3], d5 + ss[4])
        (d3, d7) = (d4 - ss[2], d6 + ss[5])
        (d2, d8) = (d3 - ss[1], d7 + ss[6])
        (d1, d9) = (d2 - ss[0], d8 + ss[7])
        if d1 < 1 or d9 > 30: continue
        ds = [ d1, d2, d3, d4, d5, d6, d7, d8, d9]
      
        # John's twin must be born on the 25th
        if 25 not in ds: continue
      
        # sum of the dates of the last 4 is an exact multiple of the sum of
        # the dates of the first four
        x = div(sum(ds[-4:]), sum(ds[:4]))
        if x is None or x < 2: continue
      
        # choose values for the ages (John is 1)
        for ns in subsets((0, 2, 3, 4, 5, 6, 7, 8), size=len, select="P", fn=list):
          # insert 1
          ns.insert(4, 1)
      
          # John's twin (age 0) was born on the 25th
          if ds[ns.index(0)] != 25: continue
      
          # the sum of the ages of the last four is 2/3 the sum of the ages
          # of the first four
          if 3 * sum(ns[-4:]) != 2 * sum(ns[:4]): continue
      
          # [exactly] 2 ages are the same as the date
          if sum(x == y for (x, y) in zip(ds, ns)) != 2: continue
      
          # the sum of the years of birth of the first three is the same as
          # the sum of the years of birth of the last three
          ys = list(1962 - x for x in ns[:5]) + list(1961 - x for x in ns[5:])
          if sum(ys[:3]) != sum(ys[-3:]): continue
      
          # find the 2 grandchildren that share a birthday
          i = ss.index(0)
          # neither can be John
          if i == 3 or i == 4: continue
          # the ages must differ by 2
          if abs(ns[i] - ns[i + 1]) != 2: continue
      
          # output solution
          for (d, n, y) in zip(ds, ns, ys):
            printf("{d:02d} June {y} -> age = {n}{s}", s=(' [*** SOLUTION ***]' if y == 1954 else ''))
          printf()
      

      Solution: The birthday of the grandchild born in 1954 is 11th June.

      The dates of birth and ages are:

      02 June 1960 → age = 2 (age 2 on the 2nd)
      07 June 1955 → age = 7 (age 7 on the 7th)
      11 June 1954 → age = 8 (answer)
      17 June 1958 → age = 4
      24 June 1961 → age = 1 (today, John’s birthday)
      25 June 1961 → age = 0 (John’s twin sister)
      28 June 1958 → ages = 3 & 5 (two granddaughters, exactly 2 years apart)
      30 June 1955 → age = 6

      The program prints two solutions because the granddaughters born on 28th June could appear in either order.

      Like

    • Frits's avatar

      Frits 3:09 pm on 29 June 2022 Permalink | Reply

         
      # dates: A,  B,  C,  D,  E,  F,  G,  H,  I  in order
      # ages : R,  S,  T,  U,  V,  W,  X,  Y,  Z  not necessarily in order
      
      '''
      Notation: XYZ stands for X + Y + Z
      
      The ages are all different, so the twins must be born either side of 
      midnight on the 24th, so John is one of the twins, and the other must
      have a birthday of the 25th. John is the youngest grandson, 
      so his (younger) twin must be a girl.
      
      E = 24, F = 25, V = 1, zero age is either W or X 
      
      After F (25) we can only have intervals 0, 2 and 3 otherwise I exceeds 30
      so I has to be 30 
      A has to be equal to E (24) - intervals 4, 5, 6 and 7, so A = 2
      
      Two children are as old as their dates of birth
      so R = A and S = B
      
      The sum of the ages of the last four is two-thirds of the sum of the
      ages of the first four, sum of all ages is 36, John in the middle is 1
      so we have to split the remaining 35 into a 21 and 14. 
      
      The sum of the years of birth of the first three is equal to the sum of 
      the years of birth of the last three, RST must be equal to XYZ + 3.
      
      2 * RSTU = 3 * WXYZ ==> 2 * RST + 2 * U = 3 * W + 3 * RST - 9
      ==> ST = 2 * U - 3 * W + 7  (using R = 2)
      
      RSTUVWXYZ = 36 
      
      R    [     ST      ]       V       [    XYZ      ]   
      2 +  2 * U - 3 * W + 7 + U + 1 + W + 2 * U - 3 * W + 6 = 36
      5 * (U - W) = 20 ==> U = W + 4
      (U, W) is (4, 0), (7, 3) or (8, 4)
      
      If W is not zero:
        X must be zero leaving (U, W) to be (7, 3) or (8, 4).
        Two daughters with same birthday are exactly two years apart.
        These must be Y and Z (not W as V is male and Y can't be 2)
        W + Y + Z = 14. if W is odd than Y and Z can't be two years apart.
        This leaves W = 4 and Y + Z = 10, only two years apart option for Y and Z
        is 4 and 6. This is impossible as W already is 4. So W = 0 and U = 4.
      '''
      
      # dates: 2,  B,  C,  D, 24, 25,  G,  H, 30  in order
      # ages : 2,  S,  T,  4,  1,  0,  X,  Y,  Z  not necessarily in order
      
      '''
      In order to have after F the intervals 2 and 3 either G or H must be 27 or 28
      so 109 <= FGHI <= 115.
      In order to have intervals 4, 5, 6, 7 we have 36 <= ABCD <= 46
      (using 2, 6, 11, 17 resp. 2, 9, 15, 20 for A, B, C, D). 
      
      So FGHI = 3 * ABCD (using multiplier three) leaving only values 37 and 38
      for ABCD (with FGHI resp. 111 and 114) .
      Using (2, 8, C, D) for (A, B, C, D) we get at least a sum of 39
      then (A, B, C, D) is either (2, 6, C, D) or (2, 7, C, D).
      
      As U = 4 RST must be 17, ST = 15 so R and S must use 7 and 8 
      leaving 3, 5 and 6 for X, Y and Z.
      
      As S is 7 or 8 and B is either 6 or 7 we can deduce R = 7, B = 7, S = 8.
      
      Using (2, 7, 13, 17) for (A, B, C, D) we get at a sum of 39 so 
      (A, B, C, D) is (2, 7, 11, 17). 
      '''
      
      # dates: 2,  7, 11, 17, 24, 25,  G,  H, 30  in order
      # ages : 2,  7,  8,  4,  1,  0,  X,  Y,  Z  not necessarily in order
      
      '''
      As a grandchild born in 1954 can only occur with age 8 and date before 25
      or age 7 and date after 24 we see the answer is 11th June 1954.
      '''
      

      Like

  • Unknown's avatar

    Jim Randell 3:55 pm on 24 June 2022 Permalink | Reply
    Tags:   

    Teaser 3118: Product dates 

    From The Sunday Times, 26th June 2022 [link] [link]

    If a date during the current millennium is day D in month M during year (2000+N), it is said to be a product date if the product of D and M equals N (for example 11 February 2022).

    My daughter and I have been investigating the numbers of days from one product date to the next product date. I was able to establish the longest such interval L, while my daughter worked out the shortest such interval S. We were surprised to find that L is a whole number multiple of S.

    What is that multiple?

    [teaser3118]

     
    • Jim Randell's avatar

      Jim Randell 4:07 pm on 24 June 2022 Permalink | Reply

      This Python program runs in 61ms. (Internal run time is 862µs).

      Run: [ @replit ]

      from datetime import date
      from enigma import (Accumulator, irange, product, tuples, catch, div, printf)
      
      # generate "product dates" in the 21st century
      def generate():
        # consider each possible D, M pairing
        for (D, M) in product(irange(1, 31), irange(1, 12)):
          N = D * M
          d = catch(date, 2000 + N, M, D)
          if d is not None:
            yield d
      
      # find the maximum and minimum separations between consecutive dates
      rs = Accumulator.multi(fns=[max, min], collect=1)
      for (a, b) in tuples(sorted(generate()), 2):
        d = (b - a).days
        rs.accumulate_data(d, (a, b))
      
      # output solution
      (L, S) = (x.value for x in rs)
      d = div(L, S)
      printf("L/S = {d} [L={L} S={S}]")
      

      Solution: The multiple is 274.

      The longest interval is 4384 days (2236-12-28 → 2348-12-29 → 2360-12-30 → 2372-12-31).

      The shortest interval is 16 days (2030-01-30 → 2030-02-15).

      Like

  • Unknown's avatar

    Jim Randell 8:56 am on 23 June 2022 Permalink | Reply
    Tags: ,   

    Teaser 2753: No question about it! 

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

    “Sign Works” make rectangular signs of all sizes. Pat ordered a sign for the pub with the following instructions:

    “The length must be twice the width. Furthermore, the dimensions should be such that if you take its length (in centimetres), square the sum of its digits and take away the length itself, then you get the width. On the other hand, if you take its width (in centimetres), square the sum of its digits and take away the width itself, then you get the length”.

    This was enough information for the sign-maker to calculate the dimensions of the sign.

    What were they?

    [teaser2753]

     
    • Jim Randell's avatar

      Jim Randell 8:57 am on 23 June 2022 Permalink | Reply

      This is very straightforward.

      from enigma import (irange, inf, dsum, sq, printf)
      
      for width in irange(1, inf):
        length  = 2 * width
        if width + length == sq(dsum(width)) == sq(dsum(length)):
          printf("width={width} length={length}")
          break
      

      Solution: The sign was 27 cm × 54 cm.

      Like

    • GeoffR's avatar

      GeoffR 9:30 am on 23 June 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:a; var 1..9:b; % length digits
      var 1..9:c; var 1..9:d; % width digits
      
      % Length and width of sign ( assumed 2 digits)
      var 10..99:L == 10*a + b; 
      var 10..99:W == 10*c + d;
      
      constraint L == 2 * W;
      
      constraint W == (a + b) * (a + b) - L;
      
      constraint L == (c + d) * (c + d) - W;
      
      solve satisfy;
      
      output [ "Length(cm) = " ++ show(L) ++ ", Width(cm) = " ++ show(W) ];
      
      % Length(cm) = 54, Width(cm) = 27
      % ----------
      % ==========
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:16 am on 21 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 719: Wotta-Woppa! 

    From The Sunday Times, 27th April 1975 [link]

    On the Island of Imperfection there are three tribes; the Pukkas who always tell the truth, the Wotta-Woppas who never tell the truth, and the Shilli-Shallas who make statements which are alternately true and false, or false and true. This story is about three inhabitants; Ugly, Stupid and Toothless, whose names give some indication of the nature of their imperfections.

    The island currency is a Hope. The weekly wage of a Pukka is between 10 and 19 Hopes, of a Shilli-Shalla between 20 and 29, and of a Wotta-Woppa between 30 and 39 (in each case a whole number of Hopes). They make three statements each, anonymously:

    A says: (1) B is a Wotta-Woppa. (2) My wages are 25% less than 20% more than one of the others. (3) I get 10 hopes more than B.

    B says: (1) The wages of one of us is different from the sum of those of the other two. (2) We all belong to the same tribe. (3) More of C‘s statements are true than mine.

    C says: (1) Ugly earns more than Toothless. (2) The wages of one of us are 15% less than the wages of another. (3) Stupid is a Shilli-Shalla.

    Find C‘s name, tribe and wages.

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

    [teaser718]

     
    • Jim Randell's avatar

      Jim Randell 11:17 am on 21 June 2022 Permalink | Reply

      This is similar to Puzzle 15 (and Puzzle 66).

      Some obvious initial analysis:

      Statement B1 (“The wages of one of us is different from the sum of those of the other two”) must always be true, as it it not possible for each of the three to have wages that are equal to the sum of the other two. (Who would have the largest value?)

      So B cannot be a Wotta-Woppa. (And so A1 is false).

      Which means B3 must be also be true. So C must make more true statements than B, so B cannot be a Pukka. Hence B is a Shilli-Shalla, and C is a Pukka. (And B2 is false).

      And so C3 must be true, and “Stupid” is a Shilli-Shalla.

      And as A1 is false, then A cannot be a Pukka, and A3 must also be false.

      I incorporated these findings into a generic solution to get a program that runs in 71ms.

      Run: [ @replit ]

      from enigma import (subsets, tuples, irange, product, div, intersect, printf)
      
      # Pukka - always tell the truth
      def P(ss):
        return all(ss)
      
      # Wotta-Woppa - never tells the truth
      def W(ss):
        return not any(ss)
      
      # Shilli-Shalla - alternates between true and false
      def S(ss):
        return all(a != b for (a, b) in tuples(ss, 2))
      
      # wages, by tribe
      wage = { P: irange(10, 19), S: irange(20, 29), W: irange(30, 39) }
      
      # assign tribes to the participants
      for ts in ((W, S, P), (S, S, P)):
        (A, B, C) = ts
      
        # B's statements
        Bs = [True, False, True]
        assert B(Bs)
      
        # assign names to the participants
        for ns in subsets("STU", size=3, select="P"):
          (nA, nB, nC) = ns
          if nC == 'S': continue  # not possible
          (iS, iT, iU) = (ns.index(x) for x in "STU")
      
          # assign wages to the participants
          for ws in product(wage[A], wage[B], wage[C]):
            (wA, wB, wC) = ws
      
            # check A's statements
            As = [False, div(10 * wA, 9) in {wB, wC}, wA == wB + 10]
            if not A(As): continue
      
            # check C's statements
            Cs = [
              ws[iU] > ws[iT],
              ts[iS] is S,
              bool(intersect([{ div(85 * x, 100) for x in ws }, ws])),
            ]
            if not C(Cs): continue
      
            # check B's 3rd statement
            assert Cs.count(True) > Bs.count(True)
      
            # output solution
            f = lambda x: x.__name__
            printf("A={A} {nA} {wA}; B={B} {nB} {wB}; C={C} {nC} {wC} [{As}; {Bs}; {Cs}]", A=f(A), B=f(B), C=f(C))
      

      Solution: C is Toothless, he is a Pukka, his wage is 17 Hopes.

      A is Ugly, he is a Wotta-Woppa, his wage is 31 – 39 Hopes.

      B is Stupid, he is a Shilli-Shalla, his wage is 20 Hopes.

      C‘s wage (17) is 15% less than that of B (20).

      Like

      • Frits's avatar

        Frits 3:04 pm on 21 June 2022 Permalink | Reply

        @Jim, You swapped the ranges of Wotta-Woppa and Shilli-Shalla.

        Like

      • Jim Randell's avatar

        Jim Randell 12:25 pm on 27 June 2022 Permalink | Reply

        Inspired by Frits’ solution (below) I wrote a solution that uses the [[ SubstitutedExpression ]] solver from the enigma.py library, and is just a restatement of the puzzle text with no analysis.

        The values assigned for the tribes make it easy to express the wages alphametically.

        This Python program runs in 73ms.

        Run: [ @replit ]

        from enigma import (SubstitutedExpression, irange, tuples, div, printf)
        
        #    statements  tribe  name  wage
        # A: a d g       p      x     ps
        # B: b e h       q      y     qt
        # C: c f i       r      z     ru
        #
        # statements: 0 (false), 1 (true)
        # tribe: 1 (P), 2 (SS), 3 (WW)
        # name: 1 (S), 2 (T), 3 (U)
        # wage (units): 0 - 9
        
        # check statements for a tribe
        def tribe(t, ss):
          if t == 1: return all(ss)
          if t == 2: return all(a != b for (a, b) in tuples(ss, 2))
          if t == 3: return not any(ss)
        
        # B1: one of the wages is different from the sum of the other two
        def B1(*ws):
          t = sum(ws)
          return any(w != t - w for w in ws)
        
        # C1: Ugly (3) earns more than Toothless (2)
        def C1(ns, ws):
          d = dict(zip(ns, ws))
          return d[3] > d[2]
        
        # C2: the wages of one of us is 85% the wages of another
        def C2(*ws):
          for w in ws:
            x = div(85 * w, 100)
            if x in ws: return True
          return False
        
        # expressions to evaluate
        exprs = [
          # tribes and statements
          "tribe({p}, [{a}, {d}, {g}])",
          "tribe({q}, [{b}, {e}, {h}])",
          "tribe({r}, [{c}, {f}, {i}])",
        
          # A1: "B is a Wotta-Woppa (3)" = {a}
          "int({q} == 3) = {a}",
          # A2: "My wages are [0.9] one of the others" = {d}
          "int(div({ps} * 10, 9) in { {qt}, {ru} }) = {d}",
          # A3: "I get 10 Hopes more than B" = {g}
          "int({ps} == {qt} + 10) = {g}",
          # B1: "The wages of one of us is different from the sum of the wages
          # of the other two" = {b}
          "int(B1({ps}, {qt}, {ru})) = {b}",
          # B2: "We all belong the same tribe" = {e}
          "int({p} == {q} == {r}) = {e}",
          # B3: "More of C's statements are true than mine" = {h}
          "int({c} + {f} + {i} > {b} + {e} + {h}) = {h}",
          # C1: "Ugly earns more than Toothless" = {c}
          "int(C1([{x}, {y}, {z}], [{ps}, {qt}, {ru}])) = {c}",
          # C2: "The wages of one of us are 0.85 the wages of another" = {f}
          "int(C2({ps}, {qt}, {ru})) == {f}",
          # C3: "Stupid (1) is a Shilli-Shalla (2)" = {i}
          "int({ {x}: {p}, {y}: {q}, {z}: {r} }[1] == 2) = {i}",
        ]
        
        # invalid digit / symbol assignments
        d2i = dict()
        for d in irange(0, 9):
          vs = set()
          if d > 1: vs.update('abcdefghi')
          if d < 1 or d > 3: vs.update('pqrxyz')
          d2i[d] = vs
        
        # create the puzzle
        p = SubstitutedExpression(
          exprs,
          distinct="xyz",
          d2i=d2i,
          env=dict(tribe=tribe, B1=B1, C1=C1, C2=C2),
          verbose=0,
        )
        
        # solve the puzzle and output solutions
        Ts = { 1: "Pukka", 2: "Shilli-Shalla", 3: "Wotta-Woppa" }
        Ns = { 1: "Stupid", 2: "Toothless", 3: "Ugly" }
        for s in p.solve():
          (a, b, c, d, e, f, g, h, i, p, q, r, s, t, u, x, y, z) = (s[k] for k in 'abcdefghipqrstuxyz')
          printf("A: {N:9s}  {T:13s}  {p}{s}  [{a} {d} {g}]", N=Ns[x], T=Ts[p])
          printf("B: {N:9s}  {T:13s}  {q}{t}  [{b} {e} {h}]", N=Ns[y], T=Ts[q])
          printf("C: {N:9s}  {T:13s}  {r}{u}  [{c} {f} {i}]", N=Ns[z], T=Ts[r])
          printf()
        

        Like

        • Frits's avatar

          Frits 1:31 pm on 29 June 2022 Permalink | Reply

          Nice idea to let tribes go from 1 to 3 so you can use the tribe value as a tens unit of the wage.

          Like

    • Frits's avatar

      Frits 3:30 pm on 21 June 2022 Permalink | Reply

      For all tribes the veracity of the first and third statement must be the same.

         
      from enigma import SubstitutedExpression
      
      # tribes 0 = Wotta-Woppa, 1 = Shilli-Shilla, 2 = Pukka 
      # names: 0 = Stupid, 1 = Toothless, 2 = Ugly
      
      #     stmnts  tribe  wages   name
      #      0/1    0/1/2         0/1/2
      # A:  D E D     P     UV      K
      # B:  F G F     Q     WX      L
      # C:  H I H     R     YZ      M
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # if Wotta-Woppa all statements are false
          "P != 0 or D == E == 0",
          "Q != 0 or F == G == 0",
          "R != 0 or H == I == 0",
          
          # if Shilli-Shalla all statements are alternating true/false
          "P != 1 or D + E == 1",
          "Q != 1 or F + G == 1",
          "R != 1 or H + I == 1",
          
          # if Pukka all statements are true
          "P != 2 or D == E == 1",
          "Q != 2 or F == G == 1",
          "R != 2 or H == I == 1",
          
          # ranges: Pukka 10-19, Shilli-Shalla 20-29 and Wotta-Woppa 30-39
          "(P != 0 or U == 3) and (P != 1 or U == 2) and (P != 2 or U == 1)",
          "(Q != 0 or W == 3) and (Q != 1 or W == 2) and (Q != 2 or W == 1)",
          "(R != 0 or Y == 3) and (R != 1 or Y == 2) and (R != 2 or Y == 1)",
          
          # A1: B is a Wotta-Woppa
          "(F == G == 0) == D",
          # A2: My wages are 25% less than 20% more than one of the others
          "(UV in {0.9 * WX, 0.9 * YZ}) == E",
          # A3: I get 10 hopes more than B
          "(UV == WX + 10) == D",
          
          # B1: The wages of one of us is different from the sum of those 
          # of the other two
          "(UV != WX + YZ or UV + WX != YZ or UV + YZ != WX) == F",
          # B2: We all belong to the same tribe
          "(P == Q == R) == G",
          # B3: More of C's statements are true than mine
          "(2* H + I >  2 * F + G) == F",
          
          
          # C1: Ugly earns more than Toothless
          "((UV if K == 2 else WX if L == 2 else YZ) > \
            (UV if K == 1 else WX if L == 1 else YZ)) == H",
          # C2: The wages of one of us are 15% less than the wages of another
          "((20 * UV in {17 * YZ, 17 * WX}) or \
            (20 * WX in {17 * YZ, 17 * UV}) or \
            (20 * YZ in {17 * UV, 17 * WX})) == I",
          # C3: Stupid is a Shilli-Shalla.
          "((K == 0 and P == 1) or \
            (L == 0 and Q == 1) or \
            (M == 0 and R == 1)) == H",
        ],
        answer="(D, E, F, G, H, I), (P, Q, R), (UV, WX, YZ), (K, L, M)",
        d2i=dict([(0, "UWY")] +
                 [(2, "DEFGHI")] +
                 [(3, "DEFGHIKLMPQR")] +
                 [(k, "DEFGHIKLMPQRUWY") for k in range(4, 10)]),
        distinct="KLM",
        verbose=0,
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

  • Unknown's avatar

    Jim Randell 3:11 pm on 19 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 77: Ali’s counter move 

    From The Sunday Times, 16th September 1962 [link]

    Ali and Baba are playing the Chay game, Ali has a bag containing 12 numbered tickets, 3 each of numbers 1, 2, 3, 4 (all numbers represented by strokes). Baba has 6 double-sided counters containing the same 12 numbers, and a board of 6 squares.

    Ali draws 6 tickets from his bag one at a time, calling out the number as he does so. At each call Baba selects a counter with that number on one of its sides and places it face up in a square. If in 6 calls he fills 6 squares he wins. Once a counter is laid it stays. The counter-couplings are so arranged that 5 squares could always be filled if the numbers were known beforehand.

    But, unnoticed by Baba, Ali has slyly added 1 stroke each to 2 of his opponent’s counters. As a result, Baba frequently places no more than 3 or 4 counters, and at last comes a deal when, after Ali has called “Two”, “One”, he calls a third number and Baba cannot fill it.

    It is the last straw.

    Baba, having lost many pice and much temper, angrily examines the four remaining counters. Three of them are identical couplings!

    “Ah! wicked one”, he cries, “you have forged my counters!”. And, throwing them down, he clears for action.

    What couplings are on these 4 counters?

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

    [teaser77]

     
    • Jim Randell's avatar

      Jim Randell 3:14 pm on 19 June 2022 Permalink | Reply

      This Python program considers possible sets of counters that meet the specified conditions, and looks at ways to fake two of the counters so that there are 3 counters the same. We then match two of the fake set to the calls “1” and “2” and look to see if there is a third call that cannot be matched from the remaining counters. (It is a good opportunity to do some work with multisets).

      This Python program runs in 295ms.

      Run: [ @replit ]

      from enigma import (multiset, irange, subsets, product, union, printf)
      
      # update multiset <m>, by removing values in <rem>, and adding values in <add>
      def update(m, rem=None, add=None):
        if rem:
          m = m.difference(multiset.from_seq(rem))
        else:
          m = m.copy()
        if add:
          m.update_from_seq(add)
        return m
      
      # partition the multiset into <k>-tuples
      # return a multiset of <k>-tuples
      def mpartitions(m, k, ss=[]):
        # are we done?
        if not m:
          yield multiset.from_seq(ss)
        else:
          # choose the next <k>-tuple
          for s in subsets(m, size=k, select="mC"):
            t = tuple(sorted(s))
            if (not ss) or not (t < ss[-1]):
              yield from mpartitions(update(m, rem=s), k, ss + [t])
      
      # the tickets
      tickets = multiset.from_seq([1, 2, 3, 4], count=3)
      
      # possible sequences of tickets
      tss = list(tickets.subsets(size=6))
      
      # possible counter displays
      def display(cs, ss=multiset()):
        if not cs:
          yield ss
        else:
          # choose the next key
          (a, b) = k = cs.peek()
          n = cs[k]
          # and choose the split (how many showing a, the rest show b)
          for x in irange(0, n):
            # remaining counters
            cs_ = cs.copy().remove(k, n)
            ss_ = ss.copy().update_from_pairs([(a, x), (b, n - x)])
            yield from display(cs_, ss_)
      
      # check this set of counters
      def check(cs, ts):
        # consider each possible display
        for x in display(cs):
          # remove ticket sequences with at least 5 matches
          ts = list(t for t in ts if len(x.intersection(t)) < 5)
          if not ts: return True
        return False
      
      # fake a counter by incrementing one side
      def fake1(x, y):
        yield (x, y + 1)
        if x < y: yield (x + 1, y)
      
      # fake a set of counters
      def fake(cs):
        # choose 2 counters
        for (a, b) in subsets(cs, size=2, select="mC"):
          # fake them
          for (fa, fb) in product(fake1(*a), fake1(*b)):
            # replace the originals with the fakes
            yield update(cs, rem=(a, b), add=(fa, fb))
      
      # consider possible counters
      for cs in mpartitions(tickets, 2):
        # check these counters can match at least 5 of all tickets sequences
        if not check(cs, tss): continue
        printf("counters = {cs}", cs=sorted(cs))
      
        # make the fake set of counters
        for fcs in fake(cs):
          # there should be 3 counters the same
          if all(v < 3 for v in fcs.values()): continue
          printf("-> faked = {fcs}", fcs=sorted(fcs))
      
          # choose 2 counters to match the calls "1" and "2"
          for (x, y) in subsets(fcs, size=2, select="mP"):
            if not (1 in x and 2 in y): continue
            # remaining counters
            rs = update(fcs, rem=(x, y))
            # is there a call that cannot be matched?
            if len(union(rs.keys())) == 4: continue
      
            # output solution (rs)
            printf("-> remaining = {rs}; [1 -> {x}, 2 -> {y}]", rs=sorted(rs))
            printf()
      

      Solution: The four remaining counters are: 2|4, 2|4, 2|4, 3|4.


      There is only one set of counters that meets the specified conditions:

      1|2, 1|3, 1|4, 2|3, 2|4, 3|4

      These are then faked by incrementing the 3rd and 4th counters to give:

      1|2, 1|3, 2|4, 2|4, 2|4, 3|4

      Which has 3 copies of the 2|4 counter.

      For the call of “1” the 1|3 counter is chosen. For the call of “2” the 1|2 counter is chosen.

      This leaves: 2|4, 2|4, 2|4, 3|4, which cannot match a call of “1”.

      Like

    • Frits's avatar

      Frits 12:10 pm on 21 June 2022 Permalink | Reply

      A lot easier to solve without using Python.

         
      # Suppose numbers in Ali's bag are a, b, c and d
      
      # ------ Check Baba's original set of counters -----------------
      # Can one counter have the same numbers on it?
      
      # Then fi we have counters a/a and a/x where x is b, c or d.
      # If Ali calls 3 a's and 3 x's then both his third a call and third x call 
      # can't be filled.   ===> each counter has 2 different numbers on it
      
      # Can 2 counters be the same?
      # Then fi we have counters a/b and a/b.
      # If Ali calls 3 a's followed by 3 b's then the last two b calls can't be
      # filled.   ===> no counter combination occurs more than once.
      
      # So Baba has to have 1/2, 1/3 and 1/4. He also needs to have 2/3 and 2/4
      # and finally 3/4.
      
      # so we have 1|2, 1|3, 1|4, 2|3, 2|4 and 3|4
      # --------------------------------------------------------------
      
      
      # In two counters a number is incremented by one, resulting in at least 
      # 3 counters with the same numbers x and y. This means the x|y counter must 
      # have been present in Baba's original set of counters.
      # x|y cannot be a counter like 1|z as there is only one option 1|(z-1) to 
      # produce an additional 1|z counter.   ===> 1|2, 1|3 and 1|4 are not x|y
      
      # x|y cannot be a counter like z|(z + 1) as there is only one option 
      # (z-1)|(z+1) to produce an additional z|(z + 1) counter.
      # ==> 2|3 and 3|4 are not x|y
      
      # so x|y is 2|4 and Baba's faked set being 1|2, 1|3, 2|4, 2|4, 2|4 and 3|4.
      # As Ali has called “Two” and “One” resp. 1|2 and 1|3 must have been placed
      # in a square.   ===> the four remaining counters are 2|4, 2|4, 2|4 and 3|4
      

      Like

  • Unknown's avatar

    Jim Randell 5:00 pm on 17 June 2022 Permalink | Reply
    Tags:   

    Teaser 3117: Stop me if you’ve heard this one 

    From The Sunday Times, 19th June 2022 [link] [link]

    A square, a triangle and a circle went into a bar.

    The barman said: “Are you numbers over 18?”

    They replied: “Yes, but we’re under a million”

    The square boasted: “I’m interesting, because I’m the square of a certain integer”

    The triangle said: “I’m more interesting — I’m a triangular number, the sum of all the integers up to that same integer”

    The circle said: “I’m most interesting — I’m the sum of you other two”

    “Well, are you actually a circular number?”

    “Certainly, in base 1501, because there my square ends in my number exactly. Now, shall we get the drinks in?”

    The square considered a while, and said: “All right, then. You(‘)r(e) round!”

    In base 10, what is the circular number?

    [teaser3117]

     
    • Jim Randell's avatar

      Jim Randell 5:27 pm on 17 June 2022 Permalink | Reply

      Circular numbers are also called automorphic numbers [@wikipedia], and we have encountered them before. See: Enigma 49, Enigma 1736.

      There is a unique value for the “certain integer” (and so S, T, C).

      This Python program runs in 59ms. (Internal runtime is 4.5ms).

      Run: [ @replit ]

      from enigma import (irange, inf, sq, tri, nsplit, zip_eq, join, printf)
      
      # consider possible "certain" integers
      for n in irange(6, inf):
        S = sq(n)
        T = tri(n)
        C = S + T
        if not (C < 1000000): break
        # consider the digits of C and C^2 in base 1501
        Cds = nsplit(C, base=1501)
        C2ds = nsplit(sq(C), base=1501)
        # does C^2 end with C?
        if zip_eq(Cds, C2ds, reverse=1):
          # output solution
          fmt = lambda ds: join(ds, sep=":", enc="{}")
          printf("n={n}: S={S} T={T} C={C} [C={Cds} C^2={C2ds}]", Cds=fmt(Cds), C2ds=fmt(C2ds))
      

      Solution: The circular number is 1027.

      Numbers up to 999,999 can be represented in base 1501 using 1- or 2- digits.

      And there are six 1- or 2- digit automorphic numbers in base 1501, which give potential values for C:

      0
      1
      475
      1027
      368220
      1884782

      The first two are too small. The last one is too big.

      Of the remaining three only 1027 gives a viable value for n, which is n = 26.

      i.e.

      S = sq(26) = 676
      T = tri(26) = 351
      C = 676 + 351 = 1027

      And in base 1501 we have:

      1027 = {1027}
      1027² = {702:1027}

      So (in base 1501) the final digit of C² is C.


      In fact, even if S and T are square and triangular numbers of (possibly) different integers, we can still work out a unique value for C.

      Like

      • Jim Randell's avatar

        Jim Randell 8:26 pm on 17 June 2022 Permalink | Reply

        Here is a faster version that doesn’t construct the base 1501 representations.

        It has an internal run time of 852µs.

        Run: [ @replit ]

        from enigma import (irange, inf, sq, tri, printf)
        
        # consider possible "certain" integers
        M = B = 1501
        for n in irange(6, inf):
          S = sq(n)
          T = tri(n)
          C = S + T
          if not (C < 1000000): break
          # consider the digits of C and C^2 in base 1501
          while not (C < M): M *= B
          if pow(C, 2, M) == C:
            # output solution
            printf("n={n}: S={S} T={T} C={C}")
        

        Liked by 1 person

        • Jim Randell's avatar

          Jim Randell 4:11 pm on 30 June 2022 Permalink | Reply

          I read up on how to find modular roots of integer-valued polynomials using Hensel lifting and the Chinese Remainder Theorem, and then wrote some code to implement it.

          Given the “certain integer” n, the following Python code constructs a polynomial function for f(n) = sq(C(n)) − C(n), and then finds the roots of this function modulo 1501^k (for k = 1, 2). We then check these roots give viable values of S, T, C.

          The internal runtime is 564µs, so it is faster than my simple approach above, but quite a lot more complicated.

          Run: [ @replit ]

          from enigma import (irange, prime_factor, gcd, lcm, egcd, cproduct, sq, tri, ndigits, arg, printf)
          
          # simplified CRT
          # solve: x = a (mod m); x = b (mod n)
          def crt1(a, b, m, n):
            g = gcd(m, n)
            if (a - b) % g != 0: return None
            (x, y, z) = (m // g, n // g, (m * n) // g)
            (u, v, _) = egcd(x, y)
            return (b * u * x + a * v * y) % z
          
          # general Chinese Remainder Theorem
          # solve: x = rs[i] (mod ms[i])
          def crt(rs, ms):
            assert len(rs) == len(ms) # or use zip(..., strict=1)
            x = mm = None
            for (r, m) in zip(rs, ms):
              if x is None:
                (x, mm) = (r, m)
              else:
                x = crt1(r, x, m, mm)
                if x is None: return None
                mm = lcm(m, mm)
            return x
          
          # find modular roots of a polynomial
          class PolyRootsMod(object):
          
            def __init__(self, f, cache=1):
              self.f = f
              self.cache = (dict() if cache else None)
          
            # find roots of <f> mod (<b>^<k>) using Hensel's lemma
            def hensel(self, b, k):
              if k == 0: return [0]
              assert k > 0
              bk = b**k
              # check the cache
              cache = self.cache
              if cache:
                rs = cache.get(bk)
                if rs is not None:
                  return rs
              # find the roots
              f = self.f
              k_ = k - 1
              bk_ = b**k_
              rs = list()
              for r in self.hensel(b, k_):
                for i in irange(b):
                  x = i * bk_ + r
                  if f(x) % bk == 0:
                    rs.append(x)
              # cache if necessary
              if cache is not None: cache[bk] = rs
              return rs
          
            # find roots of polynomial <p> mod <n>^<k>
            def roots_mod(self, n, k=1):
          
              # find roots for each prime factor of <n>^<k>
              (rs, bs) = (list(), list())
              for (f, e) in prime_factor(n):
                rs.append(self.hensel(f, e * k))
                bs.append(pow(f, e * k))
          
              # combine roots using CRT
              for xs in cproduct(rs):
                yield crt(xs, bs)
          
          
          B = arg(1501, 0, int)
          printf("[B={B}]")
          
          # start with polynomial functions: sq, tri, circ
          circ = lambda n: sq(n) + tri(n)
          
          # find k-digit automorphic values for C in base B
          f = lambda x: x * (x - 1)
          poly = PolyRootsMod(lambda n: f(circ(n)))
          for k in irange(1, ndigits(999999, base=B)):
            for n in poly.roots_mod(B, k):
              S = sq(n)
              T = tri(n)
              C = circ(n)
              if S < 18 or T < 18 or not (C < 1000000): continue
              if ndigits(C, base=B) != k: continue
              printf("S={S} T={T} C={C} [n={n}]")
          

          Like

      • Jim Randell's avatar

        Jim Randell 9:50 am on 18 June 2022 Permalink | Reply

        Here is a version adapted from my code for Enigma 49. It proceeds by constructing possible automorphic values for C and then calculating the corresponding “certain integer” n, and from that S and T. Although this is not as fast as my previous program.

        from enigma import (irange, ndigits, uniq, quadratic, sq, tri, printf)
        
        # generate <k> digit automorphic numbers (in base <b>)
        # i.e. x where the rightmost k digits of x^n are the digits of x
        # yield (<k>, <x>) where <k> in <ks>
        def automorphic(ks, b=10, n=2):
          K = max(ks)
          (k, xs, p, q) = (1, [0], 1, b)
          while not (k > K):
            f = (k in ks)
            xs_ = list()
            for d in irange(b):
              for x in xs:
                x_ = d * p + x
                if pow(x_, n, q) == x_:
                  if f: yield (k, x_)
                  xs_.append(x_)
            (k, xs, p, q) = (k + 1, xs_, q, q * b)
        
        # consider 1-, 2- digit automorphic numbers in base 1501
        d = ndigits(999999, base=1501)
        for C in uniq(x for (k, x) in automorphic(irange(1, d), 1501)):
          if C < 18 or not (C < 1000000): continue
          # calculate n: C = n(3n + 1)/2
          for n in quadratic(3, 1, -2 * C, domain="Z"):
            if n < 6: continue
            S = sq(n)
            T = tri(n)
            printf("C={C} S={S} T={T} [n={n}]")
        

        And here is a version which proceeds by constructing possible values for C using the technique given on the Wikipedia page for automorphic numbers [@wikipedia].

        from enigma import (irange, quadratic, sq, tri, printf)
        
        # Hensel's lemma - see [ https://en.wikipedia.org/wiki/Automorphic_number ]
        def hensel(poly, base, power):
          if power == 0: return [0]
          roots = hensel(poly, base, power - 1)
          rs = list()
          for r in roots:
            for i in irange(base):
              j = i * base ** (power - 1) + r
              x = poly(j) % (base ** power)
              if x == 0:
                rs.append(j)
          return rs
        
        # generate <k> digit automorphic numbers in base <b>
        # i.e. values of x where the rightmost k digits of x^2 are the digits of x
        def automorphic(k, b=10):
          poly = lambda x: x * x - x
          return hensel(poly, b, k)
        
        # consider 1-, 2- digit automorphic numbers in base 1501
        for k in (1, 2):
          for C in automorphic(k, 1501):
            if C < 18 or not (C < 1000000): continue
            # calculate n: C = n(3n + 1)/2
            for n in quadratic(3, 1, -2 * C, domain="Z"):
              if n < 6: continue
              S = sq(n)
              T = tri(n)
              printf("C={C} S={S} T={T} [n={n}]")
        

        Like

    • Robert Brown's avatar

      Robert Brown 6:19 pm on 17 June 2022 Permalink | Reply

      Hi Jim
      Your version of the text differs from the Sunday Times site. This asks “In base 10, what is the circular number?” You seem to have “what is the certain integer”.

      Like

      • Jim Randell's avatar

        Jim Randell 6:27 pm on 17 June 2022 Permalink | Reply

        Thanks. Of course if you know the “certain integer”, you can easily calculate the square, triangular and circular numbers.

        Interestingly, there is only one possible value for C, even if S and T are square and triangular numbers of different integers.

        Like

    • GeoffR's avatar

      GeoffR 8:32 pm on 17 June 2022 Permalink | Reply

      if S and T are both less than 1,000,000, then C is less than 2,000,000.

      The last two digits in base 1501 give a max value of 2,253,000.

      i.e. 1500 *1501 + 1500, which is adequate to define C.

      
      # Square and triangular numbers are less than 1,000,000
      sqs = [x * x for x in range(5, 1000)]
      tris = [ x * (x+1)// 2 for x in range(6, 1410)]
      
      CNum = []  # list to store circular numbers
      
      # consider possible (S, T) combinations
      for S in sqs:
        for T in tris:
          C = S + T
          C2 = C * C
          # only 2 digits in base 1501 are needed
          # for the maximum value of C
          num, rem = divmod(C2, 1501)
          if rem == C:
            if C not in CNum: CNum.append(C)
      
      for n in CNum:
          print(f"Circular number = {n}")
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:42 pm on 17 June 2022 Permalink | Reply

        @GeoffR: This is very similar to the first program I wrote. And then I realised that S = n² and T = tri(n) for the same value of n, which makes things much faster.

        It does however lead to the same answer for C, so there is a harder problem lurking inside this puzzle.

        Also I think the text implies that all of S, T, C are less than a million (and more than 17).

        But C is 1- or 2-digits in base 1501, so C² could be up to 4 digits, and we need to check the final 2 digits if C is length 2.

        Like

    • Frits's avatar

      Frits 11:11 pm on 17 June 2022 Permalink | Reply

      Approaching it from the possible C values.

      I have assumed that when the number of digits in the remainder of (C^2 base 1501) is smaller than the number of digits in C we need to also check the last digits of the divisor of (C^2 base 1501).

         
      B = 1501
      
      # C = f(n) = n(3n + 1) // 2
      # f(n+1) - f(n) = (n + 1)(3(n + 1) + 1) // 2 - n(3n + 1) // 2
      #               = 2 + 3n
      
      C = 40        # as C >= 2 * 18
      for n in range(5, 817):  
        sC = str(C)
        lC = len(sC)
          
        C2 = C**2
        sC2 = str(C2)
        
        # next C
        C += (2 + 3 * n)
        
        # calculate remainder
        R1 = C2 % B
        lR1 = len(str(R1))
        
        # can we directly check all digits of C
        if lR1 >= lC:
          if str(R1)[-lC:] != sC: continue
        else: # len(R1) < lC
          # check last digits of C
          if sC[-lR1:] != str(R1): continue
          
          # check the digits of the divisor
          R2 = ((C2 - R1) // B) % B
          lR2 = len(str(R2))
          
          # check if digits in C correspond with the remainder of the divisor
          if sC[-lR1 + lR2:-lR1] != str(R2): continue
          
          # too lazy to code what happens if lR1 + lR2 < lC
          if lR1 + lR2 < lC: continue
          
        print("answer:", sC)
      

      Like

    • Frits's avatar

      Frits 2:39 pm on 18 June 2022 Permalink | Reply

      @Jim,

      If the base is 272 is C = 57 an answer as C2ds=(11, 257) ?

      Like

      • Jim Randell's avatar

        Jim Randell 2:55 pm on 18 June 2022 Permalink | Reply

        @Frits: 57 is not automorphic in base 272.

        57 = {57}
        57² = {11:257}

        You compare the “digits” as atomic values not strings, so the final digit of 57² is 257 and this is not the same as 57.

        But 17 and 256 are both 1-digit automorphic numbers in base 272:

        17 = {17}
        17² = {1:17}

        256 = {256}
        256² = {240:256}

        Like

    • Frits's avatar

      Frits 4:48 pm on 18 June 2022 Permalink | Reply

      There is no solution if the circular number has to be a round number as well (at least not in base 10).

      Like

    • Jim Randell's avatar

      Jim Randell 9:06 am on 21 June 2022 Permalink | Reply

      For an answer with larger values for S, T, C, but a smaller base, try solving the same puzzle with a base of 105.

      Like

    • GeoffR's avatar

      GeoffR 12:08 pm on 21 June 2022 Permalink | Reply

      Easy to adapt Jim’s first program, substituting 105 for 1501 as the number base.

      This gives:

      n=458: S=209764 T=105111 C=314875 [C={28:58:85} C^2={7:80:71:28:58:85}]
      C is 3 digits long, so:
      28 * 105**2 + 58 * 105 + 85 = 314875

      Like

  • Unknown's avatar

    Jim Randell 9:22 am on 16 June 2022 Permalink | Reply
    Tags:   

    Teaser 2499: [Supermarket milk] 

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

    Joe was talking to the supermarket manager about shelf stocking, and she mentioned a recent exercise with milk. Each morning, they added sufficient new stock to ensure there were 1,200 litres on the shelves. They found that 56% of the new stock was sold on the first day, half that amount the next day and half that new amount the day after. Any remaining stock was regarded as out of date and removed next morning before restocking. Hence, they calculated the number of litres to be added each morning.

    What is that number?

    This puzzle was originally published with no title.

    [teaser2499]

     
    • Jim Randell's avatar

      Jim Randell 9:23 am on 16 June 2022 Permalink | Reply

      I assumed that the numbers are fixed. So the same quantity of fresh milk is added each day, and the same quantities sold each day.

      If x litres of fresh milk are stocked at the beginning of the day (to bring the total stocks to 1200 litres).

      We can track what happens to this batch milk on the following days.

      At the start of the next day 0.56x has been sold, so there is only 0.44x of this batch of milk remaining (and a new batch of x litres of fresh milk are added).

      At the start of the next day an additional 0.28x has been sold, so there is only 0.16x of the original batch remaining (along with 0.44x litres of the previous days new batch, and x litres of fresh milk).

      At the start of the next day an additional 0.14x has been sold, so there is only 0.02x of the original batch remaining, but this is removed as it is too old.

      So the 1200 litres of milk on the shelves consists of:

      x litres of fresh milk
      0.44x litres of 1-day old milk
      0.16x litres of 2-day old milk

      Hence:

      x + 0.44x + 0.16x = 1200
      1.6x = 1200
      x = 750

      Solution: Each morning 750 litres of fresh milk are added to the shelves.

      So the milk available at the start of the day consists of:

      750 litres of fresh milk (420 litres are sold during the day, leaving …)
      330 litres of 1-day old milk (210 litres are sold during the day, leaving …)
      120 litres of 2-day old milk (105 litres are sold during the day, leaving 15 litres to dispose of)
      1200 litres in total (735 litres sold)

      Like

    • Hugh+Casement's avatar

      Hugh+Casement 8:55 am on 17 June 2022 Permalink | Reply

      That’s some supermarket! How ever many metres of shelf space do 1200 litres take up?
      I suggest that a more realistic puzzle would have all the numbers a fifteenth as much.

      Like

  • Unknown's avatar

    Jim Randell 12:30 pm on 14 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 718: An eccentric race 

    From The Sunday Times, 20th April 1975 [link]

    After the tea-party Alice persuaded the Mad Hatter, the March Hare and the Dormouse to run with her round a nearby circular track, promising that they should all four win the race by reaching the winning-post at the same moment — so long as they did not vary their speeds!

    Round the track were twelve posts equally-spaced a whole number of feet apart, No. 12 being at the start, which was also the finishing-post. At each post one of the Flamingoes was stationed as umpire. We will call them F1, F2, …, F12. F12 acted as starter. The umpires reported as follows:

    (1) All four runners maintained their own constant speeds.

    (2) F2 noted that Hatter passed Dormouse at his post exactly 30 seconds after the start.

    (3) F3 reported that Hare passed Hatter at his post exactly 45 seconds after the start.

    (4) F8 said that Hare passed Alice at his post, at which time Alice was passing his post for the third time and Hare for the sixth time.

    (5) The umpires reported no more overtakings, although obviously there were others.

    The speeds of the four runners, in feet per second, were whole numbers between 5 and 20.

    How many laps had they all completed when they all won. And how many seconds did the race last?

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

    [teaser718]

     
    • Jim Randell's avatar

      Jim Randell 12:33 pm on 14 June 2022 Permalink | Reply

      I am assuming they all set off from the start at the same time (although this is not explicitly mentioned).

      This Python program runs in 55ms. (Internal run time is 326µs).

      Run: [ @replit ]

      from enigma import (irange, inf, div, divisors, fdiv, printf)
      
      # distance units for passing post <n> for the <k>th time
      dist = lambda n, k: n + 12 * (k - 1)
      
      # check velocity <v> passed post <n> at time <t>
      def check(v, n, t, d):
        x = div(v * t, d)
        return (x is not None) and (x % 12 == n)
      
      # calculate the laps and time for a race with velocities <vs>
      def race(vs, lap):
        # find the slowest speed
        s = min(vs)
        # consider total number of laps for the slowest
        for n in irange(1, inf):
          # calculate number of laps for the others
          laps = list(div(v * n, s) for v in vs)
          if None not in laps:
            return (laps, fdiv(n * lap, s))
      
      # choose a speed for the Hare (M)
      for M in irange(7, 20):
      
        # Alice (A) passed post 8 for the 3rd time, when the Hare passed it
        # for the 6th time
        A = div(M * dist(8, 3), dist(8, 6))
        if A is None: continue
      
        # choose speed for D
        for D in irange(5, M - 2):
          # Dormouse passes post 2 at exactly 30s
          # consider possible values for d
          for d in divisors(30 * D):
            if not check(D, 2, 30, d): continue
      
            # choose speed for Hatter (H)
            for H in irange(D + 1, M - 1):
      
              # Hatter passes post 2 at exactly 30s
              if not check(H, 2, 30, d): continue
      
              # Hare and Hatter pass post 3 at exactly 45s
              if not (check(H, 3, 45, d) and check(M, 3, 45, d)): continue
      
              # output solution
              ((lM, lA, lH, lD), t) = race([M, A, H, D], 12 * d)
              printf("M={lM} A={lA} H={lH} D={lD}; d={d} lap={lap} -> t={t:g}", lap=12 * d)
      

      Solution: At the end of the race the number of laps was: Alice = 8, Mad Hatter = 13, March Hare = 17, Dormouse = 7. The race lasted 180 seconds.

      The distance between posts was 15ft, so one lap was 180ft.

      The velocity of each participant, expressed in feet per second, is the same as the number of laps run during the race.

      Like

      • Frits's avatar

        Frits 10:57 pm on 15 June 2022 Permalink | Reply

        check(M, 3, 45, d) can already be done before the loop over H.

        Another idea is to choose H before D and then d must be in the intersection of the divisors of (30 * H) and (45 * H).

        Like

  • Unknown's avatar

    Jim Randell 10:01 am on 12 June 2022 Permalink | Reply
    Tags: by: R. Fisher   

    Brain-Teaser 51: In defiance of insomnia 

    From The Sunday Times, 11th March 1962 [link]

    Mr Robinson is an elderly gentleman who seeks to combat insomnia by juggling with the number of days he has lived. On April 25 last he noted that this was the product of three numbers each consisting of ten times a single figure less one. The following night he found to his surprise that the total of his days was the perfect cube of the product of the three figures of the previous day.

    How many days had he lived on April 26?

    [teaser51]

     
    • Jim Randell's avatar

      Jim Randell 10:02 am on 12 June 2022 Permalink | Reply

      This one is easy enough to tackle by brute force in Python.

      The following Python program runs in 62ms. (Internal runtime is 198µs).

      Run: [ @replit ]

      from enigma import (irange, subsets, multiply, printf)
      
      # consider the three digits
      for ds in subsets(irange(1, 9), size=3):
        # calculate the numbers of days
        n1 = multiply(10 * d - 1 for d in ds)
        n2 = pow(multiply(ds), 3)
        # are they consecutive?
        if n1 + 1 == n2:
          printf("{ds} -> {n1} {n2}")
      

      Solution: Mr Robinson had lived 27,000 days on 26th April.

      Which is almost 74 years. 27,000 days before 26th April 1961 is 24th May 1887. (So Mr Robinson and I have the same birthday).

      We have:

      26999 = 19 × 29 × 49
      27000 = (2 × 3 × 5)³


      We can reduce the number of candidate digit sets considered by noting:

      (10x − 1)(10y − 1)(10z − 1) + 1 = (xyz)³

      The LHS is divisible by 10, so one of the digits must be 5 and another must be even.

      Like

      • John Crabtree's avatar

        John Crabtree 3:40 pm on 14 June 2022 Permalink | Reply

        And (xyz)³ – 1 = ((xyz) – 1)((xyz)^2 + (xyz) + 1)
        With (xyz) being a multiple of 10, and the possible ages, the few possibilities are easily checked.

        Like

        • John Crabtree's avatar

          John Crabtree 5:21 am on 15 June 2022 Permalink | Reply

          In fact then two of the numbers must be 2 and 5, and then ((xyz)^2 + (xyz) + 1) = 19 * 49 = 931. And then (xyz) = 30, etc.

          Like

          • Frits's avatar

            Frits 10:29 am on 17 June 2022 Permalink | Reply

            @John, How do you proof one of the numbers is 2?

            If we assume the highest human age is 125 then more or less (xyz)^3 < 125*365.

            Using z=5 we get (xy)^3 < 365 so xy < 365^(1/3) so xy < 7.15.
            That leaves numbers 2 and 3 for x and y (if one of them must be even).

            Like

          • Frits's avatar

            Frits 10:43 am on 17 June 2022 Permalink | Reply

            Using the same logic for minimum age of 60 we get xy > 5.60.
            This leaves 2,3 and 1,6 for x,y.

            But 9 * 59 * 49 = 26019 so only options 2,3 remain for x,y.

            Like

    • Frits's avatar

      Frits 2:27 pm on 17 June 2022 Permalink | Reply

         
      # (10x - 1)(10y - 1)(10z - 1) + 1 = (x * y * z)^3 
      # The LHS is divisible by 10, so one of the digits must be 5 (say z)
      # and another must be even
      
      # (10 * x - 1) * (10 * y - 1) * 49 = cd999   (z = 5)
      # 4900 * x * y - 490 * (x + y) + 49 = cd999 
      # ==> 490 * (x + y) must end on 50 in order for RHS to end on 99
      # ==> x + y is either 5 or 15
      
      # if RHS ends on 9.. then ((9 * x * y) - (49 * (x + y))) must end on 9
      # x + y is either 5 or 15 ==> the product 9 * x * y must end on a 4
      
      # ==> - 490 * (x + y) + 49 must end on 401
      # ==> 490 * (x + y) must end on 450
      
      # x + y is 5 or 15 ==> x + y is 10 * a + 5 where a = 0 or 1
      # 490 * (x + y) = 4900 * a + 2450
      # a = 1 can be discarded as then 4900 * a + 2450 ends on 350
      # ==> x + y = 5. so either x, y (or y, x) is (1, 4) or (2, 3)
      # x, y (or y, x) equal to (1, 4) can be discarded as then 9 * x * y ends on 6
      # ==> x * y = 6 ==> x * y * 5 is 30
      # Mr Robinson had lived 30^3 = 27000 days on 26th April
      

      Like

  • Unknown's avatar

    Jim Randell 4:25 pm on 10 June 2022 Permalink | Reply
    Tags:   

    Teaser 3116: Poll positions 

    From The Sunday Times, 12th June 2022 [link] [link]

    In an election for golf-club president, voters ranked all four candidates, with no voters agreeing on the rankings. Three election methods were considered.

    Under first-past-the-post, since the first-preferences order was A, B, C, D, the president would have been A.

    Under Alternative Vote, since A had no majority of first preferences, D was eliminated, with his 2nd and 3rd preferences becoming 1st or 2nd preferences for others. There was still no majority of 1st preferences, and B was eliminated, with his 2nd preferences becoming 1st preferences for others. C now had a majority of 1st preferences, and would have been president.

    Under a Borda points system, candidates were given 4, 3, 2, or 1 points for each 1st, 2nd, 3rd or 4th preference respectively. D and C were equal on points, followed by B then A.

    How many Borda points did each candidate receive?

    [teaser3116]

     
    • Jim Randell's avatar

      Jim Randell 5:17 pm on 10 June 2022 Permalink | Reply

      There are only factorial(4) = 24 different ways to order the candidates, so that gives us an upper bound on the number of voters.

      This Python program finds the required points values, and the unique set of votes that leads to it.

      It isn’t particularly fast (although it is faster than my first program, which just tried all possible sets of votes). It runs in 251ms.

      Run: [ @replit ]

      from enigma import (group, subsets, join, unpack, irange, decompose, cproduct, map2str, printf)
      
      candidates = "ABCD"
      
      # consider possible voting patterns (first, second, third, fourth)
      # grouped by first choice
      patterns = group(subsets(candidates, size=len, select="P", fn=join), by=(lambda x: x[0]))
      
      # count the number of first choice votes
      def first(vs, ks):
        d = dict((k, 0) for k in ks)
        for v in vs:
          d[v[0]] += 1
        return sorted(d.items(), key=unpack(lambda p, n: n), reverse=1)
      
      # eliminate a candidate
      def eliminate(vs, x):
        return list(v.replace(x, '', 1) for v in vs)
      
      # calculate Borda points
      def borda(vs, ks):
        n = len(ks)
        d = dict((k, 0) for k in ks)
        for v in votes:
          for (i, x) in enumerate(v):
            d[x] += n - i
        return d
      
      # check the remaining puzzle constraints
      # return Borda points
      def check(N, votes):
      
        # first D is eliminated
        vs = eliminate(votes, 'D')
        # count the number of first choices
        ((p1, n1), (p2, n2), (p3, n3)) = first(vs, "ABC")
        #  there is no majority of first choices, and B is eliminated
        if 2 * n1 > N or not (n2 > n3 and p3 == 'B'): return
      
        # then B is eliminated
        vs = eliminate(vs, 'B')
        # count the number of first choices again
        ((p1, n1), (p2, n2)) = first(vs, "AC")
        if not (p1 == 'C' and 2 * n1 > N): return
      
        # count Borda points
        pts = borda(votes, candidates)
        # D and C are equal first, then B, then A
        if not (pts['D'] == pts['C'] > pts['B'] > pts['A']): return
      
        # return Borda points
        return pts
      
      # consider the number of voters [6, 24]
      for N in irange(6, 24):
        # consider the number of first choice votes (A does not have a majority)
        for (A, B, C, D) in decompose(N, 4, increasing=-1, sep=1, min_v=0, max_v=min(6, N // 2)):
          # choose the votes
          vs = (subsets(patterns[k], size=n) for (k, n) in zip("ABCD", (A, B, C, D)))
          for (As, Bs, Cs, Ds) in cproduct(vs):
            votes = As + Bs + Cs + Ds
            pts = check(N, votes)
            if pts:
              printf("points: {pts}", pts=map2str(pts, arr="=", sep=" ", enc=""))
              printf("-> {n} votes {votes}", n=len(votes), votes=join(votes, sep=" ", enc="[]"))
              printf()
      

      Solution: The Borda points are: A=33, B=35, C=36, D=36.

      There are 14 voters and the votes cast are:

      ABCD, ABDC, ACDB, ADBC, ADCB
      BCAD, BCDA, BDAC, BDCA
      CBDA, CDAB, CDBA
      DCAB, DCBA

      Using “first-past-the=post”, A wins with 5 votes, B has 4, C has 3, D has 2.

      Under “alternative vote” the first round is as given above. D is eliminated and his 2 votes are redistributed to give: A=5, B=4, C=5. Again there is no outright winner, so C’s 4 votes are redistributed to give: A=6, C=8, and C wins.

      Like

      • Frits's avatar

        Frits 10:58 am on 11 June 2022 Permalink | Reply

        @Jim,

        With a little analysis you can halve the run time by choosing a better range of number of voters than [6, 24].

        Like

        • Jim Randell's avatar

          Jim Randell 1:32 pm on 11 June 2022 Permalink | Reply

          I went with the smallest possible number of voters: D=0, C=1, B=2, A=3, gives a total of 6 voters.

          But in this scenario when D is eliminated there would be no votes to redistribute, so we can move to: D=1, C=2, B=3, A=4, to give a total of 10 voters.

          But when D’s votes are redistributed it is enough to knock B into last place, so C needs to gain at least 2 votes to leapfrog B.

          So we can increase minimum possible to: D=2, C=3, B=4, A=5, for a total of 14 voters.

          Incorporating this into my program brings the run time down to 384ms.

          And with a bit more analysis of the alternative vote system we can see that the number of voters is in {14, 15, 17, 18}.

          Like

          • Frits's avatar

            Frits 1:59 pm on 11 June 2022 Permalink | Reply

            @Jim,

            Yes, [14, 15, 17, 18] is what I had in mind.

            Like

          • Frits's avatar

            Frits 2:05 pm on 11 June 2022 Permalink | Reply

            My PuzzlingInPython program also early rejects N=18.

            Like

    • Hugh+Casement's avatar

      Hugh+Casement 9:48 am on 19 June 2022 Permalink | Reply

      I really don’t see the point of optimizing the run time for a program that is run only once.
      It takes far longer to write it!

      Like

      • Jim Randell's avatar

        Jim Randell 10:59 pm on 19 June 2022 Permalink | Reply

        If possible, I aim for a generic program that runs in under 1s, and if I can get the run time down to 100ms that’s even better (although I still like to keep it generic if possible).

        In this case my initial attempt ran in 44s, so I accepted I might have to tune it a little to improve on the run time. It wasn’t very much work, and the program posted above ran in less than 1s. I did do some more tweaking which got the run time down to 74ms, but I didn’t think the extra complication was worth posting.

        Like

        • Hugh+Casement's avatar

          Hugh+Casement 9:51 am on 20 June 2022 Permalink | Reply

          This is not process control, where milliseconds may well matter!

          If a program takes many minutes to run (while I go and do something else), I probably find it worth spending a bit of time to try and improve the method or find a different line of attack.
          But one has to keep a sense of proportion.

          Like

  • Unknown's avatar

    Jim Randell 10:12 am on 9 June 2022 Permalink | Reply
    Tags:   

    Teaser 2754: Family history 

    From The Sunday Times, 5th July 2015 [link] [link]

    Three of George’s relatives who were born in different years of the 20th century shared the same birthday. Writing these in the form D/M/Y with just two digits for the year, he tells Martha that: in one case D divided by M, when expressed as a percentage, is Y; in another M is the average of D and Y; in the remaining one D raised to the power M equals Y. George then told Martha that knowing the day, D, would not enable her to work out the three dates, but knowing any one of the three years would enable her to work out all three dates.

    What is the most recent of the three birth dates?

    [teaser2754]

     
    • Jim Randell's avatar

      Jim Randell 10:13 am on 9 June 2022 Permalink | Reply

      The years are all in the 20th Century (i.e. 1901 – 2000).

      This Python program runs in 57ms. (Internal runtime is 536µs).

      Run: [ @replit ]

      from enigma import (
        irange, product, div, seq_all_different as all_different,
        filter_unique, unpack, intersect, printf
      )
      
      # generate possible dates
      def generate():
        # choose values for D and M
        for (D, M) in product(irange(1, 31), irange(1, 12)):
          if D > 28 and M == 2: continue
          if D > 30 and M in {4, 6, 9, 11}: continue
      
          # calculate the 3 (truncated) years
          Ys = (div(100 * D, M), 2 * M - D, D ** M)
          if any(y is None or y < 0 or y > 99 for y in Ys): continue
          if not all_different(Ys): continue
          yield (D, M, Ys)
      
      # candidate solutions
      ss = generate()
      
      # knowing D does not let you work out the solution
      ss = filter_unique(ss, unpack(lambda D, M, Ys: D)).non_unique
      
      # but knowing _any_ of the years would let you work it out
      fn = lambda k: unpack(lambda D, M, Ys: Ys[k])
      ss = intersect(filter_unique(ss, fn(k)).unique for k in (0, 1, 2))
      
      # output solution
      for (D, M, Ys) in ss:
        (Y1, Y2, Y3) = Ys
        printf("D={D} M={M} -> Y1={Y1:02d} Y2={Y2:02d} Y3={Y3:02d}")
        Y = max((2000 if y == 0 else y + 1900) for y in Ys)
        printf("-> most recent (D/M/Y) = {D}/{M}/{Y}")
      

      Solution: The most recent of the birth dates is: 2/5/40 (i.e. 2nd May 1940).

      All birthdays are 2nd May, and the years are: 1908, 1932, 1940.

      So we have:

      2/5 = 40% → 1940
      5 is the mean of 2 and 8 → 1908
      2^5 = 32 → 1932

      At the time of publication the most recent birthdays would be: 107th, 83rd, 75th.


      There are 7 possible D/M pairs:

      D=1 M=2 → Ys=(50, 3, 1)
      D=1 M=4 → Ys=(25, 7, 1)
      D=1 M=5 → Ys=(20, 9, 1)
      D=1 M=10 → Ys=(10, 19, 1)
      D=2 M=4 → Ys=(50, 6, 16)
      D=2 M=5 → Ys=(40, 8, 32)
      D=3 M=4 → Ys=(75, 5, 81)

      The last of these is excluded as D is unique.

      The D=2 M=5 case is the only one which can be uniquely determined given any of the Y values.

      Like

  • Unknown's avatar

    Jim Randell 7:52 am on 7 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 713: Better and better 

    From The Sunday Times, 16th March 1975 [link]

    When the four modest gamblers — Al, Bill, Carl and Don — sat down for their usual game of poker, each of the four placed on the table, as their stake money for the evening, a different whole number of pence.

    After a number of hands, Al found that he had exactly doubled the number of pence with which he had started. it was his turn to provide the evening’s refreshments, and he bought the first round of drinks. After a few more hands, Al exactly doubled the number of pence he had had left after buying the first round and he then bought a second round. Thereafter he repeated the process, i.e. he exactly doubled his remaining pence and bought a third round.

    During the fourth session, Al took every penny his three opponents still had on the table and found that he had exactly doubled the number of pence he had had left after buying the third round. He then bought a fourth round — which took every penny he himself had!

    Each of the four rounds of drinks cost precisely the same whole number of pence.

    Carl began the game with a number of pence (between 50 and 100) which was exactly half the total number of pence which Bill and Don had between them at the start.

    How many pence did Al have when the game began?

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

    [teaser713]

     
    • Jim Randell's avatar

      Jim Randell 7:55 am on 7 June 2022 Permalink | Reply

      If the starting amounts are (A, B, C, D). Then at the end of the first session A now has 2A and buys a round (costing R), so starts the second session with (2A − R).

      He ends the second session with twice his starting amount, i.e. (4A − 2R), and buys another round. So starts the third session with (4A − 3R).

      He ends the third session with twice his starting amount, i.e. (8A − 6R), and buys another round. So starts the fourth session with (8A − 7R).

      He ends the fourth session with twice his starting amount, i.e. (16A − 14R), and buys another round, leaving him with no money.

      Hence:

      16A − 15R = 0
      R = (16/15)A

      Also the purchasing of the 4 rounds used up all of the money:

      4R = A + B + C + D
      (4(16/15) − 1)A = B + C + D
      A = (15/49)(B + C + D)

      Now: C = (B + D) / 2, so:

      B + D = 2C
      A = (15/49)(3C)
      A = (45/49)C

      And:

      R = (16/15)A
      R = (48/49)C

      48 has no factors of 7, so C must have (at least) 2 factors of 7. And as C is in [50, 100] we must have:

      C = 98
      B + D = 196
      A = 90
      R = 96

      All that remains is to ensure B and D can be assigned values such that each of the four starts with a different stake.

      If we keep keep the stakes below 100p we can assign B and D to 97p and 99p (in some order). The four starting amounts are then (90p, 97p, 98p, 99p).

      Solution: Al started the game with 90p.

      So Al’s progress is:

      1: 90p → 180p; spends 96p
      2: 84p → 168p; spends 96p
      3: 72p → 144p; spends 96p
      4: 48p → 96p; spends 96p

      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