Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:40 am on 30 July 2024 Permalink | Reply
    Tags:   

    Brain-Teaser 542: Finger trouble 

    From The Sunday Times, 14th November 1971 [link]

    The Wabu tribesmen, though invariably truthful, are inveterate thieves, and this despite a tribal law which requires any man convicted of theft to have one finger amputated. Few of them now have a full complement and, since they count on their fingers, their answers to numerical questions are confusing to the outsider. (For instance, an 8-fingered man gives his answers to base 8, so that, for example, our figure 100 is for him 144).

    Despite these handicaps, they are keen cricketers, and I recently watched their First Eleven. The Chief told me: “Only little Dojo has 10 fingers; the worst-equipped are Abo, Bunto and Coco with 4 apiece”. (I need hardly add that the Wabu would never elect a Chief who had been convicted of theft).

    Later I asked some members of the team how many fingers (thumbs are, of course, included) the Eleven totalled. Epo said “242”, and Foto agreed; Gobo said “132”, and Hingo agreed. Kinko added: “I have more fingers than Iffo”.

    How many fingers each have Epo, Hingo, Iffo and Jabo (the Wicket-keeper)?

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

    [teaser542]

     
    • Jim Randell's avatar

      Jim Randell 8:41 am on 30 July 2024 Permalink | Reply

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

      The following run file executes in 83ms. (Internal runtime of the generate code is 1.9ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --base=11  # values are between 4 and 10
      --distinct=""
      
      # we are given some values
      --assign="A,4"
      --assign="B,4"
      --assign="C,4"
      --assign="D,10"
      # and the others are between 5 and 9
      --digits="5-9"
      # additional constraints
      "E = F"
      "G = H"
      "K > I"
      
      # total number of fingers
      --macro="@total = (A + B + C + D + E + F + G + H + I + J + K)"
      
      # E (and F) says "242"
      "int2base(@total, base=E) == '242'"
      
      # G (and H) says "132"
      "int2base(@total, base=G) == '132'"
      
      --answer="(E, H, I, J)"
      --template=""
      

      Solution: Epo has 5 fingers; Hingo has 7 fingers; Iffo has 8 fingers; Jabo has 9 fingers.

      The number of fingers for each member of the team are:

      A = 4
      B = 4
      C = 4
      D = 10
      E = 5
      F = 5
      G = 7
      H = 7
      I = 8
      J = 9
      K = 9
      total = 72

      And the total given in the appropriate bases:

      A, B, C → 1020 [base 4]
      D → 72 [base 10]
      E, F → 242 [base 5]
      G, H → 132 [base 7]
      I → 110 [base 8]
      J, K → 80 [base 9]

      Like

      • Ruud's avatar

        Ruud 4:14 pm on 30 July 2024 Permalink | Reply

        It is really simple to solve this one by hand.
        But, here’s my as brute as possible code:

        import itertools
        
        a = b = c = 4
        d = 10
        for e in range(5, 11):
            sum_e = int("242", e)
        
            for g in range(4, 11):
                sum_g = int("132", g)
                if sum_e == sum_g:
                    f = e
                    h = g
                    ijk = sum_e - (a + b + c + d + e + f + g + h)
                    for i, j, k in itertools.product(range(1, 10), repeat=3):
                        if i + j + k == ijk and k > i:
                            print(f"Epo={e} Hingo={h} Iffo={i} Jabo={j}")
        

        Like

    • Ruud's avatar

      Ruud 7:30 pm on 30 July 2024 Permalink | Reply

      Even more brute force:

      import itertools
      
      a = b = c = 4
      d = 10
      for e, f, g, h, i, j, k in itertools.product(range(5, 10), repeat=7):
          if e == f and g == h and (total := int("242", e)) == int("132", g) and k > i and (a + b + c + d + e + f + g + h + i + j + k) == total:
              print(f"Epo={e} Hingo={h} Iffo={i} Jabo={j}")
      

      Like

    • Frits's avatar

      Frits 1:44 pm on 31 July 2024 Permalink | Reply

      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # G*G + 3*G - 2*(E*E + 2*E) = 0
          "is_square(1 + G * (G + 3) // 2) - 1 = E",
          
          "K > I",
          "(G * (G + 3) + 2) -  (A + B + C + D + E + E + G + G + I + J) = K",
        ],
        answer="(E, G, I, J)",
        distinct="",
        s2d=dict(A=4, B=4, C=4, D=10),
        digits=range(5, 10),
        verbose=0,    # use 256 to see the generated code
      )
      
      names= "Epo Hingo Iffo Jabo".split()
      
      # print answers
      for ans in p.answers():
        print(f"{', '.join(f'{x}: {str(y)}'for x, y in zip(names, ans))}")     
      

      Like

  • Unknown's avatar

    Jim Randell 6:58 am on 28 July 2024 Permalink | Reply
    Tags:   

    Teaser 3227: Chess club cupboard 

    From The Sunday Times, 28th July 2024 [link] [link]

    When installing the Chess Club cupboard in its new room, the members carelessly jammed it on the low ceiling and back wall, as shown from the side (not to scale).

    Measurements were duly taken and the following were noted. The floor and ceiling were level and the back wall was vertical. The cupboard was a rectangular cuboid. The ceiling height was just 2cm greater than the cupboard height. Also, the depth of the cupboard from front to back was 148 cm less than the cupboard height. Finally the point on the ceiling where the cupboard jammed was a distance from the back wall that was 125 cm less than the cupboard height.

    What is the cupboard height?

    [teaser3227]

     
    • Jim Randell's avatar

      Jim Randell 7:11 am on 28 July 2024 Permalink | Reply

      Originally I assumed some of the dimensions were whole numbers of centimetres, which allows you to find the answer, but we do not need to make this assumption.

      Here is a solution using the [[ Polynomial ]] class from the enigma.py library to generate a polynomial equation, and we can then look for positive real roots of the equation to determine the necessary dimensions.

      (Also, today is the 15th anniversary of the start of the enigma.py library).

      It runs in 73ms. (Internal runtime is 4.2ms).

      Run: [ @replit ]

      from enigma import (Polynomial, sq, printf)
      
      # let x = cupboard depth
      fx = Polynomial('x')
      
      # y = cupboard height
      fy = fx + 148
      
      # h = room height
      fh = fy + 2
      
      # d = distance top jam to top corner
      fd = fy - 125
      
      # if:
      #   a = height of upper triangle
      #   b = height of lower triangle
      # we have:
      #   h = a + b
      # and:
      #   a^2 = y^2 - d^2
      #   b = x.d/y
      #   a = h - b = (y.h - x.d)/y
      #   a^2 = (y.h - x.d)^2 / y^2 = y^2 - d^2
      #   y^2(y^2 - d^2) = (y.h - x.d)^2  # for x > 0
      f = (sq(fy) * (sq(fy) - sq(fd)) - sq(fy * fh - fx * fd)).scale()
      printf("[f(x) = {f}]", f=f.print())
      
      # look for positive real roots of f(x) = 0
      for x in f.roots(domain='F', include='+'):
        # output solution
        printf("cupboard = {x:g} x {y:g}; room height = {h:g}", y=fy(x), h=fh(x))
      

      Solution: The cupboard was 185 cm high.

      So the cupboard has dimensions: 37 cm × 185 cm, and the room is 187 cm high.

      The polynomial to determine the depth of the cupboard x is:

      f(x) = x^3 + 79x^2 − 1628x − 98568
      f(x) = (x − 37)(x^2 + 116x + 2664)

      The linear component has a positive root at:

      x = 37

      which provides the required answer for the depth of the cupboard.

      The quadratic component has two negative roots at:

      x = 10√7 − 58 ≈ −31.542
      x = −10√7 − 58 ≈ −84.458

      but these do not provide viable answers to the puzzle.

      Like

      • Jim Randell's avatar

        Jim Randell 9:09 am on 29 July 2024 Permalink | Reply

        Or the polynomial can be solved numerically, to give an approximate answer.

        And this is slightly faster. The following Python program has an internal runtime of 324µs).

        Run: [ @replit ]

        from enigma import (Polynomial, sq, find_zero, printf)
        
        # let x = cupboard depth
        # y = cupboard height
        # h = room height
        # d = distance top jam to top corner
        fx = Polynomial('x')
        fy = fx + 148
        fh = fy + 2
        fd = fy - 125
        
        # construct polynomial to solve
        f = (sq(fy) * (sq(fy) - sq(fd)) - sq(fy * fh - fx * fd)).scale()
        printf("[f(x) = {f}]", f=f.print())
        
        # find a solution numerically
        x = find_zero(f, 1, 100).v
        
        # output solution
        printf("cupboard = {x:.2f} x {y:.2f}; room height = {h:.2f}", y=fy(x), h=fh(x))
        

        Like

    • GeoffR's avatar

      GeoffR 12:21 pm on 28 July 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..250:h; var 1..250:y1; var 1..100:b;
      
      % h = cupboard height
      % y1 = depth below ceiling of top cupboard jam point
      % b = distance of bottom jam point from vertical wall
      
      % Two triangles to solve by Pythagorus
      constraint h * h == (h - 125) * (h - 125) + (y1 * y1);
      
      constraint (h - 148 ) * (h - 148) == (b * b) + (h + 2 - y1) * (h + 2 - y1);
      
      solve satisfy;
      
      output ["Cupboard height = " ++ show(h) ++ "cm."];
      
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 12:52 pm on 28 July 2024 Permalink | Reply

      I don’t suppose the setter intended it, but I found a second solution with a cupboard height of more than 400 cm higher than the presumed solution!

      Like

      • Jim Randell's avatar

        Jim Randell 2:32 pm on 28 July 2024 Permalink | Reply

        @GeoffR: Originally I thought there were further solutions involving cupboards with non-integer dimensions, or very large cupboards (and rooms). But now I have done the analysis I found that the depth of the cupboard is defined by a cubic equation that has one positive real root (which gives the required answer) and two negative real roots, so I don’t think there are any further solutions, even if the size of the cupboard/room are not restricted.

        Like

        • Frits's avatar

          Frits 2:54 pm on 28 July 2024 Permalink | Reply

          @Jim,

          The equations I noted down for this puzzle also had a solution 10 * (9 + sqrt(7)) but that was less than 148 cm.

          Like

          • Jim Randell's avatar

            Jim Randell 3:23 pm on 28 July 2024 Permalink | Reply

            @Frits: Presumably you were working in terms of the height of the cupboard. I was working in terms of the depth, so there was only one positive root.

            Like

        • GeoffR's avatar

          GeoffR 4:11 pm on 28 July 2024 Permalink | Reply

          @Jim: My original program gave the same outputs as your original solution.
          I increased the upper bounds of the variables to show the second solution I obtained with the second program below. Seems reasonable to use integers . Program is slow with the extra solution, but does it look OK?

          % A Solution in MiniZinc
          include "globals.mzn";
          
          var 1..1250:h; var 1..1250:y1; var 1..1000:b;
          
          % h = cupboard height, rht = room height (for output)
          % y1 = depth below ceiling of top cupboard jam point
          % b = distance of bottom jam point from vertical wall
          
          % Two triangles to solve by Pythagorus
          constraint h * h == (h - 125) * (h - 125) + (y1 * y1);
          
          constraint (h - 148 ) * (h - 148) == (b * b) + (h + 2 - y1) * (h + 2 - y1);
          
          solve satisfy;
          
          output ["Cupboard height = " ++ show(h) ++ "cm." ++ "\n" ++
          "[h, w, rht, y1, b ] = " ++ show( [h, h-148, h+2, y1, b ]  )];
          

          Like

          • Jim Randell's avatar

            Jim Randell 6:03 pm on 28 July 2024 Permalink | Reply

            @GeoffR: My very first program did not include a check that the upper and lower triangles were similar, which lead me to believe that there potentially multiple solutions without further constraints. But this allows cupboards that have a non-rectangular cross-section.

            However my subsequent analysis gives a single solution without further constraints. So we don’t need to limit the size of cupboard/room, and we don’t require measurements to have integer values.

            Liked by 1 person

    • Ruud's avatar

      Ruud 6:16 pm on 28 July 2024 Permalink | Reply

      I worked out by hand that the problem can be defined as finding the roots of the expression
      math.sqrt(250 *c -125*125)+((c-148)*(c-125)/c) – (c+2)
      I just put that in Wolfram Alpha as
      find roots of sqrt(250 *c -125*125)+((c-148)*(c-125)/c) – (c+2)
      to find the real solution (not revealed) and 10 (9 + sqrt(7)). The latter does not work here.

      Like

      • Frits's avatar

        Frits 6:55 pm on 28 July 2024 Permalink | Reply

        I did something similar with the same results.

        Adding the following to your code prevent multiple solutions:

          
        % same angles
        constraint h * b = (h - 148) * y1;
        

        Like

  • Unknown's avatar

    Jim Randell 10:49 am on 25 July 2024 Permalink | Reply
    Tags:   

    Brain-Teaser 416: Sharing sweets 

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

    The Binks family had a birthday party recently for Ann, the youngest child. A tin of toffees was shared among the seven youngest members of the family, whose ages were all different and less than 20.

    It was decided that the younger children should have more than the older; so Mr Binks suggested that each should receive the number obtained by dividing the total number of toffees by his or her age. This was done and it so happened that all the divisions worked out exactly and no toffees were left over.

    Mary received 18 sweets.

    How much older was she than Ann?

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

    [teaser416]

     
    • Jim Randell's avatar

      Jim Randell 10:51 am on 25 July 2024 Permalink | Reply

      Labelling the children in age order A, …, G, so A is Ann, and the total number of toffees is T, we have:

      T = T/A + T/B + T/C + T/D + T/E + T/F + T/G

      1 = 1/A + 1/B + 1/C + 1/D + 1/E + 1/F + 1/G

      So we need to find 7 different reciprocals (with denominators less than 20) that sum to 1. And we can use the [[ reciprocals() ]] function from the enigma.py library to do that (originally written for Enigma 348).

      We can then assign one of the values T/B, …, T/G to 18 (for Mary), and check that all the remaining T/X values give a whole number.

      The following Python program runs in 66ms. (Internal runtime is 753µs).

      Run: [ @replit ]

      from enigma import (reciprocals, printf)
      
      # find 7 different reciprocals that sum to 1
      for rs in reciprocals(7, 1, 1, M=19, g=1):
        # Ann is the youngest
        A = rs.pop(0)
        # Mary received 18 sweets
        for M in rs:
          T = M * 18
          if not all(T % r == 0 for r in rs): continue
          # output solution
          printf("A={A} M={M} [T={T} rs={rs}]", rs=[A] + rs)
      

      Solution: Mary (10) is 7 years older than Ann (3).

      There is only one set of 7 reciprocals that works:

      1 = 1/3 + 1/4 + 1/9 + 1/10 + 1/12 + 1/15 + 1/18

      There are 180 toffees, and the ages are:

      3 → 60 toffees (Ann)
      4 → 45 toffees
      9 → 20 toffees
      10 → 18 toffees (Mary)
      12 → 15 toffees
      15 → 12 toffees
      18 → 10 toffees
      total = 180 toffees

      Like

    • GeoffR's avatar

      GeoffR 1:32 pm on 25 July 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..19:A; var 1..19:B; var 1..19:C; var 1..19:D;
      var 1..19:E; var 1..19:F; var 1..19:G;
      
      constraint all_different ([A, B, C, D, E, F, G]);
      % Order childrens ages
      constraint A < B /\ B < C /\ C < D /\ D < E /\ E < F /\ F < G;
      
      var 28..200:T;  % total number of toffees
      
      % Distribute toffees to seven children, with none left over
      constraint T  mod A == 0 /\ T  mod B == 0 /\ T  mod C == 0 /\ T  mod D == 0 
      /\ T  mod E == 0 /\ T  mod F == 0 /\ T  mod G == 0;
      
      constraint T == T div A  + T div B + T div C + T div D 
      + T div E + T div F + T div G;
      
      solve satisfy;
      
      output ["Childrens ages = " ++ show ( [A,B,C,D,E,F,G] )
      ++ "\n" ++ "Number of toffees = " ++ show(T) ];
      
      % Childrens ages = [3, 4, 9, 10, 12, 15, 18]
      % Number of toffees = 180
      % ----------
      % ==========
      

      As Mary received 18 sweets, she was 10 years old, as 10 * 18 = 180.
      As the youngest was Ann, she was 3 years old, 7 years younger than Mary.

      Like

    • Ruud's avatar

      Ruud 6:40 am on 26 July 2024 Permalink | Reply

      [removed]

      Like

      • Jim Randell's avatar

        Jim Randell 9:21 am on 26 July 2024 Permalink | Reply

        @Ruud: Can you explain the reasoning behind your code? I tried changing the 18 to 24 to solve a variation of the puzzle, but it did not give a correct answer. (I also added the missing imports to your program).

        Like

        • ruudvanderham's avatar

          ruudvanderham 12:02 pm on 26 July 2024 Permalink | Reply

          @Jim
          My solution was incorrect. Could you remove it?
          Here’s my revised one, which is not much different from yours:

          import itertools
          import math
          
          number_of_toffee_mary = 18
          for ages in itertools.combinations(range(1, 20), 7):
              if math.isclose(sum(1 / age for age in ages), 1):
                  for age in ages:
                      if all(divmod(age * number_of_toffee_mary, try_age)[1] == 0 for try_age in ages):
                          print(f"{ages=}. Number of toffees={number_of_toffee_mary*age}. Mary is {age-ages[0]} years older than Ann")

          Like

    • Frits's avatar

      Frits 4:59 pm on 27 July 2024 Permalink | Reply

      from itertools import combinations
      
      M = 19
      N = 7
      
      # return divisors of <n> between 2 and <m>
      def divs(n, m=M):
        lst = [d for d in range(2, int(n**.5) + 1) if n % d == 0 and d <= m]
        return lst + [d for x in lst[::-1] if x * x != n and (d := n // x) <= m]
      
      # reciprocal sum
      def rs(s): 
        i, t = 0, 0.0
        ln = len(s)
        while t < 1 and i < ln:
          t += 1 / s[i]
          i += 1
        return i == ln and round(t, 5) == 1
      
      # Mary received 18 sweets
      for m in range(2, M + 1):
        # pick <N> divisors
        for c in combinations(divs(18 * m), N):
          # reciprocal sum must be 1
          if rs(c) == 1: 
            print(f"answer: {c}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:44 am on 23 July 2024 Permalink | Reply
    Tags:   

    Teaser 2576: Latin cube 

    From The Sunday Times, 5th February 2012 [link] [link]

    I have a cube and on each face of it there is a capital letter. I look at one face from the front, hold the top and bottom faces horizontal, and rotate the cube. In this way I can correctly read a four-letter Roman numeral. Secondly, I hold the cube with the original front face underneath and the original top face at the front, and I rotate the cube keeping the new top and bottom faces horizontal. In this way I correctly see another four-letter Roman numeral. Thirdly, I return the cube to its original position and rotate it with the two sides vertical to give another correct four-letter Roman numeral. The sum of the second and third numbers is less than a hundred.

    What was the third four-letter Roman numeral seen?

    [teaser2576]

     
    • Jim Randell's avatar

      Jim Randell 9:45 am on 23 July 2024 Permalink | Reply

      Some clarification of the puzzle text is required to find a single answer (and I assume the puzzle is meant to have a unique answer):

      (1) Although the letters used in Roman numerals (I, V, X, L, C, D, M) are all distinguishable whatever the orientation, the puzzle requires that when letters are read they are in a “sensible” orientation. So, I and X can be read correctly upside down. However this is not sufficient to solve the puzzle, we also require that X can be read correctly in any orientation (which may not be the case if a serif font is used, as is often the case with Roman numerals).

      (2) The mechanism to read the numbers is that the front face is read, the cube is then rotated through 90° about a major axis to change the front face, which is then read, and the same move is repeated until the four faces in a band around the cube have been read in order (and a final move would bring the cube back to the starting position).

      (3) The numbers read should be valid Roman numerals in the “usual” form. (I used the [[ int2roman() ]] and [[ is_roman() ]] functions in the enigma.py library to ensure this). I didn’t allow “IIII” to represent 4 (although if you do it doesn’t change anything).

      (4) Each of the three numbers read is different. (Possibly this is meant to be indicated by the use of “another”).


      We are not told what direction the cube is rotated, so this Python program considers rotations in each direction for the 3 numbers. Although note that for some numbers (e.g. VIII (= 8), where the second and fourth letters are the same), the direction of the rotation doesn’t matter.

      I used the [[ Cube() ]] class (originally from Teaser 2835) to keep track of the rotation of the cube (and orientation of the faces).

      This Python program considers possible values for the second and third numbers (which cover bands of the cube that include all faces, so leave the cube fully specified), and then reads the first number and looks for cases where this gives a valid Roman numeral.

      It runs in 78ms. (Internal runtime is 11ms).

      Run: [ @replit ]

      from enigma import (irange, int2roman, is_roman, join, seq2str, printf)
      from cube import (Cube, U, D, L, R, F, B, face_label as f2l)
      
      # valid orientations for roman digits
      valid = dict((k, {0}) for k in "IVXLCDM")
      valid['I'] = {0, 2}  # I can be upside down
      valid['X'] = {0, 1, 2, 3}  # X can be in any orientation
      
      # collect 4-letter roman numerals less than 100
      n2r = dict()
      for n in irange(1, 99):
        r = int2roman(n)
        if len(r) == 4:
          n2r[n] = r
      
      # read values from the front of cube <c>, and apply each of the specified moves <ms>
      # while match the values in <vs> (can be '?')
      # return (<updated cube>, <values read>)
      def read(c, ms, vs):
        rs = list()
        while True:
          # read the front face
          (v, vs0) = (c.faces[F], vs[0])
          # blank face?
          if v is None:
            v = vs0
            if v != '?':
              # fill out the new value
              c = c.update(faces=[(F, v)], orientations=[(F, 0)])
          else:
            # check it is in a valid orientation and a valid value
            if (c.orientations[F] not in valid[v]) or (vs0 != '?' and vs0 != v):
              return (None, None)
          rs.append(v)
          # any more moves?
          if not ms: break
          c = c.rotate(ms[0:1])
          ms = ms[1:]
          vs = vs[1:]
        return (c, join(rs))
      
      # start with an empty cube
      c0 = Cube(faces=[None] * 6)
      
      # choose a 4-letter roman for the second number
      for (n2, r2) in n2r.items():
        # choose the direction of rotation
        for d2 in [U, D]:
          # read the second number
          (c1, _) = read(c0.rotate([L]), [d2] * 3, r2)
          if c1 is None: continue
          # reset to original position
          c1 = c1.rotate([d2, R])
      
          # choose a 4-letter roman for the third number
          for (n3, r3) in n2r.items():
            # the first and second numbers sum to less than 100
            if n2 + n3 > 99: continue
            # choose a direction of rotation
            for d3 in [L, R]:
              # read the third number
              (c2, _) = read(c1, [d3] * 3, r3)
              if c2 is None: continue
              # all faces should now be filled out
              assert None not in c2.faces
              # reset to original position
              c2 = c2.rotate([d3])
      
              # choose a direction for the first number
              for d1 in [U, D]:
                # read the first number
                (_, r1) = read(c2, [d1] * 3, '????')
                if r1 is None: continue
                # is it a valid roman?
                n1 = is_roman(r1)
                if not n1: continue
                # check numbers are all different
                if len({n1, n2, n3}) != 3: continue
      
                # output solution
                printf("{r1} ({n1}) [{d1}]; {r2} ({n2}) [{d2}]; {r3} ({n3}) [{d3}]; {cube}",
                       d1=f2l[d1], d2=f2l[d2], d3=f2l[d3], cube=seq2str(c2.faces))
      

      Solution: The third Roman numeral seen was: LXII (= 62).

      The first two numbers were: LXIX (= 69) and XXIX (= 29), and the second and third sum to 91.

      Note that both of the first two numbers can be read using either direction of rotation (which is why my code finds 4 different ways to the answer), but the final number only works in one direction.

      The cube has faces (U, D, L, R, F, B) = (X, I, X, X, L, I).

      If we allow repeated numbers there is also a solution with the cube (U, D, L, R, F, B) = (X, I, X, X, X, I), which gives the numbers XXIX (= 29), XXIX (= 29), XXII (= 22).

      Like

    • Lise Andreasen's avatar

      Lise Andreasen 12:07 am on 24 July 2024 Permalink | Reply

      Especially (1) the orientation of X is interesting.

      Like

  • Unknown's avatar

    Jim Randell 12:59 pm on 21 July 2024 Permalink | Reply
    Tags:   

    Brain-Teaser 420: [The Island of Squares] 

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

    Here is the Island of Squares with its 22 provinces, and its capital in the middle of the central province.

    The Geographer Royal has been instructed to colour the provinces red, blue and yellow, in such a way that no two provinces with the same colouring have a common boundary-line.

    There is one further complication. Four main roads are planned each of which will go straight from the capital to one corner of the island crossing the corners of provinces on the way; one of these roads must never be in a blue province, and another must at no corner move from a yellow to a blue province or vice versa.

    What colours Geographer Royal will the use for provinces 1 and 2?

    This puzzle was originally published with no title.

    [teaser420]

     
    • Jim Randell's avatar

      Jim Randell 1:00 pm on 21 July 2024 Permalink | Reply

      This Python program colours the provinces so that no two adjacent regions share a colour, and then checks to see if there are two roads that meet the requirements.

      It runs in 68ms. (Internal runtime is 4.5ms).

      Run: [ @replit ]

      from enigma import (peek, update, join, union, map2str, printf)
      
      # adjacency matrix for provinces
      adj = {
        1: {2, 3, 4, 5},
        2: {1, 6, 7},
        3: {1, 4, 14},
        4: {1, 3, 5, 8, 15},
        5: {1, 4, 6, 9, 10},
        6: {2, 5, 7, 10, 13},
        7: {2, 6, 13, 18, 21, 22},
        8: {4, 9, 11, 16},
        9: {5, 8, 10, 11},
        10: {5, 6, 9, 12},
        11: {8, 9, 12, 17},
        12: {10, 11, 13, 17},
        13: {6, 7, 12, 18},
        14: {3, 15, 19},
        15: {4, 14, 16, 20},
        16: {8, 15, 17, 20},
        17: {11, 12, 16, 18, 21},
        18: {7, 13, 17, 21},
        19: {14, 20, 22},
        20: {15, 16, 19, 21, 22},
        21: {7, 17, 18, 20, 22},
        22: {7, 19, 20, 21},
      }
      
      # colour regions <rs> with colours <cs>
      def colour(rs, cs, m=dict()):
        # are we done?
        if not rs:
          yield m
        else:
          # choose an uncoloured region
          r = peek(rs)
          # choose a colour
          xs = cs.difference(m[x] for x in adj[r] if x in m)
          for x in xs:
            yield from colour(rs.difference([r]), cs, update(m, [(r, x)]))
      
      # regions for each road
      roads = dict(
        NW=[11, 16, 20, 19],
        NE=[11, 17, 21, 7],
        SE=[11, 10, 6, 2],
        SW=[11, 8, 4, 1],
      )
      
      # check roads
      def check(m):
        # map roads to colours
        r = dict((k, join(m[x] for x in v)) for (k, v) in roads.items())
        # find roads the do not pass through a blue province
        R1 = set(k for (k, v) in r.items() if not ('B' in v))
        if not R1: return
        # find roads that do not pass directly from blue to yellow (or vice versa)
        R2 = set(k for (k, v) in r.items() if not ('BY' in v or 'YB' in v))
        if not R2: return
        # check we can find one of each
        if len(union([R1, R2])) < 2: return
        return r
      
      # colour the regions
      for m in colour(set(adj.keys()), set("RBY")):
        # and check the roads
        r = check(m)
        if r:
          # output solution
          printf("{m}", m=map2str(m))
          printf("-> {r}", r=map2str(r))
          printf()
      

      Solution: Province 1 is red. Province 2 is yellow.

      The complete map is shown below:

      There is one province that can be either red or blue (On the extreme left of the diagram. I numbered it 14, and gave it both colours).

      The NE road passes through no blue, and the SW road does not pass directly from a blue to a yellow (or vice versa).

      We can see that there is no point trying to colour the central province blue (as that would make blue appear in every road), but I think the code is fast enough without this.

      Like

      • Ruud's avatar

        Ruud 9:27 am on 22 July 2024 Permalink | Reply

        Here’s my code:

        neighbours = {
            1: {2, 3, 4, 5},
            2: {1, 6, 7},
            3: {1, 4, 14},
            4: {1, 3, 5, 8, 15},
            5: {1, 4, 6, 9, 10},
            6: {2, 5, 7, 10, 13},
            7: {2, 6, 13, 18, 21, 22},
            8: {4, 9, 11, 16},
            9: {5, 8, 10, 11},
            10: {5, 9, 6, 12},
            11: {8, 9, 12, 17},
            12: {10, 11, 13, 17},
            13: {6, 12, 7, 18},
            14: {3, 15, 19},
            15: {4, 14, 16, 20},
            16: {8, 15, 17, 20},
            17: {11, 12, 16, 18, 21},
            18: {13, 17, 7, 21},
            19: {14, 20, 22},
            20: {15, 16, 19, 21, 22},
            21: {17, 18, 20, 7, 22},
            22: {19, 20, 21, 7},
        }
        roads = [(1, 4, 8, 11), (2, 6, 10, 11), (19, 20, 16, 11), (7, 21, 17, 11)]
        
        
        def solve(province, colors):
            for color in colors[province]:
                new_colors = colors.copy()
                new_colors[province] = color
                for neighbour in neighbours[province]:
                    new_colors[neighbour] = new_colors[neighbour].replace(color, "")
                if province == 22:
                    not_blue = set()
                    not_blue_yellow = set()
                    for road in roads:
                        road_colors = "".join(colors[province] for province in road)
                        if "b" not in road_colors:
                            not_blue.add(road)
                        if not ("yb" in road_colors or "by" in road_colors):
                            not_blue_yellow.add(road)
        
                    if len(not_blue) > 0 and len(not_blue_yellow) > 0 and len(not_blue | not_blue_yellow) >= 2:
                        print(new_colors[1], new_colors[2])
                else:
                    solve(province + 1, colors=new_colors)
        
        
        solve(province=1, colors={province: "ryb" for province in range(1, 23)})
        

        Like

  • Unknown's avatar

    Jim Randell 3:48 pm on 19 July 2024 Permalink | Reply
    Tags:   

    Teaser 3226: Prime choice 

    From The Sunday Times, 21st July 2024 [link] [link]

    I have written down three numbers, which together use each of the digits from 0 to 9 once.

    All the numbers are greater than one hundred and the two smaller numbers are prime. I have also written down the product of the three numbers. If I told you how many digits the product contains you wouldn’t be able to tell me the value of the product. However, if I also told you whether the product is odd or even then you would be able to work out all the three numbers.

    What, in increasing order, are my three numbers?

    [teaser3226]

     
    • Jim Randell's avatar

      Jim Randell 4:09 pm on 19 July 2024 Permalink | Reply

      The smaller two numbers must be 3-digit numbers, and so the remaining number must have 4 digits.

      I used the [[ SubstitutedExpression() ]] solver, from the enigma.py library, to generate the possible sets of 3 numbers.

      The sets are then grouped by (<length of product>, <parity of product>), and we look for keys that give a unique set of numbers, such that there is also at least one set grouped under the opposite parity (which means that there are multiple products with the same digit length).

      The following Python program runs in 194ms. (Internal runtime is 125ms).

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, multiply, ndigits, group, unpack, printf)
      
      # find possible sets of 3 numbers, their product and number of digits
      # return (<numbers>, <product>)
      def generate():
        # find two 3-digit primes, using different digits
        p = SubstitutedExpression(
          ["is_prime(ABC)", "is_prime(DEF)", "A < D"],
          answer="(ABC, DEF, GHIJ)",
        )
        for ns in p.answers(verbose=0):
          p = multiply(ns)
          yield (ns, p)
      
      # group candidates by (<digit length>, <parity>) of the product
      g = group(generate(), by=unpack(lambda ns, p: (ndigits(p), p % 2)))
      
      # look for candidate sets that are unique by (<digit length>, <parity>) of the product
      for ((k, m), vs) in g.items():
        if len(vs) != 1: continue
        # check there are solutions of the other parity
        if not g.get((k, 1 - m)): continue
        # output solution
        (ns, p) = vs[0]
        printf("{k} digits -> parity {m} -> {ns} = {p}")
      

      Solution: The three numbers are: 109, 367, 2485.

      The product is 99407455, which is 8-digits long. But there are 15 different products that are 8 digits long, so we cannot tell which set of numbers we started with, knowing only the number of digits in the product.

      However there is only one 8-digit product that is odd, and only one set of numbers that gives this product, and so provides the answer to the puzzle.

      Like

      • Jim Randell's avatar

        Jim Randell 9:37 am on 20 July 2024 Permalink | Reply

        In fact it enough to know the solution is unique by (<digit length>, <parity>) of the product. There is only a single candidate, so the additional check that the digit length alone is not sufficient to determine the product is not required (although a complete solution should verify it).

        But this leads to a shorter program:

        from enigma import (
          SubstitutedExpression, multiply, ndigits,
          filter_unique, unpack, printf
        )
        
        # find possible sets of 3 numbers, their product and number of digits
        # return (<numbers>, <product>)
        def generate():
          # find two 3-digit primes, using different digits
          p = SubstitutedExpression(
            ["is_prime(ABC)", "is_prime(DEF)", "A < D"],
            answer="(ABC, DEF, GHIJ)",
          )
          for ns in p.answers(verbose=0):
            p = multiply(ns)
            yield (ns, p)
        
        # answer is unique by (<digit length>, <parity>) of product
        f = unpack(lambda ns, p: (ndigits(p), p % 2))
        for (ns, p) in filter_unique(generate(), f).unique:
          # output solution
          printf("{k} digits -> parity {m} -> {ns} = {p}", k=ndigits(p), m=p % 2)
        

        Like

    • Frits's avatar

      Frits 5:52 pm on 19 July 2024 Permalink | Reply

      from itertools import permutations
      
      # prime numbers between 101 and 1000 with different digits
      P = [3, 5, 7]
      P += [x for x in range(11, 100, 2) if all(x % p for p in P)]
      P = [str(x) for x in range(101, 1000, 2) if all(x % p for p in P) and 
                                                  len(set(str(x))) == 3]                                           
      
      # product of two 3-digit numbers and a 4-digit number has length 8, 9 or 10
      impossible = {8: 0, 9: 0, 10: 0}
      d, dgts = dict(), set("0123456789")
      
      # choose two prime numbers
      for i, p1 in enumerate(P[:-1]):
        for p2 in P[i + 1:]:
          # different digits
          if len(s12 := set(p1 + p2)) != 6: continue
          p1_x_p2 = int(p1) * int(p2)
          
          # break if for high products we can never work out all the three numbers
          if impossible[9] and impossible[10] and p1_x_p2 > 99999: break
      
          # consider all variants of the third number
          for p in permutations(dgts - s12):
            if (n3 := int(''.join(p)) ) < 1000: continue
            # check if we already have enough entries for this (length, parity)
            if impossible[ln := len(str(prod := p1_x_p2 * n3))]: continue
            # store length/parity
            d[(ln, par)] = d.get((ln, par := prod % 2), []) + [(p1, p2, str(n3))]
            # if a length for both parities already has 2 entries or more
            # then remember this for subsequent processing
            if all((ln, par) in d and len(d[(ln, par)]) > 1 for par in [0, 1]):
              impossible[ln] = 1
      
      # check all possible solutions    
      for (ln, par), vs in d.items():
        # no uniqueness?
        if impossible[ln]: continue
        # can we work out all the three numbers?
        if len(vs) == 1 and (ln, 1 - par) in d:
          print("answer:", ', '.join(vs[0]))
      

      Like

    • GeoffR's avatar

      GeoffR 12:16 pm on 20 July 2024 Permalink | Reply

      Trial testing showed that there were 8, 9 or 10 digits in the product of the three numbers.

      
      from itertools import permutations
      from enigma import is_prime
      
      from collections import defaultdict
      dlen = defaultdict(list)
      key = 0
      
      for p1 in permutations('1234567890'):
          A, B, C, D, E, F, G, H, I, J = p1
          if '0' in (A, D, G):continue
          ABC = int(A + B + C)
          if not is_prime(ABC):continue
          DEF = int(D + E + F )
          if D < A:continue
          if not is_prime(DEF):continue
          GHIJ = int(G + H + I + J)
          # product of two  primes and a 4-digit number
          prod = ABC * DEF * GHIJ
      
          # Classify products for length and parity
          # Assume the products are 8, 9 or 10 digit in length
          if len(str(prod)) == 8 and prod % 2 == 0: key = 1
          if len(str(prod)) == 8 and prod % 2 == 1: key = 2
          if len(str(prod)) == 9 and prod % 2 == 0: key = 3
          if len(str(prod)) == 9 and prod % 2 == 1: key = 4
          if len(str(prod)) == 10 and prod % 2 == 0: key = 5
          if len(str(prod)) == 10 and prod % 2 == 1: key = 6
      
          # update dictionary
          dlen[key] += [ (prod, (ABC, DEF, GHIJ))]
      
      for k, v in dlen.items():
          # Answer is a single entry in dictionary
          if len(v) == 1:
              print('Ans = ', k, v)
          # find number of products in each group
          if k == 1: print(k, len(v))
          if k == 2: print(k, len(v))
          if k == 3: print(k, len(v))
          if k == 4: print(k, len(v))
          if k == 5: print(k, len(v))
          if k == 6: print(k, len(v))
      

      Like

    • Ruud's avatar

      Ruud 2:23 pm on 20 July 2024 Permalink | Reply

      Quite similar to @Frits, just a it more brute force.

      from istr import istr
      from collections import defaultdict
      
      
      def is_prime(n):
          if n < 2:
              return False
          if n % 2 == 0:
              return n == 2
          k = 3
          while k * k <= n:
              if n % k == 0:
                  return False
              k += 2
          return True
      
      
      collect = defaultdict(list)
      
      for n1 in istr(range(100, 988)):
          if not is_prime(int(n1)):
              continue
          if not n1.all_distinct():
              continue
          for n2 in istr.range(n1 + 1, 988):
              if not is_prime(n2):
                  continue
              if not (n1 | n2).all_distinct():
                  continue
              for n3 in istr.concat(istr.permutations((c for c in istr.digits() if c not in n1 | n2))):
                  if n3 < 1000:
                      continue
                  product = n1 * n2 * n3
                  collect[len(product), product.is_odd()].append((n1, n2, n3))
      
      for v in collect.values():
          if len(v) == 1:
              print(*map(int, v[0]))
      

      Like

  • Unknown's avatar

    Jim Randell 6:28 pm on 14 July 2024 Permalink | Reply
    Tags: by: E. R. J. Benson   

    Brainteaser 1081: Trifling troubles 

    From The Sunday Times, 24th April 1983 [link]

    Fred and I usually do the washing and drying up in our commune. Fred always washes the largest saucepan first, then the second largest, and so on down to the smallest. In this way I can dry them and store them away in the order they were washed, by putting one saucepan inside the previously washed saucepan, ending up with a single pile.

    Yesterday, the cook misread the quantities for the sherry trifle recipe, with the result that Fred got the washing up order completely wrong. For example, he washed the second smallest saucepan immediately after the second largest; and the smallest saucepan immediately before the middle-sized saucepan (i.e. the saucepan with as many saucepans larger than it as there are smaller).

    I realised something was wrong when, having started to put the saucepans away in the usual manner, one of the saucepans did not fit the previously washed saucepan; so I started a second pile. Thereafter each saucepan either fitted in to one, but only one pile, or did not fit into any pile, in which case I started another pile.

    We ended up with a number of piles, each containing more than one saucepan. The first pile to be completed contained more saucepans than the second, which contained more than the third etc.

    In what order were the saucepans washed up? (Answers in the form a, b, c, …, numbering the saucepans from 1 upwards, 1 being the smallest).

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

    [teaser1081]

     
    • Jim Randell's avatar

      Jim Randell 6:29 pm on 14 July 2024 Permalink | Reply

      There must be an odd number of pans (in order for there to be a middle sized one), and there must be at more than 3 pans (so that the second largest is different from the second smallest).

      This Python program considers possible orderings of pans that fit the conditions, and then attempts to organise them into piles that meet the requirements.

      It runs in 169ms. (Internal runtime is 96ms).

      Run: [ @replit ]

      from enigma import (irange, inf, subsets, is_decreasing, printf)
      
      # organise the sequence of pans into piles
      def organise(ss):
        (ps, i) = ([], 0)
        for n in ss:
          # find placement for n
          js = list(j for (j, p) in enumerate(ps) if n < p[-1])
          if len(js) > 1: return None
          if len(js) == 1:
            ps[js[0]].append(n)
          else:
            ps.append([n])
        return ps
      
      # solve for n pans
      def solve(n):
        assert n % 2 == 1
        # allocate numbers to the pans
        ns = list(irange(1, n))
        k = 0
        # choose an ordering for the pans
        for ss in subsets(ns, size=len, select='P'):
          # check ordering conditions:
          # the 2nd smallest (2) is washed immediately after the 2nd largest (n - 1)
          # the smallest (1) is washed immediately before the middle ((n + 1) // 2)
          if not (ss.index(2) == ss.index(n - 1) + 1): continue
          if not (ss.index(1) + 1 == ss.index((n + 1) // 2)): continue
          # organise the pans into piles
          ps = organise(ss)
          if ps and len(ps) > 1 and is_decreasing([len(p) for p in ps] + [1], strict=1):
            # output solution
            printf("{n} pans; {ss} -> {ps}")
            k += 1
        return k
      
      # solve for an odd number of pans > 3
      for n in irange(5, inf, step=2):
        k = solve(n)
        if k:
          printf("[{n} pans -> {k} solutions]")
          break
      

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

      Which gives 3 piles:

      [9, 8, 2, 1]
      [5, 4, 3]
      [7, 6]

      To explore larger number of pans it would probably be better to use a custom routine that generates orders with the necessary conditions, rather than just generate all orders and reject those that are not appropriate.

      Like

      • Ruud's avatar

        Ruud 5:52 pm on 15 July 2024 Permalink | Reply

        I am working on a fast recursive algorithm and find four solutions for n=9
        9 7 6 1 , 5 4 3 , 8 2
        9 7 6 3 , 8 2 1 , 5 4
        9 7 6 4 , 8 2 1 , 5 3
        9 8 2 1 , 5 4 3 , 7 6
        The last one matches yours. But aren’t the others valid? Did I miss a condition?

        Like

        • Jim Randell's avatar

          Jim Randell 6:22 pm on 15 July 2024 Permalink | Reply

          @Ruud: In your first three sequences when you get the 2 pan you could put it into either of two available piles, so they are not viable sequences.

          Like

      • Ruud's avatar

        Ruud 6:54 am on 16 July 2024 Permalink | Reply

        I have solved this with a recursive algorithm, which is significantly faster than Jim’s but still very slow when the number of pans becomes >= 15.
        Here’s the code:

        from itertools import count
        
        def wash(remaining_pans, last_pan, piles, washed_pans):
            if not remaining_pans:
                if all(len(piles1) > len(piles2) > 1 for piles1, piles2 in zip(piles, piles[1:])):
                    print(piles, washed_pans)
                return
            if len(piles) > max_number_of_piles:
                return
        
            for pan in remaining_pans:
                if pan != 2 and last_pan == (n - 1):
                    continue
                if pan != (n + 1) // 2 and last_pan == 1:
                    continue
                if pan == 2 and last_pan != (n - 1):
                    continue
                if pan == (n + 1) // 2 and last_pan != 1:
                    continue
                selected_pile = None
                for pile in piles:
                    if pile[-1] > pan:
                        if selected_pile is None:
                            selected_pile = pile
                        else:
                            selected_pile = None
                            break
                if selected_pile is None:
                    wash(
                        last_pan=pan,
                        remaining_pans=[ipan for ipan in remaining_pans if ipan != pan],
                        piles=[pile[:] for pile in piles] + [[pan]],
                        washed_pans=washed_pans + [pan],
                    )
                else:
                    wash(
                        last_pan=pan,
                        remaining_pans=[ipan for ipan in remaining_pans if ipan != pan],
                        piles=[pile + [pan] if pile == selected_pile else pile[:] for pile in piles],
                        washed_pans=washed_pans + [pan],
                    )
        
        
        for n in count(5,2):
            print(f'number of pans = {n}')
            max_number_of_piles=0
            cum_pile_height=0
            for pile_height in count(2):
                cum_pile_height+=pile_height
                max_number_of_piles+=1
                if cum_pile_height>=n: 
                    break
            wash(last_pan=0, remaining_pans=range(1, n + 1), piles=[], washed_pans=[])
        

        Like

        • Frits's avatar

          Frits 4:52 pm on 18 July 2024 Permalink | Reply

          @Ruud,

          “cum_pile_height” probably must be initialized to 1 to get the correct pile_height for n = 15.

          With some more checks (like “if len(piles) > 1 and len(piles[-1]) >= len(piles[-2]): return”) your program runs faster:

           
          pypy TriflingTroubles1.py
          2024-07-18 17:47:39.184252 number of pans = 5
          2024-07-18 17:47:39.186248 n = 5 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:39.188241 n = 5 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:39.191231 number of pans = 7
          2024-07-18 17:47:39.191231 n = 7 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:39.192234 n = 7 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:39.192234 n = 7 pile_height = 4 max_number_of_piles = 3 cum_pile_height = 10
          2024-07-18 17:47:39.194224 number of pans = 9
          2024-07-18 17:47:39.194224 n = 9 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:39.194224 n = 9 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:39.195221 n = 9 pile_height = 4 max_number_of_piles = 3 cum_pile_height = 10
          [[9, 8, 2, 1], [5, 4, 3], [7, 6]] [9, 8, 2, 1, 5, 4, 3, 7, 6]
          2024-07-18 17:47:39.202204 number of pans = 11
          2024-07-18 17:47:39.204206 n = 11 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:39.205194 n = 11 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:39.206192 n = 11 pile_height = 4 max_number_of_piles = 3 cum_pile_height = 10
          2024-07-18 17:47:39.206192 n = 11 pile_height = 5 max_number_of_piles = 4 cum_pile_height = 15
          2024-07-18 17:47:39.267028 number of pans = 13
          2024-07-18 17:47:39.267460 n = 13 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:39.268462 n = 13 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:39.271365 n = 13 pile_height = 4 max_number_of_piles = 3 cum_pile_height = 10
          2024-07-18 17:47:39.271365 n = 13 pile_height = 5 max_number_of_piles = 4 cum_pile_height = 15
          2024-07-18 17:47:39.467843 number of pans = 15
          2024-07-18 17:47:39.468983 n = 15 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:39.469985 n = 15 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:39.472540 n = 15 pile_height = 4 max_number_of_piles = 3 cum_pile_height = 10
          2024-07-18 17:47:39.472540 n = 15 pile_height = 5 max_number_of_piles = 4 cum_pile_height = 15
          2024-07-18 17:47:40.190655 number of pans = 17
          2024-07-18 17:47:40.191653 n = 17 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:40.192626 n = 17 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:40.195803 n = 17 pile_height = 4 max_number_of_piles = 3 cum_pile_height = 10
          2024-07-18 17:47:40.195803 n = 17 pile_height = 5 max_number_of_piles = 4 cum_pile_height = 15
          2024-07-18 17:47:40.196803 n = 17 pile_height = 6 max_number_of_piles = 5 cum_pile_height = 21
          2024-07-18 17:47:51.650183 number of pans = 19
          2024-07-18 17:47:51.651412 n = 19 pile_height = 2 max_number_of_piles = 1 cum_pile_height = 3
          2024-07-18 17:47:51.652742 n = 19 pile_height = 3 max_number_of_piles = 2 cum_pile_height = 6
          2024-07-18 17:47:51.655439 n = 19 pile_height = 4 max_number_of_piles = 3 cum_pile_height = 10
          2024-07-18 17:47:51.656182 n = 19 pile_height = 5 max_number_of_piles = 4 cum_pile_height = 15
          2024-07-18 17:47:51.656182 n = 19 pile_height = 6 max_number_of_piles = 5 cum_pile_height = 21 
          

          Like

          • Frits's avatar

            Frits 4:58 pm on 18 July 2024 Permalink | Reply

            @Ruud, Sorry, my first statement is incorrect (I forgot that each pile has to have more than one saucepan)

            Like

    • Frits's avatar

      Frits 4:43 pm on 18 July 2024 Permalink | Reply

      Runs within a second for up to 21 saucepans.

      from itertools import combinations
      from math import ceil
      
      # triangular root
      trirt = lambda x: 0.5 * ((8 * x + 1)**.5 - 1.0)
      
      # check validity of sequence <s>
      def check(n, p, s):
        if not s: return False
        # check second smallest and second largest
        if (n28 := (2 in s) + ((n - 1) in s)) == 0: return True
        if n28 == 1: return False
        return not((p2 := s.index(2)) <= 0 or s[p2 - 1] != n - 1)
        
      def solve(n, h, ns, p, m, ss=[]):
        if not ns:
          yield ss
          return # prevent indentation
       
        # determine which numbers we have to select (besides smallest number)
        base = [x for x in ns[:-1] if x != h] if p == 1 else ns[:-1]
        if p == 2: # second pile starts with <h>
          base = [x for x in base if x < h]
      
        mn = max(0, ceil(trirt(len(ns))) - 1 - (p == 2))
        mx = min(len(base), n if not ss else len(ss[-1]) - 1)  
        for i in range(mn, mx + 1):
          for c in combinations(base, i):
            c += (ns[-1], )              # suffix lowest remaining number
            if p == 2 and h not in c: 
              c = (h, ) + c              # second pile starts with <h>
            if (m_ := m - len(c)) == 1:  # each containing more than one saucepan
              continue
            if check(n, p, c):           # check second smallest and second largest
              yield from solve(n, h, [x for x in ns if x not in c], p + 1, m_,
                               ss + [c])
            
      M = 21                             # maximum number of saucepans
      for n in range(5, M + 1, 2):
        print(f"number of saucepans: {n}")
        for c in solve(n, (n + 1) // 2, range(n, 0, -1), 1, n):
          print("-----------------", c)
      

      Like

      • Ruud's avatar

        Ruud 5:49 am on 19 July 2024 Permalink | Reply

        @Frits
        Can you explain this code/algortithm. The one letter varibiables don’t really help …

        Like

      • Frits's avatar

        Frits 2:37 pm on 19 July 2024 Permalink | Reply

        @Jim, please replace my code with this version with more comments and using header:

        I noticed that each pile can be regarded as the result of a combinations() call with descending saucepan numbers.

        from itertools import combinations
        from math import ceil
        
        # triangular root
        trirt = lambda x: 0.5 * ((8 * x + 1)**.5 - 1.0)
        
        # check validity of sequence <s> (2nd smallest 2nd largest logic)
        def check(n, s):
          if not s: return False
          # check second smallest and second largest
          if (totSecSmallSecLarge := (2 in s) + ((n - 1) in s)) == 0: return True
          if totSecSmallSecLarge == 1: return False        # we need none or both
          # the 2nd smallest must be washed immediately after the 2nd largest 
          return not((p2 := s.index(2)) == 0 or s[p2 - 1] != n - 1)
        
        # add a new pile of saucepans
        # <n> = original nr of saucepans (middle one <h>), <ns> = saucepan nrs left,
        # <p> = pile nr, <r> = nr of saucepans left (rest), <ss> = piles
        def solve(n, h, ns, p, r, ss=[]):
          if not ns: # no more saucepans left
            yield ss
            return # prevent indentation
         
          # determine which numbers we have to use below in combinations() 
          # (besides smallest number)
          base = [x for x in ns[:-1] if x != h] if p == 1 else ns[:-1]
          if p == 2: # second pile starts with <h>
            base = [x for x in base if x < h] # only saucepan nrs below <h>
        
          # if triangular sum 1 + 2 + ... + y reaches nr of saucepans left <r> then we
          # need at least y + 1 saucepans (if r = tri(y) - 1 we only need y saucepans)
          mn = max(0, int(trirt(r)) - (p == 2))    
          # use less saucepans than in previous pile
          mx = min(len(base), n if not ss else len(ss[-1]) - 1)  
          
          # loop over number of saucepans to use for the next/this pile
          for i in range(mn, mx + 1):      # nr of saucepans needed besides smallest
            # pick <i> descending saucepan numbers for next/this pile
            for c in combinations(base, i):
              c += (ns[-1], )              # suffix lowest remaining saucepan number
              if p == 2 and h not in c:    # second pile starts with <h>
                c = (h, ) + c              
              if (r_ := r - len(c)) == 1:  # each containing more than one saucepan
                continue
              if check(n, c):              # check second smallest and second largest
                yield from solve(n, h, [x for x in ns if x not in c], p + 1, r_,
                                 ss + [c])
              
        M = 21                             # maximum number of saucepans (adjustable)
        for n in range(5, M + 1, 2):       
          print(f"number of saucepans: {n}")
          # use a descending sequence of numbers n...1 so that combinations() will
          # also return a descending sequence of saucepan numbers
          for c in solve(n, (n + 1) // 2, range(n, 0, -1), 1, n):
            print("-----------------", c)
        

        Like

  • Unknown's avatar

    Jim Randell 4:36 pm on 12 July 2024 Permalink | Reply
    Tags:   

    Teaser 3225: My extended family 

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

    When Daniela, the youngest of my nieces, was born not so long ago, her brother John remarked that Daniela’s year of birth was very special: no number added to the sum of that number’s digits was equal to her year of birth. The same was true for John’s year of birth.

    “Well,” intervened Lorena, the eldest of my nieces, “if you want to know, my own year of birth — earlier than yours — also shares that very same property, and so does the year of birth of our father Diego. The same is even true for our grandmother Anna, who, as you know, was born in the 1950s”.

    When were Daniela, John, Lorena, Diego and Anna born?

    [teaser3225]

     
    • Jim Randell's avatar

      Jim Randell 4:43 pm on 12 July 2024 Permalink | Reply

      It is straightforward to find candidate years, and we can then fit them to the family circumstances.

      I imposed a reasonable gap between generations, and although I’m not sure this is strictly necessary it does give a unique solution to the puzzle.

      The following Python program runs in 73ms. (Internal runtime is 245µs).

      Run: [ @replit ]

      from enigma import (irange, dsum, diff, subsets, printf)
      
      # find numbers added to their digit sum
      xs = set(n + dsum(n) for n in irange(1922, 2024))
      
      # look for recent years not in xs
      years = diff(irange(1950, 2024), xs)
      printf("years = {years}")
      
      # enforce generation gaps
      gap = lambda x, y: 15 < abs(y - x) < 50
      # assign birth years in order
      for (A, D, L, J, D2) in subsets(years, size=5):
        if not (A // 10 == 195): continue
        if not (gap(A, D) and gap(D, L) and gap(D, J) and gap(D, D2)): continue
        # output solution
        printf("A={A} D={D} L={L} J={J} D2={D2}")
      

      Solution: The years of birth were: Daniela = 2022; John = 2007; Lorena = 1996; Diego = 1974; Anna = 1952.

      There are only 7 recent candidate years (shown below), and only one reasonable assignment of birth years (also shown below):

      1952 = Anna
      1963
      1974 = Diego
      1985
      1996 = Lorena
      2007 = John
      2022 = Daniela

      Anna was born in the 1950’s (i.e. 1952), and was (presumably) more than 11 when Diego was born, so he could be born in 1974 (when Anna was 22).

      And Diego was (presumably) more than 11 when Lorena was born, so the nephews/nieces were born in 1996, 2007, 2022 (when Diego was 22, 33, 48).

      And this is the only situation where there is more than 11 years between generations.

      Like

      • Tony Smith's avatar

        Tony Smith 12:01 pm on 13 July 2024 Permalink | Reply

        Biologically generation gap could be eg 11, and Diego could have been born before Anna.
        These possibilities give multiple answers.

        Like

    • Ruud's avatar

      Ruud 7:47 am on 13 July 2024 Permalink | Reply

      We can simply with

      from istr import istr
      
      print(sorted(set(range(1950, 2024)) - {int(sum(n) + n) for n in istr(range(2025))}))
      

      find that the only possible years of birth are
      1952, 1963, 1974, 1985, 1996, 2007, 2022
      But, given the fact that there are three generations, this leads to
      Anna 1952
      Diego 1974
      Loreena 1996
      John 2007
      Daniella 2022

      Like

      • Brian Gladman's avatar

        Brian Gladman 9:13 am on 13 July 2024 Permalink | Reply

        Hi Ruud,

        These teasers are published on Sundays each week as a competition for prizes in the print edition of the Sunday Times. They are designed with the intention that they are solved without computer assistance. I would hence ask you not to encourage cheating by revealing solutions before the competition ends two weeks after its publication.

        Like

  • Unknown's avatar

    Jim Randell 9:46 am on 11 July 2024 Permalink | Reply
    Tags:   

    Brainteaser 1368: Playgoers 

    From The Sunday Times, 20th November 1988 [link]

    In the following exact long division each separate letter represents a different digit:

    Actually, Daisy, Rod, May and Sue are four friends who have just been to see a play. Using the same digits to replace its letters, the play is called 4347096.

    Name the play.

    [teaser1368]

     
    • Jim Randell's avatar

      Jim Randell 9:47 am on 11 July 2024 Permalink | Reply

      We can use the [[ SubstitutedDivision() ]] solver from the enigma.py library to solve this puzzle, and the [[ output_div() ]] function to generate the completed division sum.

      The following Python program runs in 84ms. (Internal runtime is 7.9ms).

      Run: [ @replit ]

      from enigma import (SubstitutedDivision, output_div, translate, printf)
      
      p = SubstitutedDivision(
        "DAISY / MAY = ROD",
        ["DAI - SUE = ??", "??? - ??? = ???", "???? - ???? = 0"],
      )
      
      for s in p.solve(verbose=''):
        output_div(s.a, s.b, pre="  ", start="", end="")
        # map digits -> symbols
        m = dict((str(v), k) for (k, v) in s.d.items())
        play = translate("4347096", m)
        printf("-> play = \"{play}\"")
      

      Solution: The play is “AMADEUS”.

      The completed sum is:

      Like

    • GeoffR's avatar

      GeoffR 1:22 pm on 11 July 2024 Permalink | Reply

      Just using the specified letters, without doing a full division sum, so not a full program solution, but it gets the answer

      % A Solution in MiniZinc
      include "globals.mzn";
       
      var 1..9:R; var 0..9:O; var 0..9:D; var 1..9:M;
      var 0..9:A; var 0..9:Y; var 0..9:I; var 1..9:S;
      var 0..9:U; var 0..9:E;
      
      constraint all_different ([R,O,D,M,A,Y,I,S,U,E]);
      
      var 100..999:ROD == 100*R + 10*O + D;
      var 100..999:MAY == 100*M + 10*A + Y;
      var 100..999:SUE == 100*S + 10*U + E;
      var 10000..99999:DAISY == 10000*D + 1000*A + 100*I + 10*S + Y;
      
      constraint ROD * MAY == DAISY;
      constraint R * MAY == SUE;
      
      solve satisfy;
      
      output ["[R, O, D, M, A, Y, I, S, U, E ] = " ++ show( [R, O, D, M, A, Y, I, S, U, E ] ) ];
      
      % [R, O, D, M, A, Y, I, S, U, E ] =
      % [2, 1, 7, 3, 4, 5, 8, 6, 9, 0]
      % ----------
      % ==========
      % By inspection, 4347096 = AMADEUS
      
      
      

      Like

    • ruudvanderham's avatar

      ruudvanderham 1:26 pm on 11 July 2024 Permalink | Reply

      from istr import istr
      
      for m, a, y, r, o, d in istr.permutations(istr.digits(), 6):
          daisy = (m | a | y) * (r | o | d)
          if len(daisy) == 5 and daisy[0] == d and daisy[1] == a and daisy[4] == y:
              i = daisy[2]
              s = daisy[3]
              sue = r * (m | a | y)
              if len(sue) == 3 and sue[0] == s:
                  u = sue[1]
                  e = sue[2]
                  if (m | a | y | r | o | d | i | s | u | e).all_distinct():
                      print("".join("mayrodisue"[(m | a | y | r | o | d | i | s | u | e).index(c)] for c in istr(4347096)))
      

      Like

  • Unknown's avatar

    Jim Randell 5:24 pm on 9 July 2024 Permalink | Reply
    Tags:   

    Teaser 2579: Box clever 

    From The Sunday Times, 26th February 2012 [link] [link]

    I have placed the digits 1 to 9 in a three-by-three grid made up of nine boxes. For each two-by-two array within the grid, the sum of its four entries is the same. Furthermore, if you reverse the order of the digits in that common total, then you get the sum of the four corner entries from the original grid.

    Which digits are adjacent to the 6? (i.e. digits whose boxes have a common side with 6’s box?)

    [teaser2579]

     
    • Jim Randell's avatar

      Jim Randell 5:25 pm on 9 July 2024 Permalink | Reply

      Here is a run-file that uses the [[ SubstitutedExpression ]] solver from the enigma.py library to find possible grid layouts.

      It runs in 94ms. (Internal runtime of the generated program is 8.6ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C
      #  D E F
      #  G H I
      
      --distinct="ABCDEFGHI"
      --invalid="0,ABCDEFGHI"
      
      # each box of 4 digits sums to XY
      "A + B + D + E = XY"
      "B + C + E + F = XY"
      "D + E + G + H = XY"
      "E + F + H + I = XY"
      
      # the four corners sum to YX
      "A + C + G + I = YX"
      
      # [optional] remove duplicate solutions
      "A < C" "A < G" "C < G"
      
      --template=""
      

      Line 22 ensures only one version of symmetrically equivalent layouts (rotations and reflections) is produced.

      For a complete solution to the puzzle we can further process the results to find the required digits for the answer.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, grid_adjacency, chunk, printf)
      
      adj = grid_adjacency(3, 3)
      
      p = SubstitutedExpression.from_file("teaser2579.run")
      
      for s in p.solve(verbose=''):
        grid = list(s[x] for x in 'ABCDEFGHI')
        ans = sorted(grid[i] for i in adj[grid.index(6)])
        printf("{grid} -> {ans}", grid=list(chunk(grid, 3)))
      

      Solution: The digits adjacent to 6 are: 3, 4, 5.

      Here is a viable layout:

      The 2×2 grids give:

      1 + 9 + 8 + 3 = 21
      9 + 2 + 3 + 7 = 21
      8 + 3 + 4 + 6 = 21
      3 + 7 + 6 + 5 = 21

      And the corners:

      1 + 2 + 4 + 5 = 12

      There are 8 potential layouts, but they can all be rotated/reflected to give the one above.

      Like

      • Ruud's avatar

        Ruud 12:40 pm on 10 July 2024 Permalink | Reply

        """
        label boxes as
        012
        345
        678
        """
        import itertools
        
        
        def sum4(*indexes):
            return sum(digits[index] for index in indexes)
        
        
        adjacent = {0: (1, 3), 1: (0, 2, 4), 2: (1, 5), 3: (0, 4, 6), 4: (1, 3, 5, 7), 5: (2, 4, 8), 6: {3, 7}, 7: (6, 4, 8), 8: (5, 7)}
        
        for digits in itertools.permutations(range(1, 10)):
            if (s := sum4(0, 1, 3, 4)) == sum4(1, 2, 4, 5) == sum4(3, 4, 6, 7) == sum4(4, 5, 7, 8) and int(str(s)[::-1]) == sum4(0, 2, 6, 8):
                print(digits[0], digits[1], digits[2], "\n", digits[3], digits[4], digits[5], "\n", digits[6], digits[7], digits[8], sep="")
                print("     ", *sorted(digits[index] for index in adjacent[digits.index(6)]))
        

        Like

      • Frits's avatar

        Frits 9:19 pm on 10 July 2024 Permalink | Reply

        @Jim, “verbose” only seems to work in the run member.

        Like

        • Jim Randell's avatar

          Jim Randell 7:17 am on 11 July 2024 Permalink | Reply

          @Frits: Care to elaborate?

          Like

          • Frits's avatar

            Frits 8:09 am on 11 July 2024 Permalink | Reply

            @Jim,

            “for s in p.solve(verbose=256):” or “for s in p.solve(verbose=’3′):” is not working in my environment. ‘–verbose=”3″‘ works well in the run member.

            Like

            • Jim Randell's avatar

              Jim Randell 3:06 pm on 11 July 2024 Permalink | Reply

              @Frits:

              You can add an appropriate [[ --verbose ]] directive to the run file, or pass it as an argument to the [[ from_file() ]] call, so that it is active when the code is being generated.

              p = SubstitutedExpression.from_file("teaser/teaser2579.run", ["--verbose=C"])
              

              But I’ve changed the [[ solve() ]] function so that the verbose parameter takes effect before anything else, which means if the code has not already been generated (which will usually be the case the first time solve() is called), then it will be displayed before it is compiled.

              Like

    • GeoffR's avatar

      GeoffR 7:31 pm on 9 July 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      %  A B C
      %  D E F
      %  G H I
      
      var 1..9:A; var 1..9:B; var 1..9:C;
      var 1..9:D; var 1..9:E; var 1..9:F;
      var 1..9:G; var 1..9:H; var 1..9:I;
      
      var 12..98:total;
      var 12..98:rev_total;
      
      constraint all_different([A,B,C,D,E,F,G,H,I]);
      
      constraint A+B+D+E == total /\ B+C+E+F == total /\ D+E+G+H == total /\ E+F+H+I == total;
      
      constraint rev_total == 10*(total mod 10) + total div 10;
      
      constraint A + C + G + I == rev_total;
      
      % order constraint
      constraint A < C /\ A < G /\ C < G;
      
      solve satisfy; 
      
      output[  show([A,B,C]) ++ "\n" ++ show([D,E,F]) ++ "\n" ++ show([G,H,I]) ];
      
      % [1, 9, 2]
      % [8, 3, 7]
      % [4, 6, 5]
      % ----------
      % ==========
      % From grid, the digits adjacent to 6 are 3, 4, and 5.
      
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 3:34 pm on 10 July 2024 Permalink | Reply

      
      from enigma import Timer
      timer = Timer('ST2579', timer='E')
      timer.start()
      
      from itertools import permutations
      
      # a b c
      # d e f
      # g h i
      
      for a, b, c, d, e, f, g, h, i in permutations (range(1, 10)):
        # check four square totals
        tot = a + b + d + e
        if b + c + e + f == tot:
          if d + e + g + h == tot:
            if e + f + h + i == tot:
              
              # check corner digits sum for a reverse total
              x, y = tot // 10, tot % 10
              rev_tot = 10 * y + x
              if a + c + g + i == rev_tot:
                if a < c and a < g and c < g:
                  
                  # digits whose boxes have a common side with 6’s box
                  if a == 6:print(f"Adjacent digits = {b} and {d}.")
                  if b == 6:print(f"Adjacent digits = {a},{e} and {c}.")
                  if c == 6:print(f"Adjacent digits = {b} and {f}.")
                  if d == 6:print(f"Adjacent digits = {a},{e} and {g}.")
                  if e == 6:print(f"Adjacent digits = {b},{d},{f} and {h}.")
                  if f == 6:print(f"Adjacent digits = {c},{e} and {i}.")
                  if g == 6:print(f"Adjacent digits = {d} and {h}.")
                  if h == 6:print(f"Adjacent digits = {g},{e} and {i}.")
                  if i == 6:print(f"Adjacent digits = {h} and {f}.")
                  # Adjacent digits = 4,3 and 5.
                  timer.stop()      
                  timer.report()
                  # [ST2579] total time: 0.0290888s (29.09ms)
      
      
      

      Like

      • GeoffR's avatar

        GeoffR 11:51 am on 11 July 2024 Permalink | Reply

        I recoded this teaser in a 3-stage permutation for 4, 2 and 3 digits – my previous posting was a brute-force 9-digit permutation. It was much faster, albeit that this 2nd posting was on an I9 CPU as opposed to an I7 CPU for the original posting

        from enigma import Timer
        timer = Timer('ST2579', timer='E')
        timer.start()
        
        from itertools import permutations
        
        # a b c
        # d e f
        # g h i
        
        digits = set(range(1, 10))
        
        for p1 in permutations (digits, 4):
          a, b, d, e = p1
         
          # check four square totals
          tot = a + b + d + e
          q1 = digits.difference( p1)
          for p2 in permutations(q1, 2):
            c, f = p2
            if b + c + e + f == tot:
               q3 = q1.difference(p2)
               for p3 in permutations(q3):
                 g, h, i = p3
                 if d + e + g + h == tot:
                   if e + f + h + i == tot:
               
                     # check corner digits sum for a reverse total
                     x, y = tot // 10, tot % 10
                     rev_tot = 10 * y + x
                     if a + c + g + i == rev_tot:
                       if a < c and a < g and c < g:
                   
                         # digits whose boxes have a common side with 6’s box
                         if a == 6:print(f"Adjacent digits = {b} and {d}.")
                         if b == 6:print(f"Adjacent digits = {a},{e} and {c}.")
                         if c == 6:print(f"Adjacent digits = {b} and {f}.")
                         if d == 6:print(f"Adjacent digits = {a},{e} and {g}.")
                         if e == 6:print(f"Adjacent digits = {b},{d},{f} and {h}.")
                         if f == 6:print(f"Adjacent digits = {c},{e} and {i}.")
                         if g == 6:print(f"Adjacent digits = {d} and {h}.")
                         if h == 6:print(f"Adjacent digits = {g},{e} and {i}.")
                         if i == 6:print(f"Adjacent digits = {h} and {f}.")
                         # Adjacent digits = 4,3 and 5.
                         timer.stop()      
                         timer.report()
                         # [ST2579] total time: 0.0055232s (5.52ms)
        
        
        

        Like

    • Frits's avatar

      Frits 9:08 pm on 10 July 2024 Permalink | Reply

      Looping over 4 variables.

           
      from itertools import permutations
      
      row, col = lambda i: i // 3, lambda i: i % 3
      
      # adjacent fields to field <i>
      adj = {i: tuple(j for j in range(9)
                if sum(abs(tp(j) - tp(i)) for tp in (row, col)) == 1)
                for i in range(9)}
                
      #  A B C
      #  D E F
      #  G H I
      
      '''
      # each box of 4 digits sums to XY
      "A + B + D + E = XY"
      "B + C + E + F = XY"
      "D + E + G + H = XY"
      "E + F + H + I = XY"
       
      # the four corners sum to YX
      "A + C + G + I = YX"
      '''
      
      sols, set9 = set(), set(range(1, 10))
      # Y is 1 or 2 as YX < 30 and X = 2 (H formula) so XY = 21
      XY, YX = 21, 12
      # get first three numbers
      for B, D, E in permutations(range(1, 10), 3):
        # remaining numbers after permutation
        r = sorted(set9 - {B, D, E})
        A = XY - (B + D + E)
        if A in {B, D, E} or not (0 < A <= 12 - sum(r[:2])): continue
       
        for F in set(r).difference([A]):
          C = XY - B - E - F
          H = (4 * XY - YX) // 2 - (B + D + 2 * E + F)
          G = XY - D - E - H
          I =  YX - A - C - G
          if E + F + H + I != XY: continue
          
          vars = [A, B, C, D, E, F, G, H, I]
          if set(vars) != set9: continue
          # store digits adjacent to the 6
          sols.add(tuple(sorted(vars[j] for j in adj[vars.index(6)])))
      
      print("answer:", ' or '.join(str(s) for s in sols)) 
      

      Like

  • Unknown's avatar

    Jim Randell 4:27 pm on 5 July 2024 Permalink | Reply
    Tags:   

    Teaser 3224: Put your finger on it 

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

    On a particular stringed instrument, a fingering pattern of 2, 2, 3, 1 means that the pitches of strings 1 to 4 are raised by that many steps respectively from the “open strings” (i.e. their pitches when not fingered); this gives a “chord” of pitches C, E, G, A♯ in some order. The fingering pattern 4, 1, 0, x (for some whole number x from 0 to 11 inclusive) would give pitches F, A, C, D in some order, if these were all shifted by some fixed amount.

    [Pitches range through C, C♯, D, D♯, E, F, F♯, G, G♯, A, A♯, B, which then repeat].

    What are the pitches of “open strings” 1 to 4, and what is the value of x

    [teaser3224]

     
    • Jim Randell's avatar

      Jim Randell 4:46 pm on 5 July 2024 Permalink | Reply

      A simple exhaustive search of the problem space finds the answer fairly quickly. So it will do for now – I might refine it a bit later.

      This Python program runs in 99ms. (Internal runtime is 8.3ms).

      Run: [ @replit ]

      from enigma import (irange, subsets, join, printf)
      
      # format a set of notes
      fmt = lambda ns: join(ns, sep=" ", enc="()")
      
      # the order of notes
      notes = "C C# D D# E F F# G G# A A# B".split()
      n = len(notes)
      
      # finger strings <ss> at frets <fs>
      def finger(ss, fs):
        return list(notes[(notes.index(x) + y) % n] for (x, y) in zip(ss, fs))
      
      # target chords
      tgt1 = sorted("C E G A#".split())
      tgt2 = sorted("F A C D".split())
      
      # choose an ordering for the first target chord
      for ns1 in subsets(tgt1, size=4, select='P'):
        # and fret it down to open strings
        ss = finger(ns1, [-2, -2, -3, -1])
      
        # choose a value for x and a transposition
        for (x, t) in subsets(irange(0, 11), size=2, select='M'):
          ns2 = finger(ss, [4 + t, 1 + t, 0 + t, x + t])
          # check for the target chord
          if sorted(ns2) == tgt2:
            printf("{ss} @[2, 2, 3, 1] = {ns1}; @[4, 1, 0, {x}] + {t} = {ns2}", ss=fmt(ss), ns1=fmt(ns1), ns2=fmt(ns2))
      

      Solution: The open strings are [D, G♯, E, B]. And the value of x is 2.

      Fretting @[2, 2, 3, 1] gives [E, A♯, G, C].

      And, shifting up by 8 frets (e.g. using a capo) gives [A♯, E, C, G].

      Then fretting @[4, 1, 0, 2] (above the capo) gives [D, F, C, A].

      Like

    • Ruud's avatar

      Ruud 7:31 am on 6 July 2024 Permalink | Reply

      Here’s my brute force solution using sets:

      from itertools import product
      
      pitches = "C C# D D# E F F# G G# A A# B".split()
      
      def index_set(s):
          return {pitches.index(n) for n in s}
      
      def iadd(i, n):
          return (i + n) % 12
      
      for istrings in product(range(12), repeat=4):
          if {iadd(istrings[0], 2), iadd(istrings[1], 2), iadd(istrings[2], 3), iadd(istrings[3], 1)} == index_set({"C", "E", "G", "A#"}):
              for shift in range(12):
                  for x in range(12):
                      if {iadd(istrings[0], 4 + shift), iadd(istrings[1], 1 + shift), iadd(istrings[2], 0 + shift), iadd(istrings[3], x + shift)} == index_set(
                          {"F", "A", "C", "D"}
                      ):
                          print(f"open strings are {' '.join(pitches[i] for i in istrings)}")
                          print(f"x = {x}")
      

      Like

  • Unknown's avatar

    Jim Randell 4:32 pm on 28 June 2024 Permalink | Reply
    Tags:   

    Teaser 3223: Shaping up 

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

    Clark wondered how many different shapes he could draw with identical squares joined edge to edge and each shape containing the same number of squares. He only drew five different shapes containing four squares, for example, because he ignored rotations and reflections:

    He drew all of the different shapes containing 1, 2, 3, 4, 5 and 6 squares and wrote down the total number of different shapes in each case. He took the six totals in some order, without reordering the digits within any total, and placed them end to end to form a sequence of digits which could also be formed by placing six prime numbers end to end.

    In ascending order, what were the six prime numbers?

    [teaser3223]

     
    • Jim Randell's avatar

      Jim Randell 4:53 pm on 28 June 2024 Permalink | Reply

      This is fairly straightforward, if you know how many free n-polyominoes there are of the required sizes. [@wikipedia]

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

      Run: [ @replit ]

      from enigma import (irange, primes, subsets, concat, uniq, ordered, printf)
      
      # number of n-polyominoes (n = 1..6) [see: OEIS A000105]
      ns = [1, 1, 2, 5, 12, 35]
      
      # can we split a string into k primes?
      def solve(s, k, ps=list()):
        if k == 1:
          p = int(s)
          if p in primes:
            yield ps + [p]
        elif k > 0:
          for n in irange(1, len(s) + 1 - k):
            p = int(s[:n])
            if p in primes:
              yield from solve(s[n:], k - 1, ps + [p])
      
      # collect answers (ordered sequence of primes)
      ans = set()
      # consider possible concatenated sequences
      for s in uniq(subsets(ns, size=len, select='P', fn=concat)):
        # can we split it into 6 primes?
        for ps in solve(s, 6):
          ss = ordered(*ps)
          printf("[{s} -> {ps} -> {ss}]")
          ans.add(ss)
      
      # output solution(s)
      for ss in sorted(ans):
        printf("answer = {ss}")
      

      Solution: The 6 prime numbers are: 2, 2, 5, 5, 11, 13.

      (Although note that they are not 6 different prime numbers).


      The number of free n-polyominoes for n = 1..6 is:

      1, 1, 2, 5, 12, 35

      (i.e. there is only 1 free monomino, going up to 35 free hexominoes).

      And we don’t have to worry about polyominoes with holes, as they don’t appear until we reach septominoes (7-ominoes).

      We can order these numbers as follows:

      1, 12, 1, 35, 2, 5 → “11213525”

      and this string of digits can be split into 6 prime numbers as follows:

      “11213525” → 11, 2, 13, 5, 2, 5

      In fact, we can order the numbers into 24 different strings of digits that can be split into primes, but they all yield the same collection of primes.

      Like

  • Unknown's avatar

    Jim Randell 3:59 pm on 25 June 2024 Permalink | Reply
    Tags:   

    Teaser 2578: The right money 

    From The Sunday Times, 19th February 2012 [link] [link]

    In order to conserve his change, Andrew, our local shopkeeper, likes to be given the exact money. So today I went to Andrew’s shop with a collection of coins totalling a whole number of pounds in value. I could use these coins to pay exactly any amount from one penny up to the total value of the coins. Furthermore, the number of coins equalled the number of pounds that all the coins were worth; I had chosen the minimum number of coins to have this property.

    (a) How many coins did I have?
    (b) For which denominations did I have more than one coin of that value?

    [teaser2578]

     
    • Jim Randell's avatar

      Jim Randell 3:59 pm on 25 June 2024 Permalink | Reply

      I am assuming we are using “standard” UK coinage which was in circulation at the time of the publication of the puzzle. These are: 1p, 2p, 5p, 10p, 20p, 50p, £1 (= 100p), £2 (= 200p).

      There are special commemorative 25p and £5 coins, but these are not commonly used in making purchases.

      We can get a nice compact program without doing any particular analysis (like determining there must be at least one 1p coin), and it runs in just under 1s, so it not too bad. (I also tried a custom express() routine which makes an amount using an exact number of coins, but it was only a little bit faster).

      We are going to need at least 10 coins to be able to make the required different amounts (with a set of 10 different coins there are 2^10 − 1 = 1023 subsets, which could potentially allow all values from 1p – 1000p to be made, but with fewer coins there will be insufficiently many subsets).

      This Python program runs in 860ms (using PyPy).

      Run: [ @replit ]

      from enigma import (irange, inf, subsets, multiset, printf)
      
      # coin denominations (in pence)
      coins = (1, 2, 5, 10, 20, 50, 100, 200)
      
      # consider number coins (= total value in pounds)
      for n in irange(10, inf):
        done = 0
        # choose n coins ...
        for vs in subsets(coins, size=n, select='R', fn=multiset.from_seq):
          # ... with a total value of n pounds
          if vs.sum() != 100 * n: continue
          # can we make all values between 1 and 100n using these coins?
          if not all(any(vs.express(x)) for x in irange(1, 100 * n)): continue
          # output solution
          printf("{n} coins -> {vs}", vs=vs.map2str(arr='p x', sep='; '))
          done = 1
        if done: break
      

      Solution: (a) There are 16 coins; (b) There is more than one of the 2p, 10p, £2 coins.

      The collection is:

      1× 1p
      2× 2p
      1× 5p
      2× 10p
      1× 20p
      1× 50p
      1× £1
      7× £2
      total = £16 in 16 coins


      If we additionally allow 25p and £5 coins we can achieve the answer using only 13 coins:

      1× 1p
      2× 2p
      2× 5p
      1× 10p
      1× 25p
      1× 50p
      1× £1
      3× £2
      1× £5
      total = £13 in 13 coins

      1x 1p
      2× 2p
      1× 5p
      2× 10p
      1× 20p
      1× 50p
      1× £1
      3× £2
      1× £5
      total = £13 in 13 coins

      Like

      • Frits's avatar

        Frits 1:30 pm on 1 July 2024 Permalink | Reply

        Lucky for you the coin denominations weren’t something like [1, 2, 5, 10, 20, 50, 101, 104].

         
        -------- number of coins: 182 [1, 2, 1, 1, 2, 1, 2, 172]
        

        Like

      • Jim Randell's avatar

        Jim Randell 2:21 pm on 3 July 2024 Permalink | Reply

        I’ve just realised that [[ multiset.express() ]] already supports making an amount with a specific number of coins, so my code can be a bit more compact (and faster).

        Run: [ @replit ]

        from enigma import (irange, inf, multiset, printf)
        
        # coin denominations
        coins = (1, 2, 5, 10, 20, 50, 100, 200)
        
        # consider total value in pounds
        for n in irange(10, inf):
          done = 0
          # make a total of n pounds using exactly n coins
          tot = 100 * n
          for vs in multiset.from_seq(coins, count=n).express(tot, n):
            # can we make all other values between 1 and tot using these coins?
            if not vs.expressible(irange(1, tot - 1)): continue
            # output solution
            printf("{n} coins -> {vs}", vs=vs.map2str(arr='p x', sep='; '))
            done = 1
          if done: break
        

        Like

        • Frits's avatar

          Frits 2:55 pm on 3 July 2024 Permalink | Reply

          I know you said you were not doing any particular analysis but with this update the program is faster especially for more difficult cases.

          if not all(any(vs.express(x)) for x in irange(1, max(vs) - 1)): continue     
          

          Like

      • Jim Randell's avatar

        Jim Randell 8:43 am on 5 July 2024 Permalink | Reply

        The following approach builds up increasing sets of coins that can make change for all values from 1..n, by adding a coin to the previous sets. We can expand a set by adding another instance of the largest denomination in the set, or we can add a larger denomination coin only if we can already make all values lower than it.

        This program runs (under PyPy) in 125ms (internal runtime is 37ms).

        Run: [ @replit ]

        from enigma import (multiset, printf)
        
        # coin denominations (in pence)
        coins = (1, 2, 5, 10, 20, 50, 100, 200)
        
        # build up increasing sets of coins that cover range 1..n
        assert 1 in coins
        (ss, tgt, done) = ([(0, [])], 0, 0)
        while not done:
          tgt += 100
          ss_ = list()
          # consider current sets
          for (n, vs) in ss:
            m = (vs[-1] if vs else 0)
            # and try to add in another coin
            for d in coins:
              if d < m: continue
              if d - 1 > n: break
              (n_, vs_) = (n + d, vs + [d])
              if n_ == tgt:
                printf("{k} coins -> {vs}", k=len(vs_), vs=multiset.from_seq(vs_).map2str(arr="p x", sep="; "))
                done += 1
              ss_.append((n_, vs_))
          ss = ss_
        
        printf("[{done} solutions]")
        

        Note that with the following 16 coins we can make change for all values up to 1610p:

        1× 1p
        2× 2p
        1× 5p
        1× 10p
        2× 20p
        1× 50p
        1× 100p
        7× 200p
        total = £16.10 in 16 coins

        Like

      • Frits's avatar

        Frits 2:20 pm on 5 July 2024 Permalink | Reply

        Jim’s clever latest program can use a lot of memory and become quite slow for some denominations (like “pypy3 teaser2578.py 1 2 5 10 39 71 103 131”).
        His program suggested to me there might be a minimal number of pounds to have a solution. This minimal number of pounds can be used with his earlier program (unfortunately not with my program (of which I made a similar recursive version)).

        from enigma import (irange, inf, multiset, map2str, printf)
        from sys import argv
        
        # coin denominations (in pence)
        coins = ([1, 2, 5, 10, 20, 50, 100, 200] if len(argv) == 1 else
                 list(int(x) for x in argv[1:]))
        
        # build up increasing sets of coins that cover range 1..n
        assert 1 in coins
        coins.sort()
        
        print(f"{coins = }") 
        
        # determine the minimal number of pounds for which a solution is feasible
        (tgt, ss, done, strt) = (0, [(0, [])], 0, 10)
        while not done and tgt < 1000000:
          tgt += 100
          if tgt - ss[-1][0] <= coins[-1]:   # within reach
            if tgt > coins[-1]:  
              strt = tgt // 100
              print("minimal number of pounds:", strt)
              done = 1
              continue 
          
          ss_ = list()
          # consider current sets
          for (n, vs) in ss:
            m = (vs[-1] if vs else 0)
            # and try to add in another high coin
            for d in coins[::-1]:
              if d < m: continue     # not below minimum 
              # don't choose a coin higher than <n + 1>
              # otherwise number <n + 1> can never me made
              if d - 1 > n: continue
              ss_.append((n + d, vs + [d]))
              break # we have found the highest coin
          ss = ss_
          
        
        if strt > 26:  # this can be changed to a better value
          print("Jim 1")
          # consider total value in pounds
          for n in irange(strt, inf):
            done = 0
            # make a total of n pounds using exactly n coins
            tot = 100 * n
            for vs in multiset.from_seq(coins, count=n).express(tot, n):
              # can we make all other values between 1 and tot using these coins?
              #if not vs.expressible(irange(1, tot - 1)): continue
              if not vs.expressible(irange(1, max(vs) - 1)): continue
              # output solution
              printf("{n} coins -> {vs}", vs=vs.map2str(arr='p x', sep='; '))
              done = 1
            if done: break  
        else:
          print("Jim 2")
          (tgt, ss, done) = (0, [(0, [])], 0)
          while not done:
            tgt += 100
            ss_ = list()
            # consider current sets
            for (n, vs) in ss:
              m = (vs[-1] if vs else 0)
              # and try to add in another coin
              for d in coins:
                if d < m: continue
                if d - 1 > n: break
                (n_, vs_) = (n + d, vs + [d])
                if n_ == tgt:
                  printf("{k} coins -> {vs}", k=len(vs_), vs=multiset.from_seq(vs_).map2str(arr="p x", sep="; "))
                  done += 1
                ss_.append((n_, vs_))
            ss = ss_
           
          printf("[{done} solutions]")     
        

        Like

    • Frits's avatar

      Frits 1:25 pm on 1 July 2024 Permalink | Reply

      For this program I didn’t try to keep every line within 80 bytes (it is already a bit messy).
      I used a performance enhancement I don’t understand myself and I used break statements that I theoretically are not supposed to make. So far I have not encountered a case that differs from Jim’s program. The programs supports different coin denominations and also more than 8.

      With the performance enhancement the program runs in 21ms.

      from math import ceil
      from sys import argv
      
      # coin denominations
      coins = (1, 2, 5, 10, 20, 50, 100, 200) if len(argv) == 1 else list(int(x) for x in argv[1:])
      N = len(coins)
      M = 20      # maximum quantity
      vars = "abcdefghijklmnopqrstuxwxyz"
      print(f"{coins = }, {N = }")
      mn = 1e8    # minimum number of coins
      
      # make number <n> for specific deniminations and quantities
      def make(n, t, k, denoms, qs, ns=set(), ss=[]):
        if t == 0:
          yield ns | {n}
        if k :
          v, q = denoms[0], qs[0]
          ns.add(n - t)
          for i in range(min(q, t // v) + 1):
            yield from make(n, t - i * v, k - 1, denoms[1:], qs[1:], ns, ss + [i])
         
      # make all numbers from <strt> to <n>      
      def make_all(n, denoms, qs, strt=1):
        todo = set(i for i in range(strt, n + 1))  
        ln = len(denoms)
        while todo:
          n = max(todo)
          # make number <n>
          for made in make(n, n, ln, denoms, qs): 
            break
          else: return False  
          
          todo -= made
        return True
      
      # enhance performance (don't know why this is the case)
      if N < 9:
        _ = make_all(sum(qs := [2] * N) * 100, coins, qs)
        
      exprs = ""
      
      exprs += "mn, cnt = 1e8, 0\n\n"  
      exprs += "def check(lev, qs, tv, mn, make=1):\n"
      exprs += "  global cnt\n"  
      exprs += f"  if mn < 1e8:\n"
      exprs += "    # calculate minimum last quantity\n"  
      exprs += "    h_ = ceil((100 * mn - tv) / coins[-1])\n"  
      exprs += "    if sum(qs) + h_ > mn:\n"  
      exprs += "      return 'break'\n"  
      
      exprs += "  if make:\n"  
      exprs += "    n = coins[lev] - 1\n"  
      exprs += "    if tv < n: return False \n"  
      exprs += "    strt = 1 + coins[lev - 1] if lev > 1 else 1\n"  
      exprs += "    cnt += 1\n"  
      exprs += "    # can we make numbers <strt>...<n> with these denominations?\n"  
      exprs += "    if not make_all(n, coins[:lev], qs, strt):\n"  
      exprs += "      return False\n"
      exprs += "  return True\n\n"  
      
      exprs += f"make_{vars[0]} = 1\n"
      exprs += f"# can we make numbers 1..<total pence> with denominations {coins}?\n"
      
      last_odd = [i for i, x in enumerate(coins) if x % 2][-1]
      sols = dict()
      
      for i in range(1, N + 1):
        if i < N: 
          # our worst case scenario is if we have only one odd denomination and we check
          # all combinations with only one coin of 1 with no chance of achieving an 
          # even number of pence
          if last_odd == 0 and i == 1:
            exprs += f"{'  '* (i - 1)}for {vars[i - 1]} in range({coins[1] - (coins[1] % 2)}, {max(coins[1] - (coins[1] % 2) + 15, M) if i != N - 1 else 2 * M}, 2):\n"
          elif last_odd == i - 1 and i != N:
            exprs += f"{'  '* (i - 1)}for {vars[i - 1]} in range(prod_{vars[i - 2]} % 2, max(prod_{vars[i - 2]} % 2 + 15, M) if i != N - 1 else {2 * M}, 2):\n"  
          else:  
            exprs += f"{'  '* (i - 1)}for {vars[i - 1]} in range({coins[1] - 1 if i == 1 else 0}, {max(coins[1] + 14 if i == 1 else 0, M) if i != N - 1 else 2 * M}):\n"
            
          if i == N - 1: 
            exprs += f"{'  '* i}tot = sum(lst_{vars[i - 2]}) + {vars[i - 1]}\n"
            if coins[-2] != 100:
              exprs += f"{'  '* i}# calculate last quantity so that the total is a multiple of 100\n"
              exprs += f"{'  '* i}{(last := vars[i])}, rest = divmod(prod_{vars[i - 2]} + {vars[i - 1]} * {coins[i - 1]} - "
              exprs += f"100 * tot, 100 - coins[-1])\n"
              exprs += f"{'  '* i}if rest or {last} < 0: continue\n"  
          
          exprs += f"{'  '* i}# can we make numbers 1..{coins[i] - 1} with denominations {coins[:i]}\n"
          exprs += f"{'  '* i}if not (chk := check({i}, lst_{vars[i - 1]} := [{', '.join(vars[:i])}],\n"
          if i > 1:
            exprs += f"{'  '* i}                        prod_{vars[i - 1]} := prod_{vars[i - 2]} + {vars[i - 1]} * {coins[i - 1]},\n"
          else:
            exprs += f"{'  '* i}                        prod_{vars[i - 1]} := {vars[i - 1]} * {coins[i - 1]},\n"  
          exprs += f"{'  '* i}                        mn, make_{vars[i - 1]})): continue\n"
          exprs += f"{'  '* i}if chk == 'break': break\n"
          exprs += f"{'  '* i}make_{vars[i - 1]}, make_{vars[i]} = 0, 1   # don't try to make 1..x anymore in this loop\n"
        
        if i == N - 2 and coins[-2] == 100: 
          exprs += f"{'  '* i}# we can already calculate last quantity\n"
          exprs += f"{'  '* i}{(last := vars[N - 1])}, rest = divmod(prod_{vars[i - 1]} - 100 * sum(lst_{vars[i - 1]}), 100 - coins[-1])\n"
          exprs += f"{'  '* i}if rest or {last} < 0: continue\n"
          
        if i == N: 
          exprs += f"{'  '* (i - 1)}if tot + {last} > mn: break\n"
          exprs += f"{'  '* (i - 1)}mn = tot + {last}\n"
          exprs += f"{'  '* (i - 1)}sols[mn] = sols.get(mn, []) + [lst_{vars[i - 2]} + [{last}]]\n"
      
      exprs += "if mn not in sols:\n"   
      exprs += "  print('ERROR: no solutions, mn =', mn)\n"   
      exprs += "else:\n"   
      exprs += "  print(f'solutions: {len(sols[mn])}            {cnt} calls to make_all')\n"    
      exprs += "  for s in sols[mn]: print('-------- number of coins:', mn, s)\n"   
      
      #print(exprs)  
      exec(exprs) 
      

      The following code is generated:

      mn, cnt = 1e8, 0
      
      def check(lev, qs, tv, mn, make=1):
        global cnt
        if mn < 1e8:
          # calculate minimum last quantity
          h_ = ceil((100 * mn - tv) / coins[-1])
          if sum(qs) + h_ > mn:
            return 'break'
        if make:
          n = coins[lev] - 1
          if tv < n: return False
          strt = 1 + coins[lev - 1] if lev > 1 else 1
          cnt += 1
          # can we make numbers <strt>...<n> with these denominations?
          if not make_all(n, coins[:lev], qs, strt):
            return False
        return True
      
      make_a = 1
      # can we make numbers 1..<total pence> with denominations (1, 2, 5, 10, 20, 50, 100, 200)?
      for a in range(1, 20):
        # can we make numbers 1..1 with denominations (1,)
        if not (chk := check(1, lst_a := [a],
                                prod_a := a * 1,
                                mn, make_a)): continue
        if chk == 'break': break
        make_a, make_b = 0, 1   # don't try to make 1..x anymore in this loop
        for b in range(0, 20):
          # can we make numbers 1..4 with denominations (1, 2)
          if not (chk := check(2, lst_b := [a, b],
                                  prod_b := prod_a + b * 2,
                                  mn, make_b)): continue
          if chk == 'break': break
          make_b, make_c = 0, 1   # don't try to make 1..x anymore in this loop
          for c in range(prod_b % 2, max(prod_b % 2 + 15, M) if i != N - 1 else 40, 2):
            # can we make numbers 1..9 with denominations (1, 2, 5)
            if not (chk := check(3, lst_c := [a, b, c],
                                    prod_c := prod_b + c * 5,
                                    mn, make_c)): continue
            if chk == 'break': break
            make_c, make_d = 0, 1   # don't try to make 1..x anymore in this loop
            for d in range(0, 20):
              # can we make numbers 1..19 with denominations (1, 2, 5, 10)
              if not (chk := check(4, lst_d := [a, b, c, d],
                                      prod_d := prod_c + d * 10,
                                      mn, make_d)): continue
              if chk == 'break': break
              make_d, make_e = 0, 1   # don't try to make 1..x anymore in this loop
              for e in range(0, 20):
                # can we make numbers 1..49 with denominations (1, 2, 5, 10, 20)
                if not (chk := check(5, lst_e := [a, b, c, d, e],
                                        prod_e := prod_d + e * 20,
                                        mn, make_e)): continue
                if chk == 'break': break
                make_e, make_f = 0, 1   # don't try to make 1..x anymore in this loop
                for f in range(0, 20):
                  # can we make numbers 1..99 with denominations (1, 2, 5, 10, 20, 50)
                  if not (chk := check(6, lst_f := [a, b, c, d, e, f],
                                          prod_f := prod_e + f * 50,
                                          mn, make_f)): continue
                  if chk == 'break': break
                  make_f, make_g = 0, 1   # don't try to make 1..x anymore in this loop
                  # we can already calculate last quantity
                  h, rest = divmod(prod_f - 100 * sum(lst_f), 100 - coins[-1])
                  if rest or h < 0: continue
                  for g in range(0, 40):
                    tot = sum(lst_f) + g
                    # can we make numbers 1..199 with denominations (1, 2, 5, 10, 20, 50, 100)
                    if not (chk := check(7, lst_g := [a, b, c, d, e, f, g],
                                            prod_g := prod_f + g * 100,
                                            mn, make_g)): continue
                    if chk == 'break': break
                    make_g, make_h = 0, 1   # don't try to make 1..x anymore in this loop
                    if tot + h > mn: break
                    mn = tot + h
                    sols[mn] = sols.get(mn, []) + [lst_g + [h]]
      if mn not in sols:
        print('ERROR: no solutions, mn =', mn)
      else:
        print(f'solutions: {len(sols[mn])}            {cnt} calls to make_all')
        for s in sols[mn]: print('-------- number of coins:', mn, s)
      

      Like

      • Ruud's avatar

        Ruud 3:12 pm on 1 July 2024 Permalink | Reply

        You could generate exprs with:

        exprs = f"""\
        mn, cnt = 1e8, 0
        
        def check(lev, qs, tv, mn, make=1):
          global cnt
          if mn < 1e8:
            # calculate minimum last quantity
            h_ = ceil((100 * mn - tv) / coins[-1])
            if sum(qs) + h_ > mn:
              return 'break'
          if make:
            n = coins[lev] - 1
            if tv < n: return False
            strt = 1 + coins[lev - 1] if lev > 1 else 1
            cnt += 1
            # can we make numbers <strt>...<n> with these denominations?
            if not make_all(n, coins[:lev], qs, strt):
              return False
          return True
        
        make_{vars[0]} = 1
        # can we make numbers 1..<total pence> with denominations {coins}?
        """
        

        No change in functionality, just easier to understand and maintain.

        Like

        • Frits's avatar

          Frits 5:27 pm on 1 July 2024 Permalink | Reply

          Thanks, I had seen it before but had forgotten it.

          I also tried to use make() and make_all() in this way but had to use
          yield ns | {{n}}
          for my original line 15. I hoped in making these 2 functions local as well I wouldn’t need the performance enhancement but it is still lowers the run time.

          Like

      • Frits's avatar

        Frits 5:44 pm on 1 July 2024 Permalink | Reply

        Does anybody have an explanation why line 38 (in the first program) lowers the run time? Is it a memory thing and does it also occur on a linux system?

        Like

    • Frits's avatar

      Frits 9:26 pm on 18 July 2024 Permalink | Reply

      from sys import argv
      from math import ceil
      from time import perf_counter
      
      # coin denominations
      coins = [1, 2, 5, 10, 20, 50, 100, 200] if len(argv) == 1 else \
               list(int(x.replace(",", "", 1)) for x in argv[1:])
      coins.sort()
      N = len(coins)
      assert 1 in coins and coins[-1] > 100 and len(set(coins)) == N
      last_odd = [i for i, x in enumerate(coins) if x % 2][-1]
      mx_factor = max(divs := [ceil(coins[i + 1] / coins[i]) for i in range(N - 1)])
      print(f"{coins = }")
      
      # solve problem by adding coins of <N> different denominations  
      def solve(ds, k=0, tv=0, nc=0, qs=[]):
        global mn
        if k == N - 1:
          # we can calculate the quantity of the highest denomination
          last, r = divmod(tv - 100 * nc, 100 - ds[-1])
          if r or last < 0 or (nc_ := nc + last) > mn: return
          mn = nc_
          yield qs + [last]
        else:
          min_i = max(0, ceil((ds[k + 1] - tv - 1) / ds[k]))
          if k == last_odd:
            min_i += (tv + min_i) % 2
          
          remaining = 100 * mn - tv
          # minimum elements to follow = ceil((remaining - (i * ds[k])) / ds[-1])
          # nc + i + minimum elements to follow <= mn 
          max_i = min((ds[-1] * (mn - nc) - remaining) // (ds[-1] - ds[k]),
                  mn - nc,
                  # do not make a total which is higher than the minimum
                  remaining // ds[k])
          
          if max_i < min_i: 
            return
            
          if k < N - 2:
            # with <j - 1> quantities can we still make numbers up to ds[j] - 1
            # if we choose <i> of ds[k]    (for j >= k + 2)
            # tv + i * ds[k] + (mn - nc - i) * ds[j - 1] >= ds[j] - 1
            mx1 = min(((mn - nc) * ds[j - 1] + tv - c + 1) // (ds[j - 1] - ds[k])
                    for j, c in enumerate(ds[k + 2:], k + 2))
            max_i = min(max_i, mx1)
          
          for i in range(min_i, max_i + 1, 1 if k != last_odd else 2):
            yield from solve(ds, k + 1, tv + i * ds[k], nc + i, qs + [i])
          
      sols = dict()
      mn = mx_factor + 27 # solving approx 80% of all cases in one pass 
      inc = 15
      cnt = 0
      while not sols:
        print(f"try {mn = }")
        M = mn
        for s in solve(coins):
          cnt += 1
          # limit the number of printed solutions (causing slowness due to memory)
          if cnt > 999 and mn in sols: continue    
          if mn not in sols: 
            sols = dict()
            cnt = 1
          sols[mn] = sols.get(mn, []) + [s]
           
        if not sols:
          mn += inc  
      
      for s in sols[mn]: print('-------- number of coins:', mn, s)  
      print(f'[{cnt:>5} solutions]')     
      

      solving 1000 random testcases in approximately 200 seconds.

      from random import sample
      from os import system
      from sys import argv, stdout, stderr
      
      N = 8
      
      c = 0
      for _, _ in enumerate(iter(bool, 1), 1): # infinite loop     
        c += 1
        if c == 1001: break
        lst = [1] + sorted(sample(range(2, 2001), N - 1))
        if lst[-1] < 101: continue
        
        lst = ' '.join(str(x) for x in lst)
        print(f"{c = } {lst = }", file=stderr)
        system("python TheRightMoney.py " + lst)
      

      Like

  • Unknown's avatar

    Jim Randell 4:28 pm on 21 June 2024 Permalink | Reply
    Tags:   

    Teaser 3222: Squarism 

    From The Sunday Times, 23rd June 2024 [link] [link]

    There are 84 marbles, numbered sequentially from 1, in a bag. Oliver (going first) and Pam take turns, each removing two marbles from the bag. The player finds the “most square” integer factorisation of each number. The difference between the two factors for each number is then added to the player’s score (e.g. removing marbles 1 and 84 scores 5 points since 1 = 1 × 1 and 84 = 7 × 12), and the game ends when all marbles are removed.

    After each of the first four turns (two each), both players’ expected final score was a whole number. For example, if there were 90 points remaining, with 4 turns left for Oliver and 5 for Pam, Oliver would expect to score another 40 points. In addition, each player’s score for their second turn was the same as their first. Also, Pamela scored the lowest possible game total.

    What was Oliver’s expected final score after Pamela’s second turn?

    [teaser3222]

     
    • Jim Randell's avatar

      Jim Randell 5:27 pm on 21 June 2024 Permalink | Reply

      This Python program looks for possible repeated scores for O and P that could happen in the first 4 turns, that give the required whole number expectations, to give candidate solutions.

      Once candidates are found (there are only two) it then attempts to choose marble values for the turns that give the required score, and any non-viable candidates (one of them) are eliminated.

      It runs in 71ms. (Internal runtime is 656µs).

      from enigma import (irange, isqrt, div, ediv, group, subsets, disjoint_cproduct, disjoint_union, printf)
      
      # calculate the score for a marble
      def score(n):
        for a in irange(isqrt(n), 1, step=-1):
          b = div(n, a)
          if b is not None: return abs(a - b)
      
      # group the marbles by score
      g = group(irange(1, 84), by=score)
      
      # calculate total number of points
      tot = sum(k * len(vs) for (k, vs) in g.items())
      
      # find possible scores available to O and P
      (keysO, keysP) = (list(), list())
      t = 0
      for k in sorted(g.keys()):
        n = len(g[k])
        t += n
        if not (t < 42):
          keysO.append(k)
        if not (t > n + 42):
          keysP.append(k)
      
      # construct example scores for O and P
      def choose(OPs, i=0, ss=list(), used=set()):
        # are we done?
        if not OPs:
          yield ss
        else:
          # choose values for the next score
          s = OPs[0]
          for vs in subsets([keysO, keysP][i], size=2, select='R'):
            if not (sum(vs) == s): continue
            # choose marbles
            for xs in disjoint_cproduct(g[v] for v in vs):
              used_ = disjoint_union([used, xs])
              if used_ is not None:
                yield from choose(OPs[1:], 1 - i, ss + [xs], used_)
      
      # possible scores for O and P
      scoresO = set(sum(ks) for ks in subsets(keysO, size=2, select='R'))
      scoresP = set(sum(ks) for ks in subsets(keysP, size=2, select='R'))
      
      # check expected remaining points
      def check_exp(r, *fs):
        try:
          return list(ediv(r * a, b) for (a, b) in fs)
        except ValueError:
          return None
      
      # consider scores for O's first 2 turns
      for sO in scoresO:
        # expected remaining points after turn 1 (41 remaining turns)
        if not check_exp(tot - sO, (20, 41), (21, 41)): continue
      
        # consider scores for P's first 2 turns
        for sP in scoresP:
          # expected remaining points after turn 2 (40 remaining turns)
          if not check_exp(tot - (sO + sP), (20, 40)): continue
          # expected remaining points after turn 3 (39 remaining turns)
          if not check_exp(tot - (sO + sP + sO), (19, 39), (20, 39)): continue
          # expected remaining points after turn 4 (38 remaining turns)
          es = check_exp(tot - (sO + sP + sO + sP), (19, 38))
          if not es: continue
      
          # check scores can be made twice
          if not any(choose([sO, sP] * 2)): continue
      
          # output solution candidate
          printf("expected total scores: O={tO} P={tP} [sO={sO} sP={sP}]", tO=sO + sO + es[0], tP=sP + sP + es[0])
      

      Solution: Oliver’s expected final score after Pam takes her second turn is 685.

      There are only two candidate turn scores that give the required whole number expectations:

      O scores 52 on turns 1 and 3
      P scores 8 on turns 2 and 4

      and:

      O scores 134 on turns 1 and 3
      P scores 0 on turns 2 and 4

      However a score of 134 can only be made by choosing marbles 53 (score = 52) and 83 (score = 82), so this cannot be repeated in the third turn. Hence the second candidate solution is eliminated.

      But there are many ways to make the scores for the first candidate solution, for example:

      O picks (7, 47); score = 6 + 46 = 52
      → 1230 points remaining; O’s expected total = 652; P’s expected total = 630.

      P picks (3, 27); score = 2 + 6 = 8
      → 1222 points remaining; O’s expected total = 663; P’s expected total = 619.

      O picks (11, 43); score = 10 + 42 = 52
      → 1170 points remaining; O’s expected total = 674; P’s expected total = 608.

      P picks (8, 55); score = 2 + 6 = 8
      → 1162 points remaining; O’s expected total = 685; P’s expected total = 597.

      In fact, when the game ends, P has scored the lowest possible total, which is 92 points, and so O has scored the remaining 1190 points.

      Like

      • Jim Randell's avatar

        Jim Randell 12:59 pm on 22 June 2024 Permalink | Reply

        We can simplify this approach by just keeping a multiset (or bag(!)) of the possible scores, and then we don’t need to worry about the actual marbles chosen at all.

        Run: [ @replit ]

        from enigma import (irange, isqrt, div, ediv, group, multiset, first, subsets, cproduct, call, printf)
        
        # calculate the score for a marble
        def score(n):
          for a in irange(isqrt(n), 1, step=-1):
            b = div(n, a)
            if b is not None: return abs(a - b)
        
        # make a multiset of scores
        scores = multiset.from_seq(score(x) for x in irange(1, 84))
        
        # calculate total number of points
        tot = scores.sum()
        
        # scores for P and O (P has the lowest 42)
        scoresP = multiset.from_seq(first(scores.sorted(), 42))
        scoresO = scores.difference(scoresP)
        
        # possible total scores for O and P in a turn
        turnsO = group(scoresO.subsets(size=2), by=sum)
        turnsP = group(scoresP.subsets(size=2), by=sum)
        
        # check expected remaining points
        def check_exp(r, *fs):
          try:
            return list(ediv(r * a, b) for (a, b) in fs)
          except ValueError:
            return None
        
        # check scores can be made twice
        def check_scores(sO, sP):
          for (ssO, ssP) in cproduct(subsets(xs, size=2, select='R') for xs in [turnsO[sO], turnsP[sP]]):
            if call(multiset.from_dict, ssO + ssP).issubset(scores):
              return True
        
        # consider scores for O's first 2 turns
        for sO in turnsO:
          # expected remaining points after turn 1 (41 remaining turns)
          if not check_exp(tot - sO, (20, 41), (21, 41)): continue
        
          # consider scores for P's first 2 turns
          for sP in turnsP:
            # expected remaining points after turn 2 (40 remaining turns)
            if not check_exp(tot - (sO + sP), (20, 40)): continue
            # expected remaining points after turn 3 (39 remaining turns)
            if not check_exp(tot - (sO + sP + sO), (19, 39), (20, 39)): continue
            # expected remaining points after turn 4 (38 remaining turns)
            es = check_exp(tot - (sO + sP + sO + sP), (19, 38))
            if not es: continue
        
            # check the turn scores can be made twice
            if not check_scores(sO, sP): continue
        
            # output solution candidate
            printf("expected total scores: O={tO} P={tP} [sO={sO} sP={sP}]", tO=sO + sO + es[0], tP=sP + sP + es[0])
        

        Like

      • Frits's avatar

        Frits 11:35 am on 23 June 2024 Permalink | Reply

        Just for fun:

        g = {i: set() for i in range(84)}
        for a in range(1, 85):
          for b in range(a, 0, -1):
            if a * b > 84 or any(a * b in g[i] for i in range(a - b)): continue
            g[a - b].add(a * b)
        g = {k: vs for k, vs in g.items() if vs}    # remove empty items
        

        @Jim, you don’t use the “ss” list so you could just yield True “if not OPs”.

        Like

    • Frits's avatar

      Frits 8:40 pm on 21 June 2024 Permalink | Reply

      There must be a smarter way of getting this solution.
      The program runs in 350ms (PyPy).

       
      from enigma import divisor_pairs
      from itertools import combinations
      
      # determine differences for "most square" integer factorisation
      seq = [sorted(b - a for a, b in divisor_pairs(i))[0] for i in range(1, 85)]
      # total of all scores
      T = sum(seq)
      
      P, O = [], []
      # determine the lowest scores for Pam
      for i, s, in enumerate(sorted(seq), 1):
        if i <= 42:
          P.append(s)
        else:  
          O.append(s)
      
      sols = set()
      # first turn for Oliver
      for o12 in combinations(O, 2):
        # 20/41 * remaining is a whole number
        if (T - (sum_o12 := o12[0] + o12[1])) % 41: continue
        # first turn for Pam
        for p12 in combinations(P, 2):
          # 20/40 * remaining is a whole number
          if (T - (sum_p12 := p12[0] + p12[1]) - sum_o12) % 2: continue
          O_ = O.copy()
          for o in o12:
            O_.remove(o)
          # second turn for Oliver  
          for o34 in combinations(O_, 2):
            # 19/39 * remaining is a whole number
            if (T - (sum_o34 := o34[0] + o34[1]) - sum_o12 - sum_p12) % 39: continue
            # score for their second turn was the same as their first
            if sum_o34 != sum_o12: continue
            
            P_ = P.copy()
            for p in p12:
              P_.remove(p)
            for p34 in combinations(P_, 2):
              remaining = (T - (sum_p34 := p34[0] + p34[1]) - 
                           sum_o12 - sum_p12 - sum_o34)
              # 19/38 * remaining is a whole number
              # score for their second turn was the same as their first
              if remaining % 2 or sum_p34 != sum_p12: continue
              # Oliver's expected final score after Pamela’s second turn
              sols.add(str(sum_o12 + sum_o34 + remaining // 2))
      
      print("answer:", ' or '.join(sols))    
      

      Like

      • Frits's avatar

        Frits 2:16 pm on 22 June 2024 Permalink | Reply

        An easy performance improvement is to use keep2() in the combinations() lines.

        # keep only a maximum of 2 occurences of a sequence item
        keep2 = lambda seq: [b for a in [[x] * min(2, seq.count(x)) 
                             for x in sorted(set(seq))] for b in a]     
        

        Like

    • Frits's avatar

      Frits 7:19 am on 24 June 2024 Permalink | Reply

      from itertools import combinations
       
      seq = []
      for n in range(1, 85):
        # find the two factors nearest to a square
        for a in range(int(n**.5), 0, -1):
          b, r = divmod(n, a)
          if r: continue
          seq.append(b - a) 
          break            
       
      seq.sort()
      T = sum(seq)                 # total of all scores
      assert T % 2 == 0            # whole number expected score after 4th turn
      P, O = seq[:42], seq[42:]    # points for Pam and Oliver
       
      # sum of points of 2 marbles for Oliver that guarantees divisibility by 41
      o_sums = [sm for i in range(5) 
                if sum(O[:2]) <= (sm := (T % 41) + 41 * i) <= sum(O[-2:])]
       
      # indices of 2 marbles for Oliver that guarantees divisibility by 41
      o_pairs = {sm: [(i1, i1 + i2 + 1) for i1, m1 in enumerate(O[:-1]) 
                 for i2, m2 in enumerate(O[i1 + 1:]) if m1 + m2 == sm] 
                 for sm in o_sums}
                 
      # reduce items if possible           
      o_pairs = {k: vs for k, vs in o_pairs.items() if len(vs) > 1}      
      o_sums = [sm for sm in o_sums if sm in o_pairs]    
       
      # valid Oliver/Pam points that also guarantees divisibility by 39
      # after the third turn and divisibility by 2 after the second turn
      op_pts = [(o_pts, p_pts) for o_pts in o_sums 
                if (T - o_pts - (p_pts := (T - 2 * o_pts) % 39)) % 2 == 0]
       
      sols = set()
      # can solutions be formed by picking 4 different marbles per person
      for o, p in op_pts:
        # pick 4 marbles for Oliver
        if not any(len(set(a + b)) == 4 for a, b in combinations(o_pairs[o], 2)):
          continue
         
        # indices of 2 marbles for Pam
        p_pairs = [(i1, i1 + i2 + 1) for i1, m1 in enumerate(P[:-1]) 
                    for i2, m2 in enumerate(P[i1 + 1:]) if m1 + m2 == p]
        
        # pick 4 marbles for Pam
        if not any(len(set(a + b)) == 4 for a, b in combinations(p_pairs, 2)):
          continue
        
        # store Oliver's expected final score after Pamela's second turn
        sols.add(str(T // 2 + o - p))
       
      print("answer:", ' or '.join(sols))       
      

      Like

  • Unknown's avatar

    Jim Randell 8:19 am on 18 June 2024 Permalink | Reply
    Tags:   

    Brain-Teaser 339: Cross country 

    From The Sunday Times, 5th November 1967 [link]

    Tom, Dick and Harry had come up the school together and in three successive years had competed in the Junior, Colts and Senior, cross-country races.

    Every year they finished in the same positions but interchanged so that no boy came in the same position twice. The same applied to the numbers on their vests, all six numbers being different, odd, and greater than 1.

    When each boy multiplied his position number by the number he was wearing at the time, the nine results were all different and together totalled 841.

    Dick beat the other two in the Junior race, but the number he was wearing was smaller than his position number; but he was wearing the highest card number in the Senior race, and this was [also] smaller than his position number.

    Harry’s three products had a sum smaller than that of either of the other two.

    What were the three boys’ positions in the Colts race and what numbers were they wearing?

    This puzzle is included in the book Sunday Times Brain Teasers (1974). The puzzle text is taken from the book.

    [teaser339]

     
    • Jim Randell's avatar

      Jim Randell 8:20 am on 18 June 2024 Permalink | Reply

      Let the finishing positions be (best to worst) (a, b, c), and the runner numbers be (smallest to largest) (x, y, z).

      These six numbers are all different, odd, and greater than 1.

      Taking the cartesian product of these sets we get:

      {a, b, c} × {x, y, z} = {ax, ay, az, bx, by, bz, cx, cy, cz}

      And for each boy in each race the product of his runner number and his finishing position gives a unique value, so they correspond directly with the 9 values produced by this cartesian product.

      The following Python program considers divisor pairs of 841, and then splits each divisor into 3 odd numbers meeting the requirements. We then use the [[ SubstitutedExpression() ]] solver from the enigma.py library to assign runner numbers and finishing positions in each race.

      The program runs in 116ms. (Internal runtime is 38ms).

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, divisors_pairs, decompose, div, cproduct, sprintf as f, join, printf)
      
      # find 3 odd numbers that give the required total
      def numbers(t):
        r = div(t - 3, 2)
        if r:
          for (a, b, c) in decompose(r, 3, min_v=1, sep=1):
            yield (2 * a + 1, 2 * b + 1, 2 * c + 1)
      
      # solve for positions <pos>, numbers <num>
      def solve(pos, num):
        # let the positions and numbers in the races be:
        #           pos   | num
        #           T D H | T D H
        #  junior:  A B C | D E F
        #  colt:    G H I | J K L
        #  senior:  M N P | Q R S
      
        # an expression to calculate the sum of products
        def fn(ps, ns):
          ps = join((f("pos[{x}]") for x in ps), sep=", ", enc="[]")
          ns = join((f("num[{x}]") for x in ns), sep=", ", enc="[]")
          return f("dot({ps}, {ns})")
        # construct an alphametic puzzle
        exprs = [
          # "D beat T and H in the junior race ..."
          "pos[B] < pos[A]",
          "pos[B] < pos[C]",
          # "... but his runner number was less than his position number"
          "num[E] < pos[B]",
          # "D has the largest runner number in the senior race ..."
          "num[R] > num[Q]",
          "num[R] > num[S]",
          # "... but his runner number was less than his position number"
          "num[R] < pos[N]",
          # "the sum of H's products were smaller than T's or D's"
          f("{H} < min({T}, {D})", T=fn("AGM", "DJQ"), D=fn("BHN", "EKR"), H=fn("CIP", "FLS")),
        ]
        p = SubstitutedExpression(
          exprs,
          base=3, # values are (0, 1, 2)
          distinct="ABC DEF GHI JKL MNP QRS AGM BHN CIP DJQ EKR FLS".split(),
          env=dict(pos=pos, num=num),
        )
        # solve the alphametic
        for s in p.solve(verbose=""):
          # output solution
          for (t, ps, ns) in zip(["junior", "colt  ", "senior"], ["ABC", "GHI", "MNP"], ["DEF", "JKL", "QRS"]):
            ((pT, pD, pH), (nT, nD, nH)) = ((pos[s[k]] for k in ps), (num[s[k]] for k in ns))
            printf("{t}: T (#{nT}) = {pT}; D (#{nD}) = {pD}; H (#{nH}) = {pH}")
      
      # consider the values of (a + b + c) * (x + y + z)
      for (p, q) in divisors_pairs(841, every=1):
        if p < 15 or q < 15: continue
        for (abc, xyz) in cproduct([numbers(p), numbers(q)]):
          # all numbers and products are different
          if len(set(abc + xyz)) != 6: continue
          if len(set(i * j for (i, j) in cproduct([abc, xyz]))) != 9: continue
          # solve the puzzle
          solve(abc, xyz)
      

      Solution: In the Colts race Tom (#15) finished 5th; Dick (#11) finished 7th; Harry (#3) finished 17th.

      The full results are:

      Junior: Tom (#11) = 17th; Dick (#3) = 5th; Harry (#15) = 7th
      Colts: Tom (#15) = 5th; Dick (#11) = 7th; Harry (#3) = 17th
      Senior: Tom (#3) = 7th; Dick (#15) = 17th; Harry (#11) = 5th

      In each race the runner numbers are #3, #11, #15 and the positions are 5th, 7th, 17th.

      And the sum of the nine different products of these numbers is:

      (3 + 11 + 15) × (5 + 7 + 17) = 29 × 29 = 841

      In fact the only viable factorisation of 841 is 29 × 29.

      Like

  • Unknown's avatar

    Jim Randell 8:59 am on 16 June 2024 Permalink | Reply
    Tags: by: J R Wowk   

    Brain-Teaser 959: A question of age 

    From The Sunday Times, 7th December 1980 [link]

    The remarkable Jones family has a living male member, each directly descended, in five generations. At a recent reunion Ben, the genius of the family, made the following observations. After much deliberation, the rest of the Joneses agreed that these facts were indeed true.

    Ben began: “If Cy’s great grandfather’s age is divided by Cy’s own age, it gives an even whole number, which, if doubled, gives Dan’s grandson’s age”.

    “Eddy’s father’s age and his own have the same last digit. My great grandfather’s age is an exact multiple of my own, and if one year is taken away from that multiple it gives the age of my son”.

    “Adam’s age, divided by that of his grandson, gives an even whole number which, when doubled, is his son’s age reversed. Furthermore, this figure is less than his grandfather’s age”.

    All fathers were 18 or above when their children were born. Nobody has yet reached the age of 100.

    Give the ages of the five named people in ascending order.

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

    [teaser959]

     
    • Jim Randell's avatar

      Jim Randell 9:01 am on 16 June 2024 Permalink | Reply

      The following Python program constructs a collection of alphametic expressions that give a viable puzzle, and then uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to solve them.

      It runs in 156ms.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, subsets, peek, sprintf as f, rev, seq2str, printf)
      
      # choose ordering 0 = youngest to 4 = eldest
      for (A, B, C, D, E) in subsets((0, 1, 2, 3, 4), size=len, select='P'):
      
        # construct a SubstitutedExpression recipe
        # symbols used for the ages:
        sym = lambda k, vs=['AB', 'CD', 'EF', 'GH', 'IJ']: peek(vs, k)
        try:
          exprs = [
            # "C's great-grandfather's age (C + 3) divided by C's age is an even whole number ..."
            f("div({x}, 2 * {y})", x=sym(C + 3), y=sym(C)),
            # "... which, if doubled, gives D's grandson's age (D - 2)"
            f("div(2 * {x}, {y}) = {z}", x=sym(C + 3), y=sym(C), z=sym(D - 2)),
            # "E's fathers age (E + 1) and E's age have the same last digit"
            f("{x} = {y}", x=sym(E + 1)[-1], y=sym(E)[-1]),
            # "B's great-grandfather's age (B + 3) is an exact multiple of B's ..."
            # "... and this multiple less 1 gives the age of B's son (B - 1)"
            f("div({x}, {y}) - 1 = {z}", x=sym(B + 3), y=sym(B), z=sym(B - 1)),
            # "A's age divided by the age of A's grandson (A - 2) is an even whole number ..."
            f("div({x}, 2 * {y})", x=sym(A), y=sym(A - 2)),
            # "... which, when doubled, is A's son's age (A - 1) reversed"
            f("div(2 * {x}, {y}) = {z}", x=sym(A), y=sym(A - 2), z=rev(sym(A - 1))),
            # "... and this value is less than A's grandfather's age (A + 2)"
            f("{z} < {x}", z=rev(sym(A - 1)), x=sym(A + 2)),
          ]
        except IndexError:
          continue
        # generation gap is at least 18
        exprs.extend(f("{y} - {x} >= 18", x=sym(i), y=sym(i + 1)) for i in (0, 1, 2, 3))
      
        # solve the puzzle
        p = SubstitutedExpression(exprs, distinct="", d2i={})
        for s in p.solve(verbose=""):
          # output ages
          ss = list(p.substitute(s, sym(i)) for i in (0, 1, 2, 3, 4))
          printf("ages = {ss} [A={A} B={B} C={C} D={D} E={E}]", ss=seq2str(ss), A=ss[A], B=ss[B], C=ss[C], D=ss[D], E=ss[E])
      

      Solution: The ages are: 3, 23, 48, 72, 92.

      Corresponding to:

      Cy = 3
      Ben = 23
      Adam = 48
      Eddy = 72
      Dan = 92

      And the given facts are:

      Cy’s great-grandfather = Eddy = 72
      divided by Cy = 3
      gives 72/3 = 24, an even whole number
      when doubled = 48 = Adam = Dan’s grandson.

      Eddy’s father = Dan = 92
      Eddy = 72, they have the same final digit.
      Ben’s great-grandfather = Dan = 92
      is 4 times Ben = 23
      and 4 − 1 = 3 = Cy = Ben’s son.

      Adam = 48
      divided by Adam’s grandson = Cy = 3
      gives 48/3 = 16, an even whole number
      when doubled = 32,
      reverse of Adam’s son = Ben = 23 is also 32.
      Furthermore 32 is less than Adam’s grandfather = Dan = 92.

      It turns out that the facts lead to a single possible ordering (youngest to eldest), which is: (C, B, A, E, D), so we only evaluate a single alphametic puzzle.

      Like

    • Lise Andreasen's avatar

      Lise Andreasen 4:00 pm on 16 June 2024 Permalink | Reply

      h - i denotes h is the father of i.

      (1) x - ? - ? - c
      (2) x/c = 2k, k integer.
      (3) d - ? - y
      (4) y = 4k
      (5) z - e
      (6) z and e have the same last digit.
      (7) u - ? - ? - b - v
      (8) u = bl, l integer.
      (9) v = l minus 1
      (10) s - ? - a - r - w
      (11) a/w = 2m, m integer.
      (12) 4m is r reversed
      (13) 4m < s
      (14) d < 100
      (15) h minus i >= 18
      (16) Youngest >= 1 - assumption

      (7) and (10):
      ? - ? - a - b - ?

      Combine with (1):
      ? - ? - a - b - c

      Combine with (5):
      ? - e - a - b - c

      Only d is left:
      d - e - a - b - c

      As d and b are 3 generations apart:
      (17) d minus b >= 3 * 18 = 54

      (15) and (16):
      (18) b >= 19

      (8), (18) and (14):
      d = b * l
      76 = 19 * 4
      95 = 19 * 5
      80 = 20 * 4
      84 = 21 * 4
      88 = 22 * 4
      92 = 23 * 4
      96 = 24 * 4

      As (15) and (9), we can throw away solutions, where b minus l plus 1 < 18.
      That's 76, 95 and 80.

      4m is b reversed.
      b reversed is 12, 22, 23 and 42.
      As 4 should divide this, throw away 22 and 42.
      That is, throw away 88 and 96.

      d = b * l, 4m, m
      84 = 21 * 4, 12, 3
      92 = 23 * 4, 32, 8

      As (11), a/c= 2m.
      Further, (9), c = 3
      So a is 6m, either 18 or 48.
      18 < 21, won't work.

      As (4):
      k = 12

      As (2):
      e/3 = 2 * 12
      e = 72

      So it has to be:

      d - e - a - b - c
      92 - 72 - 48 - 23 - 3

      Like

    • Frits's avatar

      Frits 2:23 pm on 18 June 2024 Permalink | Reply

      Python program to describe a manual solution.

      '''
      GGF(C) / C = even   (Ia), 2 * even = GS(D)           (Ib)
      F(E) and E have the same last digit                  (II)
      GGF(B) = k * B    (IIIa), S(B) = k - 1               (IIIb)
      A / GS(A) = even   (IVa), 2 * even = reversed(S(A))  (IVb)
      reversed(S(A)) < GF(A)                               (IVc) 
      generation gap is at least 18                        (V) 
      '''
      
      # hierarchy [S, F, GF, GGF, GGGF]
      hier = [set("ABCDE") for _ in range(5)]
      
      # determine order
      for i in range(2, 5): hier[i].remove('C')                 # (Ia)
      for i in range(5): hier[i].discard('A'); hier[2] = {"A"}  # (IVa and IVc)
      for i in range(5): hier[i].discard('B'); hier[1] = {"B"}  # (IIIa and IIIb)
      hier[0] = {"C"}   # (Ia and hier[1] = B)
      for i in [0, 1, 2, 4]: hier[i].discard('E');              # (II --> no 4)
      hier[3] = {"E"}   # only place left for E
      hier[4] = {"D"}   # what remains 
      
      # D = k * B  (as GGF(B) = D), D >= 72, k = D / B > 2 as D - B >= 54
      range_k = {i for i in range(3, 99 // 18 + 1)}
      
      # as rev(B) is even (IVb) B must be 2. or 4. so k can't be 5
      range_k -= {5} 
      # E / C = A / 2  (Ia and Ib), so C can't be 2 and thus k can't be 3 (IIIb)
      range_k -= {3} 
      k = next(iter(range_k))                   # only one value remains
      C = k - 1                                 # IIIb
      
      # A is a multiple of 3 (IVa) and of 4 (Ib) thus also a multiple of 12
      range_A = [a for a in range(C + 2 * 18, 100 - 38) if a % 12 == 0]
      for A in range_A:
        E = C * A // 2                          # (Ia and Ib)
        for D in range(E + 20, 100, 10):
          B, r = divmod(D, k)                   # (IIIa)
          if r or not(C + 18 <= B <= A - 18): continue
          revB = int(str(B)[::-1])
          if revB != (2 * A) // C: continue     # (IVa and IVb)
          print("answer:", sorted([A, B, C, D, E]))     
      

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 14 June 2024 Permalink | Reply
    Tags:   

    Teaser 3221: Faulty towers 

    From The Sunday Times, 16th June 2024 [link] [link]

    The famous “Tower of Hanoi” puzzle consists of a number of different-sized rings in a stack on a pole with no ring above a smaller one. There are two other poles on which they can be stacked and the puzzle is to move one ring at a time to another pole (with no ring ever above a smaller one) and to end up with the entire stack moved to another pole.

    Unfortunately I have a faulty version of this puzzle in which two of the rings are of equal size. The minimum number of moves required to complete my puzzle (the two equal rings may end up in either order) consists of four different digits in increasing order.

    What is the minimum number of moves required?

    [teaser3221]

     
    • Jim Randell's avatar

      Jim Randell 4:52 pm on 14 June 2024 Permalink | Reply

      See also: Teaser 1858 and Enigma 14.

      I re-used the H() function from Teaser 1858 to calculate the number of moves required for a configuration of discs.

      This Python program runs in 65ms. (Internal runtime is 421us).

      Run: [ @replit ]

      from enigma import (Accumulator, irange, inf, nsplit, is_increasing, printf)
      
      # number of moves for sequence of discs (largest to smallest)
      def H(ns):
        t = 0
        m = 1
        for n in ns:
          t += m * n
          m *= 2
        return t
      
      # consider numbers of different size discs
      for n in irange(1, inf):
        r = Accumulator(fn=min)
        # duplicate one of the discs
        for i in irange(n):
          ns = [1] * n
          ns[i] += 1
          # calculate the number of moves
          t = H(ns)
          # answer is a 4 digit number consisting of strictly increasing digits
          if 999 < t < 10000 and is_increasing(nsplit(t), strict=1):
            printf("{ns} -> {t}")
          r.accumulate(t)
        if r.value >= 6789: break
      

      Solution: The minimum number of moves required is 1279.

      The arrangement of discs is:

      There are 11 discs (of 10 different sizes) and discs 9 and 10 (counting from the bottom) are the same size.

      So the number of moves is:

      1×1 + 1×2 + 1×4 + 1×8 + 1×16 + 1×32 + 1×64 + 1×128 + 2×256 + 1×512 = 1279


      With analysis:

      The number of moves for a standard “Tower of Hanoi”, is the sum of the powers of 2 corresponding to each disc.

      So for a tower of 4 discs we would have:

      1 + 2 + 4 + 8 = 15 moves

      When one of the discs is duplicated we also duplicate one of the powers of 2, and this gives us a 4-digit increasing number (the smallest is 1234, and the largest 6789).

      So our starting tower must have between 10 and 12 different sizes.

      Summing the first n powers of 2:

      n=10: sum(1..512) = 1023
      n=11: sum(1..1024) = 2047
      n=12: sum(1..2048) = 4095

      And we need to duplicate one of the powers of 2 to give a number with (strictly) increasing digits:

      1023 + 256 = 1279
      2047 + 512 = 2559 (increasing, but not strictly increasing)

      Like

  • Unknown's avatar

    Jim Randell 5:08 pm on 13 June 2024 Permalink | Reply
    Tags:   

    Teaser 2577: Number waves 

    From The Sunday Times, 12th February 2012 [link] [link]

    I challenged Sam to find a 9-digit number using all of the digits 1 to 9, such that each digit occupying an even position was equal to the sum of its adjacent digits. (For example, the digit occupying position four would be equal to the sum of the digits occupying positions three and five). Sam produced a solution that was exactly divisible by its first digit, by its last digit, and by 11.

    What was his number?

    [teaser2577]

     
    • Jim Randell's avatar

      Jim Randell 5:08 pm on 13 June 2024 Permalink | Reply

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

      It runs in 77ms. (Internal runtime of the generated program is 230µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # suppose the number is: ABCDEFGHI
      #                  pos = 123456789
      
      SubstitutedExpression
      
      --digits="1-9"
      
      # even positions are the sum of the adjacent digits
      "A + C = B"
      "C + E = D"
      "E + G = F"
      "G + I = H"
      
      # solution is divisible by A, I and 11
      "ABCDEFGHI % A = 0"
      "ABCDEFGHI % I = 0"
      "ABCDEFGHI % 11 = 0"
      
      --answer="ABCDEFGHI"
      

      Solution: Sam’s number was: 143968275.

      Like

      • Ruud's avatar

        Ruud 7:49 pm on 15 June 2024 Permalink | Reply

        No a priori knowledge, just brute force:

        from istr import istr
        
        for i in istr.concat(istr.permutations(istr.digits("1-"), 9)):
            if (
                i[1] == i[0] + i[2]
                and i[3] == i[2] + i[4]
                and i[5] == i[4] + i[6]
                and i[7] == i[6] + i[8]
                and i.divisible_by(11)
                and i.divisible_by(i[0])
                and i.divisible_by(i[8])
            ):
                print(i)
        
        

        Note that this requires the latest-soon to be published- version of istr.

        Like

    • GeoffR's avatar

      GeoffR 6:56 pm on 13 June 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:a; var 1..9:b; var 1..9:c;
      var 1..9:d; var 1..9:e; var 1..9:f;
      var 1..9:g; var 1..9:h; var 1..9:i;
      
      constraint all_different([a,b,c,d,e,f,g,h,i]);
      
      var 123456789..987654321:N == a*pow(10,8) + b*pow(10,7) + c*pow(10,6) 
      + d*pow(10,5) + e*pow(10,4) + f*pow(10,3) + g*pow(10,2) + h*pow(10,1) + i;
       
      constraint N mod a == 0;
      constraint N mod i == 0;
      constraint N mod 11 == 0;
      
      constraint a + c == b /\ c + e == d /\ e + g == f /\ g + i ==h;
      
      solve satisfy;
       
      output [ "His number was " ++ show(N)];
      
      % His number was 143968275
      % ----------
      % ==========
      % Finished in 461 msec - Chuffed Solver
      
      

      Like

    • Frits's avatar

      Frits 7:01 pm on 13 June 2024 Permalink | Reply

           
      #! python3 -m enigma -r
       
      # suppose the number is: ABCDEFGHI
      #                  pos = 123456789
       
      SubstitutedExpression
       
      --digits="1-9"
      
      # divisibility of 11 rule, abs(sum odd digits - sum even digits) is 0 or a 
      # multiple of 11, sum odd: A + C + E + G + I, sum even: A + 2.(C + G + E) + I
       
      "(11 if C + E < 11 else 22) - (C + E) = G"
       
      # even positions are the sum of the adjacent digits
      "A + C = B"
      "C + E = D"
      "E + G = F"
      "G + I = H"
       
      # solution is divisible by A and I
      "ABCDEFGHI % A = 0"
      "ABCDEFGHI % I = 0"
       
      --answer="ABCDEFGHI"
      

      Like

      • Frits's avatar

        Frits 9:23 pm on 13 June 2024 Permalink | Reply

        You only need to know three digits (like the 1st, 3rd and 5th digits).

        from functools import reduce
        
        # convert digit sequences to number
        d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
        
        dgts8 = set(range(1, 9))
        dgts9 = dgts8 | {9}
        
        for C in dgts8:
          for E in dgts8 - {C}:
            # 11 divisibility rule
            G = (11 if C + E < 11 else 22) - (C + E)
            D, F = C + E, E + G
            # different digits
            if len(r1 := dgts9 - {C, E, G, D, F}) != 4: continue
            for A in r1:
              B = A + C
              if len(r2 := sorted(r1 - {A, B})) != 2: continue
              H, I = r2[1], r2[0]
              if H != G + I: continue
              
              ABCDEFGHI = d2n([A, B, C, D, E, F, G, H, I])
              if ABCDEFGHI % A: continue
              if ABCDEFGHI % I: continue
              print("answer:", ABCDEFGHI)     
        

        Like

    • Ruud's avatar

      Ruud 6:44 am on 16 June 2024 Permalink | Reply

      This version does not require the divisible_by method:

      from istr import istr
      
      for i in istr.concat(istr.permutations(istr.digits("1-"), 9)):
          if i[1] == i[0] + i[2] and i[3] == i[2] + i[4] and i[5] == i[4] + i[6] and i[7] == i[6] + i[8] and i % 11 == 0 and i % i[0] == 0 and i % i[8] == 0:
              print(i)
      

      Like

  • Unknown's avatar

    Jim Randell 4:42 pm on 7 June 2024 Permalink | Reply
    Tags:   

    Teaser 3220: Graffiti 

    From The Sunday Times, 9th June 2024 [link] [link]

    Six pupils were in detention having been caught painting graffiti on the school wall. The teacher decided to show them some examples of abstract art and produced seven different pictures in order ABCDEFG for them to study for a few minutes. Teacher took them back, showed them again upside down and in random order, and asked them to write down which of ABCDEFG each one was.

    The answers were:

    Helen: A B C D E F G
    Ian:   A B C F E D G
    Jean:  A D F G C E B
    Kevin: A D B F C E G
    Lee:   A F C G E D B
    Mike:  C A F B E D G

    It transpired that each picture had been correctly identified by at least one pupil, and everyone had a different number of correct answers.

    What was the correct order for the upside down ones?

    [teaser3220]

     
    • Jim Randell's avatar

      Jim Randell 4:54 pm on 7 June 2024 Permalink | Reply

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

      Run: [ @replit ]

      from enigma import (cproduct, seq_all_different, join, printf)
      
      # guesses
      guesses = "ABCDEFG ABCFEDG ADFGCEB ADBFCEG AFCGEDB CAFBEDG".split()
      
      # count number of correct answers
      correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
      
      # consider possible correct orderings (each is correctly placed by someone)
      for order in cproduct(set(xs) for xs in zip(*guesses)):
        if not seq_all_different(order): continue
        # each pupil got a different number of correct answers
        if not seq_all_different(correct(order, guess) for guess in guesses): continue
        # output solution
        printf("order = {order}", order=join(order))
      

      Solution: The correct order was: C A F G E D B.

      So the number of correct identifications for each pupil were:

      H = 1 (E)
      I = 2 (D E)
      J = 3 (B F G)
      K = 0
      L = 4 (B D E G)
      M = 5 (A C D E F)

      There are factorial(7) = 5040 possible orders that the paintings could be presented in, but as we know that each must appear in the correct position for one of the pupils we only need to look for positions that have received a guess, and using the disjoint cartestian product of the possible positions brings the number of candidate orderings down to 15.

      Like

      • Jim Randell's avatar

        Jim Randell 10:21 am on 8 June 2024 Permalink | Reply

        I had a look at a more efficient way to generate the disjoint cartesian product of a sequence of sets (where no value can appear in more than one position).

        It is possible to implement this fairly directly using the [[ exact_cover() ]] function from the enigma.py library, but it was slightly faster to implement a specific algorithm.

        The following Python program has an internal runtime of 331µs.

        Run: [ @replit ]

        from enigma import (peek, seq_all_different, join, printf)
        
        # guesses
        guesses = "ABCDEFG ABCFEDG ADFGCEB ADBFCEG AFCGEDB CAFBEDG".split()
        
        # count number of correct answers
        correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
        
        # disjoint cartestian product of a bunch of sets, any value may only appear in one position
        def disjoint_cproduct(ss):
          ss = dict(enumerate(set(xs) for xs in ss))
          return _disjoint_cproduct(ss, [None] * len(ss))
        
        # ss = remaining sets to process
        # vs = selected values
        def _disjoint_cproduct(ss, vs):
          # final slot?
          if len(ss) == 1:
            j = peek(ss.keys())
            for x in ss[j]:
              vs[j] = x
              yield tuple(vs)
          else:
            # choose a slot with the fewest options
            j = min(ss.keys(), key=(lambda j: len(ss[j])))
            for x in ss[j]:
              ss_ = dict((k, v.difference([x])) for (k, v) in ss.items() if k != j)
              vs[j] = x
              yield from _disjoint_cproduct(ss_, vs)
        
        # consider possible correct orderings (each is correctly placed by someone)
        for order in disjoint_cproduct(zip(*guesses)):
          # each pupil got a different number of correct answers
          if not seq_all_different(correct(order, guess) for guess in guesses): continue
          # output solution
          printf("order = {order}", order=join(order))
        

        Expect to see [[ disjoint_cproduct() ]] appearing in the next update to enigma.py. (Note: It is available from version 2024-06-07).

        Like

    • Frits's avatar

      Frits 6:41 pm on 7 June 2024 Permalink | Reply

      guesses = "ABCDEFG ABCFEDG ADFGCEB ADBFCEG AFCGEDB CAFBEDG".split()
      M, N = len(guesses), len(guesses[0])
       
      # count number of correct answers
      correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
      
      # candidate correct pictures per slot
      sols = sorted([(set(g[i] for g in guesses), i) for i in range(N)], 
                     key=lambda x: len(x[0]))
      # save the original order
      order = [x[1] for x in sols]
      
      # get a sequence of different pictures
      def solve(ps, k, ss=[]):
        if k == 0:
          yield ss
        else:
          for n in ps[0][0]:
            if n in ss: continue
            yield from solve(ps[1:], k - 1, ss + [n])
      
      # get a sequence of <N> different pictures     
      for s in solve(sols, N):
        # put solution in the right order
        sol = ''.join(x for x, y in sorted(zip(s, order), key=lambda z: z[1]))
        # check for different numbers of correct answers
        if len(set(correct(sol, g) for g in guesses)) != M: continue
        print("answer", sol)     
      

      Like

      • Frits's avatar

        Frits 1:36 pm on 8 June 2024 Permalink | Reply

        I assumed choosing slots with the fewest options was more efficient but without it the program is slightly faster. Jim’s disjoint_cproduct program is definitely faster compared to the version with “j = min(ss.keys())”.

        Like

        • Ruud's avatar

          Ruud 7:24 pm on 8 June 2024 Permalink | Reply

          It’s because the number of combinations of 4 out of 15 is the same as the number of combinations of 11 out of 15. Both are 1365.

          Like

    • Ruud's avatar

      Ruud 7:27 pm on 7 June 2024 Permalink | Reply

      Here’s my version:

      from itertools import permutations
      
      guesses = ["A B C D E F G".split(), "A B C F E D G".split(), "A D F G C E B".split(), "A D B F C E G".split(), "A F C G E D B".split(), "C A F B E D G".split()]
      for correct in permutations("A B C D E F G".split()):
          counts = set()
          for guess in guesses:
              count = sum(1 for c0, c1 in zip(correct, guess) if c0 == c1 and c0 != " ")
              counts.add(count)
          if len(counts) == 6:
              print(" ".join(correct))
      

      Like

    • Rob's avatar

      Rob 5:39 pm on 16 June 2024 Permalink | Reply

      Jim’s solution shows CAFGEBD provides Jean (ADFGCEB) with 3 correct matches (BFG). It seems there are only 2 correct matches (BF) in which case the solution does not meet the criterion of “everyone having a different number of correct answers”
      The correct solution is CAFGEDB

      Like

  • Unknown's avatar

    Jim Randell 5:20 pm on 6 June 2024 Permalink | Reply
    Tags:   

    Teaser 2573: Prime cricketers 

    From The Sunday Times, 15th January 2012 [link] [link]

    George and Martha support their local cricket team and keep records of all the results. At the end of last season, they calculated the total number of runs scored by each of the 11 players. George commented that the totals were all different three-digit palindromic primes. Martha added that the average of the 11 totals was also a palindromic prime.

    What were the two highest individual totals?

    [teaser2573]

     
    • Jim Randell's avatar

      Jim Randell 5:21 pm on 6 June 2024 Permalink | Reply

      Very straightforward constructive solution.

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

      Run: [ @replit ]

      from enigma import (primes, is_npalindrome, subsets, div, printf)
      
      # 3-digit palindromic primes
      scores = list(n for n in primes.between(100, 999) if is_npalindrome(n))
      
      # choose 11 different scores
      for ss in subsets(scores, size=11):
        # calculate the mean
        avg = div(sum(ss), 11)
        # which is also in scores
        if avg and avg in scores:
          # output solution
          printf("{avg} -> {ss}")
      

      Solution: The two highest individual totals are 787 and 919.

      The full list of totals is:

      101, 131, 151, 181, 191, 313, 353, 373, 383, 787, 919
      mean = 353

      Like

    • Ruud's avatar

      Ruud 6:41 pm on 6 June 2024 Permalink | Reply

      Very similar to @Jim Randell’s solution, but without enigma:

      import sympy
      from itertools import combinations
      
      palindromic_primes_with_length_3 = []
      for i in range(1, 1000):
          prime = sympy.prime(i)
          prime_as_str = str(prime)
          if len(prime_as_str) > 3:
              break
          if len(prime_as_str) == 3 and str(prime_as_str) == str(prime_as_str)[::-1]:
              palindromic_primes_with_length_3.append(prime)
      
      solutions = set()
      for scores in combinations(palindromic_primes_with_length_3, 11):
          average_score = sum(scores) / 11
          if average_score in palindromic_primes_with_length_3:
              solutions.add((scores, int(average_score)))
      print(solutions)
      

      Like

      • Frits's avatar

        Frits 11:28 am on 7 June 2024 Permalink | Reply

        Sympy also has a primerange() method.

        Like

    • GeoffR's avatar

      GeoffR 8:16 pm on 6 June 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 100..999:A; var 100..999:B; var 100..999:C; var 100..999:D;
      var 100..999:E; var 100..999:F; var 100..999:G; var 100..999:H;
      var 100..999:I; var 100..999:J; var 100..999:K; 
      
      constraint all_different([A,B,C,D,E,F,G,H,I,J,K]);
      
      predicate is_prime(var int: x) = 
       x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) ((i < x) -> (x mod i > 0));
       
      constraint A div 100 == A mod 10 /\ is_prime(A);
      constraint B div 100 == B mod 10 /\ is_prime(B);
      constraint C div 100 == C mod 10 /\ is_prime(C);
      constraint D div 100 == D mod 10 /\ is_prime(D);
      constraint E div 100 == E mod 10 /\ is_prime(E);
      constraint F div 100 == F mod 10 /\ is_prime(F);
      constraint G div 100 == G mod 10 /\ is_prime(G);
      constraint H div 100 == H mod 10 /\ is_prime(H);
      constraint I div 100 == I mod 10 /\ is_prime(I);
      constraint J div 100 == J mod 10 /\ is_prime(J);
      constraint K div 100 == K mod 10 /\ is_prime(K);
      
      var 1000..9999:asum;
      var 100..999:average;
      
      constraint asum  == A+B+C+D+E+F+G+H+I+J+K;
      constraint average == asum div 11 /\ asum mod 11 == 0;
      
      constraint is_prime(average);
      
      constraint card( {average} intersect {A,B,C,D,E,F,G,H,I,J,K}) == 1;
      
      constraint increasing( [A,B,C,D,E,F,G,H,I,J,K]);
      
      solve satisfy;
      
      output["Scores are " ++ show([A,B,C,D,E,F,G,H,I,J,K])
      ++ "\n" ++ "Average = " ++ show(average) ];
      
      % Scores are [101, 131, 151, 181, 191, 313, 353, 373, 383, 787, 919]
      % Average = 353
      % ----------
      % ==========
      
      

      Like

      • GeoffR's avatar

        GeoffR 9:39 am on 7 June 2024 Permalink | Reply

        Shorter code, but still slow.

        % A Solution in MiniZinc
        include "globals.mzn";
        
        % Eleven cricket scores
        var 100..999:A; var 100..999:B; var 100..999:C; var 100..999:D;
        var 100..999:E; var 100..999:F; var 100..999:G; var 100..999:H;
        var 100..999:I; var 100..999:J; var 100..999:K; 
         
        constraint all_different([A,B,C,D,E,F,G,H,I,J,K]);
        
        var 100..999:average;
         
        predicate is_pr_pal(var int: x) = 
         x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) 
         ((i < x) -> (x mod i > 0)) /\ x div 100 == x mod 10;
         
        constraint sum([is_pr_pal(A), is_pr_pal(B),is_pr_pal(C), is_pr_pal(D),
         is_pr_pal(E), is_pr_pal(F), is_pr_pal(G), is_pr_pal(H),
         is_pr_pal(I), is_pr_pal(J), is_pr_pal(K)]) == 11;
           
        constraint average == (A+B+C+D+E+F+G+H+I+J+K) div 11
        /\ (A+B+C+D+E+F+G+H+I+J+K) mod 11 == 0;
         
        constraint is_pr_pal(average);
         
        constraint card( {average} intersect {A,B,C,D,E,F,G,H,I,J,K}) == 1;
         
        constraint increasing( [A,B,C,D,E,F,G,H,I,J,K]);
         
        solve satisfy;
         
        output["Scores are " ++ show([A,B,C,D,E,F,G,H,I,J,K])
        ++ "\n" ++ "Average = " ++ show(average) ];
         
        
        
        

        Like

    • GeoffR's avatar

      GeoffR 9:57 pm on 6 June 2024 Permalink | Reply

      
      from enigma import is_prime
      from itertools import combinations
      
      scores = [x for x in range(100,999) if is_prime(x) \
                if x // 100 == x % 10 ]
      
      for s in combinations(scores, 11):
          av, r = divmod(sum(s), 11)
          if r != 0: continue
          if av in scores and av //100 == av % 10:
              print(f"Average = {av}")
              print(f"Scores = {s}")
      

      Like

      • Ruud's avatar

        Ruud 10:20 am on 7 June 2024 Permalink | Reply

        You can leave out the av // 100 == av % 10 test in the final if condition, as that’d guaranteed by the membership of scores.

        Like

    • Frits's avatar

      Frits 10:17 am on 7 June 2024 Permalink | Reply

      Same number of iteratons. It also works if scores would have had exactly 11 items.

      from itertools import combinations
      
      # prime numbers from 101-999
      P = [3, 5, 7]
      P += [x for x in range(11, 100, 2) if all(x % p for p in P)]
      P = [x for x in range(101, 1000, 2) if all(x % p for p in P)]
      # 3-digit palindromic primes
      scores = {p for p in P if p // 100 == p % 10}
      assert (ln := len(scores)) >= 11
      
      t = sum(scores)
      
      # choose scores outside the 11
      for ss in combinations(scores, ln - 11):
        # calculate the average
        avg, r = divmod(t - sum(ss), 11)
        # which is also in scores
        if r or avg not in scores: continue
        print("answer", sorted(scores.difference(ss))[-2:])     
      

      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