Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:50 am on 9 February 2021 Permalink | Reply
    Tags:   

    Teaser 2775: Strictly not 

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

    Four celebrities entered a dance competition. Five judges each shared out their eight marks among the four dancers, with each getting a non-zero whole number. Each judge split the eight marks in a different way and then allocated them as follows. Amanda’s marks to Lana and Natasha added to the same total as Barry’s marks to Madge and Paula. Barry gave more marks to Madge than to any other dancer, Charles gave more to Paula than to any other, and Doris gave more to Natasha than to any other. Lana scored more from Edna than from Amanda. All dancers had the same total so the head judge’s scores were used, giving a winner and runner-up.

    Who was the head judge, who was the winner and who was the runner-up?

    [teaser2775]

     
    • Jim Randell's avatar

      Jim Randell 8:51 am on 9 February 2021 Permalink | Reply

      Each of the 5 judges gives out 8 points, so 40 points are awarded in total. And there are 4 dancers, and they all receive the same total number of points. So each dancer receives 10 points in total.

      Each judge awards a non-zero number of points to each dancer, so the points awarded are between 1 and 5.

      I used the [[ SubstitutedExpression ]] solver from the enigma.py library to find possible collections of points awarded by the judges, and the scores are then examined to determine which judges scores identify a clear winner and runner up.

      The following run file executes in 80ms.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, irange, seq_all_different, ordered, printf)
      
      # consider the points given:
      #
      #             Lana
      #             |  Madge
      #             |  |  Natasha
      #             |  |  |  Paula
      #   Amanda:   A  B  C  D = 8
      #   Barry:    E  F  G  H = 8
      #   Charles:  I  J  K  L = 8
      #   Doris:    M  N  P  Q = 8
      #   Edna:     R  S  T  U = 8
      #             =  =  =  =
      #            10 10 10 10
      
      # check the splits are all different
      check_splits = lambda *xs: seq_all_different(ordered(*x) for x in xs)
      
      # construct the alphametic problem
      p = SubstitutedExpression([
          # each judge gave out 8 points
          "A + B + C + D = 8",
          "E + F + G + H = 8",
          "I + J + K + L = 8",
          "M + N + P + Q = 8",
          "R + S + T + U = 8",
      
          # points were split in a different way by each judge
          "check_splits((A, B, C, D), (E, F, G, H), (I, J, K, L), (N, M, P, Q), (R, S, T, U))",
      
          # "A's marks to L and N had the same sum as B's marks to M and P"
          "A + C == F + H",
      
          # "B gave more marks to M than to any other"
          "max(E, G, H) < F",
      
          # "C gave more to P than to any other"
          "max(I, J, K) < L",
      
          # "D gave more to N than to any other"
          "max(M, N, Q) < P",
      
          # "L scored more from E than from A"
          "R > A",
      
          # all dancers had the same total (= 10)
          "A + E + I + M + R = 10",
          "B + F + J + N + S = 10",
          "C + G + K + P + T = 10",
          "D + H + L + Q + U = 10",
        ],
        distinct=(),
        digits=irange(1, 5), # points are in the range 1-5
        env=dict(check_splits=check_splits),
        # answer is the scores from each judge
        answer="((A, B, C, D), (E, F, G, H), (I, J, K, L), (M, N, P, Q), (R, S, T, U))",
        denest=-1, # [CPython] work around statically nested block limit
      )
      
      # solve the puzzle, and output solutions
      (judges, dancers) = ("ABCDE", "LMNP")
      for (_, scores) in p.solve(verbose=0):
        # look for scores that differentiate 1st and 2nd place
        for (j, ss) in zip(judges, scores):
          printf("{j}: {ss}\\")
          (p1, p2, p3, p4) = sorted(ss, reverse=1)
          if p1 > p2 > p3:
            (d1, d2) = (dancers[ss.index(p)] for p in (p1, p2))
            printf(" -> head judge; 1st = {d1}, 2nd = {d2}\\")
          printf()
        printf()
      

      Solution: Doris was the head judge. Natasha was the winner. Lana was the runner-up.

      The scores were allocated as follows (L, M, N, P):

      A: (2, 2, 2, 2)
      B: (2, 3, 2, 1)
      C: (1, 1, 1, 5)
      D: (2, 1, 4, 1)
      E: (3, 3, 1, 1)

      There are only 5 different (unordered) ways to divide a score of 8 between 4 dancers, and only one of them (4, 2, 1, 1) gives a clear 1st and 2nd place.

      Like

    • Frits's avatar

      Frits 9:26 pm on 9 February 2021 Permalink | Reply

      from collections import defaultdict
       
      # consider the points given:
      #
      #             Lana
      #             |  Madge
      #             |  |  Natasha
      #             |  |  |  Paula
      #   Amanda:   A  B  C  D = 8
      #   Barry:    E  F  G  H = 8
      #   Charles:  I  J  K  L = 8
      #   Doris:    M  N  P  Q = 8
      #   Edna:     R  S  T  U = 8
      #             =  =  =  =
      #            10 10 10 10
      
      M = 5  # number of rows
      N = 4  # number of columns 
      
      # decompose number <t> into <k> positive numbers from <ns>
      # so that sum(<k> numbers) equals <t>
      def decompose(t, k, ns, s=[]):
        if k == 1:
          if t in ns:
            yield s + [t]
        else:
          for (i, n) in enumerate(ns):
            if not(n + k - 2 < t): break
            #yield from decompose(t - n, k - 1, ns[i:], s + [n])
            yield from decompose(t - n, k - 1, ns, s + [n])   
            
      # combine rows of dictionary <d>
      # so that every column total equals <t>
      def combineRows(t, k, d, s=[]):
        if k == 1:
          remaining = [t - sum(x) for x in zip(*s)]
          if remaining in d[0]:
            yield s + [remaining]
        else:
          for row in d[k - 1]:
            # for each column ...
            for j in range(N):
              # .. sum already processed numbers
              already = sum([0 if len(s) == 0 else x[j] for x in s]) 
              # and check if there is room for remaining rows
              if not(row[j] + already  + k - 2 < t): 
                already = -1  # skip this row
                break
            if already > -1:
              yield from combineRows(t, k - 1, d, s + [row])         
      
      
      types = []
      d_rows = defaultdict(list)
      
      # create dictionary of possible rows for each type of row
      # the points awarded are between 1 and 5
      for x in decompose(8, N, range(1, 6)): 
        sortx = sorted(x) 
        if sortx in types:
          type = types.index(sortx)
        else:
          type = len(types)
          types.append(x)
        # append possible row  
        d_rows[type] += [x]
        
      
      # combine rows so that column total always equals 10  
      for rows in combineRows(10, M, d_rows): 
        testB, testC, testD = -1, -1, -1
        
        # validate rows
        for i, r in enumerate(rows):
          sr = sorted(r)
          # check if maximum value is unique
          if sr[-1] > sr[-2]:
            
            if sr[-1] == r[1]:    # testB : "max(E, G, H) < F",
              testB = i  
            elif sr[-1] == r[3]:  # testC : "max(I, J, K) < L",
              testC = i
            elif sr[-1] == r[2]:  # testD : "max(M, N, Q) < P",
              testD = i
        
        if -1 in {testB, testC, testD}: continue
        
        # get the other rows
        ix_oth = [x for x in range(M) if x not in {testB, testC, testD}]
        
        # "R > A"
        if rows[ix_oth[0]][0] < rows[ix_oth[1]][0]:
          testA = ix_oth[0]; testE = ix_oth[1]
        elif rows[ix_oth[0]][0] > rows[ix_oth[1]][0]:
          testE = ix_oth[0]; testA = ix_oth[1]
        else:
          continue
        
        # "A + C == F + H"
        if rows[testA][0] + rows[testA][2] != rows[testB][1] + rows[testB][3]:
          continue
        
        # the head judge's scores were used, giving a winner and runner-up
        judges, dancers = "ABCDE", "LMNP"
        for i, r in enumerate(rows):
          sr = sorted(r)
          if sr[1] < sr[2] < sr[3]:
            print(f"head judge: {judges[i]} ")
            print(f"winner    : {dancers[r.index(sr[3])]}")
            print(f"runner up : {dancers[r.index(sr[2])]}")
            print()
           
            for r in rows:
              print(r)
      

      Like

  • Unknown's avatar

    Jim Randell 9:26 am on 7 February 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 20: The kite 

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

    Some problems that look simple enough at first can prove to be remarkably tricky. Consider, for instance, the kite pictured above. The shaded areas are squares of equal size, the sides of each square being 15 inches. The width of the kite from A to B is exactly 42 inches.

    What is the length of the kite from C to D?

    [teaser20]

     
    • Jim Randell's avatar

      Jim Randell 9:29 am on 7 February 2021 Permalink | Reply

      The figure is a right kite [ @wikipedia ], so is composed of 2 identical right-angled triangles joined along their common hypotenuse CD (which is the length we wish to determine).

      Writing: AC = BC = a, and AD = BD = b, then the value we wish to determine, CD, is:

      CD = √(a² + b²)

      We are told the other diagonal, AB, has length 42, hence:

      42 = 2ab / √(a² + b²)
      21√(a² + b²) = ab

      And the sides of the squares that run from the centre to the edges of the kite are radii of the incircle, so:

      15 = ab / (a + b)

      Writing: x = ab, y = a + b, we get:

      15 = x / y
      x = 15y
      x² = 225y²

      Also:

      a² + b² = (a + b)² − 2ab = y² − 2x

      So:

      441(y² − 2x) = x²
      441(y² − 30y) = 225y²
      216y² = 13230y
      4y = 245
      y = 245 / 4
      x = 3675 / 4

      So the value of CD is:

      CD = √(a² + b²)
      = √(y² − 2x)
      = √(30625 / 16)
      = 175 / 4

      Solution: CD = 43.75 inches.

      And the lengths of the sides of the kite are 26.25 in and 35 in.

      Like

    • John Crabtree's avatar

      John Crabtree 10:22 pm on 8 February 2021 Permalink | Reply

      Let the mid-point of AB be F, and the point where the squares meet be G
      By Pythagoras, FG = √(2 * 225 – 441) = 3
      Let z be the angle formed by GAF.
      Tan(z) = t = 3 / 21 = 1 / 7
      CD = 21 [tan(45 + z) + tan(45 – z)] = 21 [(1+t)/(1-t) + (1-t)/(1+t))
      = 21 [4/3 + 3/4] = 21 * 25/12 = 43.75 inches.
      AC = 21 * 5/4 = 26.25 in. AD = 21 * 5/3 = 35 in.

      Like

  • Unknown's avatar

    Jim Randell 4:52 pm on 5 February 2021 Permalink | Reply
    Tags:   

    Teaser 3046: Square phone numbers 

    From The Sunday Times, 7th February 2021 [link] [link]

    George and Martha run a business in which there are 22 departments. Each department has a perfect-square three-digit extension number, i.e., everything from 100 (10²) to 961 (31²), and all five of their daughters are employees in separate departments.

    Andrea, Bertha, Caroline, Dorothy and Elizabeth have extensions in numerical order, with Andrea having the lowest number and Elizabeth the highest. George commented that, if you look at those of Andrea, Bertha and Dorothy, all nine positive digits appear once. Martha added that the total of the five extensions was also a perfect square.

    What is Caroline’s extension?

    [teaser3046]

     
    • Jim Randell's avatar

      Jim Randell 5:03 pm on 5 February 2021 Permalink | Reply

      A simple Python program finds the solution in 67ms.

      from enigma import (powers, subsets, concat, is_square, printf)
      
      # possible square numbers
      squares = powers(10, 31, 2)
      
      # choose 5 of the squares (in order)
      for (A, B, C, D, E) in subsets(squares, size=5):
      
        # the sum of the numbers is also a square
        if not is_square(A + B + C + D + E): continue
      
        # A, B, D use the digits 1-9
        ds = concat(A, B, D)
        if '0' in ds or len(set(ds)) != 9: continue
      
        # output solution
        printf("A={A} B={B} C={C} D={D} E={E}")
      

      Solution: Caroline’s extension is 729.

      Manually, you can write out the squares, and then working through the possible candidates for A, B, D, the solution is discovered quite quickly.

      Like

    • GeoffR's avatar

      GeoffR 7:51 pm on 5 February 2021 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
       
      % Andrea, Bertha, Caroline, Dorothy and Elizabeth's numbers
      var 100..961:A; var 100..961:B; var 100..961:C;
      var 100..961:D; var 100..961:E;
      
      constraint all_different([A, B, C, D, E]);
      constraint A < B /\ B < C /\ C < D /\ D < E;
      
      % digits for squares A, B and D
      var 1..9:a1; var 1..9:a2; var 1..9:a3;
      var 1..9:b1; var 1..9:b2; var 1..9:b3;
      var 1..9:d1; var 1..9:d2; var 1..9:d3;
      
      % all 3-digit squares
      set of int: sq3 = {n*n | n in 10..31};
      
      predicate is_sq(var int: y) =
      exists(z in 1..ceil(sqrt(int2float(ub(y)))))
      (z*z = y);
              
      % the total of the five extensions was also a perfect square        
      constraint is_sq(A + B + C + D + E);
      
       % all five telephone extensions are 3-digit squares       
      constraint A in sq3 /\ B in sq3 /\ C in sq3 /\ D in sq3 /\ E in sq3;
      
      % digit components in squares A, B and D
      constraint a1 == A div 100 /\ a2 == A div 10 mod 10 /\ a3 == A mod 10;
      constraint b1 == B div 100 /\ b2 == B div 10 mod 10 /\ b3 == B mod 10;
      constraint d1 == D div 100 /\ d2 == D div 10 mod 10 /\ d3 == D mod 10;
      
      set of int: all_dig ={1, 2, 3 ,4, 5, 6, 7, 8, 9};
      var set of int: S1 == {a1, a2, a3, b1, b2, b3, d1, d2, d3};
      constraint all_dig == S1;
      
      solve satisfy;
      
      output ["Caroline's extension is " ++ show(C)
      ++"\nOther extensions are " ++ show(A) ++ ", " ++ show(B)
      ++ ", " ++ show(D) ++ ", " ++ show(E) ];
      
      

      Like

    • GeoffR's avatar

      GeoffR 8:44 am on 6 February 2021 Permalink | Reply

      from itertools import combinations
      from enigma import nsplit, is_square
      
      squares =[x * x for x in range(10, 32)]
      
      for A, B, C, D, E in combinations(squares, 5):
        # five extensions are in numerical order
        if A < B < C < D < E:
          sq_total = A + B + C + D + E 
          if is_square(sq_total):
            # find individual digits of A, B and D
            a1, a2, a3 = nsplit(A)
            b1, b2, b3 = nsplit(B)
            d1, d2, d3 = nsplit(D)
            S1 = set((a1, a2, a3, b1, b2, b3, d1, d2, d3))
            # check this set contains nine positive digits
            if len(S1) == 9 and 0 not in S1:
              print(f"Five extensions are {A}, {B}, {C}, {D}, {E}")
      
      
      

      Like

    • Frits's avatar

      Frits 7:44 pm on 6 February 2021 Permalink | Reply

      squares = [x * x for x in range(15, 66)]
      ABD_squares = [x * x for x in range(13, 31) 
                     if len(set(str(x * x) + "0")) == 4]
      
      for i, a in enumerate(ABD_squares[:-2]):
        for j, b in enumerate(ABD_squares[i + 1:-1]):
          if len(set(str(a) + str(b))) != 6: continue
          for d in ABD_squares[i + j + 2:]:
            if len(set(str(a) + str(b) + str(d))) != 9: continue
            p = squares.index(b)
            q = squares.index(d)
            
            for c in squares[p + 1:q]:
              for e in squares[q + 1:17]:
                if not (a + b + c + d + e) in squares: continue
                print(f"Caroline's extension is {c}.")
      

      Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 10:21 am on 7 February 2021 Permalink | Reply

      import itertools
      
      digits = set([str(j) for j in range(1, 10)])
      
      for M in itertools.combinations([i**2 for i in range(10, 32)], 5):
        if (sum(M)**(1/2))%1 == 0:
          if set(str(M[0])+str(M[1])+str(M[3])) == digits: print("Caroline's extension is", M[2])
      

      Like

    • Frits's avatar

      Frits 4:21 pm on 7 February 2021 Permalink | Reply

      Logic can reduce a,b and d to specific squares.
      The version without reduction logic is faster.

      from collections import defaultdict
      
      squares = [x * x for x in range(15, 66)]
      ABD_squares = [x * x for x in range(13, 31) 
                     if len(set(str(x * x) + "0")) == 4]
          
      # return first column    
      col0 = lambda li: [x[0] for x in li]               
      
      fixed = set()
      str_fixed = ""
      mut_excl = defaultdict(set)
      
      # reduce candidates in list <ABD_squares>
      # by looking for digits occurring once or twice in list <ABD_squares>
      for loop in range(9):
        occ = defaultdict(list)
        
        for x in "123456789":
          # add entries to occurence dictionary <occ>
          for i, s in enumerate(ABD_squares):
            s1 = str(s)
            if x not in s1 or s1 in fixed: continue
            # skip if a square digit already occurs in <fixed>
            if any(y in str_fixed for y in s1): continue
            occ[x].append([i, s1])
      
          if len(occ[x]) == 1:  
            # a square has to occur in <ABD_squares>
            if not occ[x][0][1] in fixed:
              fixed.add(occ[x][0][1])
              str_fixed = "".join(fixed)
              # also update occurrence for other 2 related digits to same square
              for c in occ[x][0][1]:
                if c == x: continue
                occ[c] = occ[x]
          elif len(occ[x]) == 2:
            # we have a square with digits x,e,f and a square with digits x,g,h
            # so e, e, f, f may not occur in another square with resp. g, h ,g, h
            for y in occ[x][0][1]: 
              if y == x: continue
              for z in occ[x][1][1]:
                if z == x: continue
                mut_excl[y].add(z)  
                mut_excl[z].add(y) 
      
        # check if 2 digits always occur in different squares
        for x1, y1 in occ.items():
          for x2, y2 in occ.items():
            if x1 >= x2: continue
            if len(set(col0(y1) + col0(y2))) != \
               len(col0(y1)) + len(col0(y2)): continue
            # digit x1 and x2 occur in different squares
            # so maintain dictionary <mut_excl>
            mut_excl[x1].add(x2)
            mut_excl[x2].add(x1)
      
        # reduce squares if a square contains mutually exclusive digits
        ABD_squares = [s for s in ABD_squares 
                       if not any(m in str(s) for y in str(s) 
                                  for m in mut_excl[y])
                      ]
         
        if len(ABD_squares) == 3: break
        
        
      # start with a,b,d and then check c and e
      for i, a in enumerate(ABD_squares[:-2]):
        for j, b in enumerate(ABD_squares[i + 1:-1]):
          if len(set(str(a) + str(b))) != 6: continue
          for d in ABD_squares[i + j + 2:]:
            if len(set(str(a) + str(b) + str(d))) != 9: continue
            p = squares.index(b)
            q = squares.index(d)
            
            for c in squares[p + 1:q]:
              for e in squares[q + 1:17]:
                if not (a + b + c + d + e) in squares: continue
                print(f"Caroline's extension is {c}.")
      

      Like

    • Frits's avatar

      Frits 1:25 pm on 8 February 2021 Permalink | Reply

      Not very efficient.

      # decompose a number into <k> increasing 3-digit squares a, b, c, d, e
      # (a, b, d) has to contain all digits 1-9
      def decompose(t, k, m=1, ss=[]):
        if k == 1:
          # check if last number is higher and a square
          if t > ss[-1] and int(t ** 0.5) ** 2 == t:
            yield ss + [t]
        else:
          for i in range(m, 32 - k + 1):
            s = i * i
            if s > t: break
            
            if k in {3, 1} or len(set(str(s) + "0")) == 4:
              yield from decompose(t - s, k - 1, i + 1, ss + [s])
              
      # sum first five 3-digit squares is 750 and last five 3-digit squares is 4215
      for r in range(28, 65):
        # get five 3-digit squares which add up to square <r * r>
        for x in decompose(r * r, 5, 10): 
          sabd = set(str(1000000 * x[0] + 1000 * x[1] + x[3]))
          if len(sabd) != 9: continue
      
          print(f"Caroline's extension is {x[2]}.")
      

      Like

  • Unknown's avatar

    Jim Randell 8:47 am on 4 February 2021 Permalink | Reply
    Tags: by: D C Pusinelli   

    Brain-Teaser 688: Job allocation 

    From The Sunday Times, 22nd September 1974 [link]

    Ashley, Bill, Charles, David, and Edward are (not necessarily in that order), a dustman, a grocer, a miner, a blacksmith, and an artist, and all live on the right hand side of Strife Lane, in even numbered houses. All five are of different ages and no man has reached the age of retirement (65). All of course are upright and honest citizens, and never tell lies. However, I had forgotten what job each man did, where he lived, and how old he was, and so, to help me, each man volunteered the following statements:

    Ashley:
    (1) The artist lives at No. 10, next to Charles;
    (2) Nobody lives next to the grocer, although Bill is only two doors away.

    Bill:
    (3) I am the only man whose age is indivisible by 9;
    (4) I am 4 years older than Ashley;

    Charles:
    (5) The blacksmith’s age is 5 times his house number;

    David:
    (6) The miner lives 4 houses higher up the road from me;
    (7) The miner’s age is 3 times the dustman’s house number, but he is two-thirds the dustman’s age;

    Edward:
    (8) The dustman is twice as old as David;
    (9) I am the oldest man in the street.

    At what number does Ashley live?
    How old is the grocer?
    Who is the artist?

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

    [teaser688]

     
    • Jim Randell's avatar

      Jim Randell 8:48 am on 4 February 2021 Permalink | Reply

      I wrote a MiniZinc model to solve this puzzle, and then used the minizinc.py wrapper to format the output.

      This Python program runs in 178ms.

      from enigma import irange, printf
      from minizinc import MiniZinc
      
      model = """
      
        include "globals.mzn";
      
        % indices for A, B, C, D, E
        int: A = 1;
        int: B = 2;
        int: C = 3;
        int: D = 4;
        int: E = 5;
      
        % house numbers
        array [1..5] of var int: n;
      
        % even numbers
        constraint forall (i in 1..5) (n[i] > 0 /\ n[i] mod 2 = 0);
        % all different
        constraint all_different(n);
      
        % ages
        array [1..5] of var 16..64: a;
      
        % all different
        constraint all_different(a);
      
        % jobs
        var 1..5: Du;
        var 1..5: Gr;
        var 1..5: Mi;
        var 1..5: Bl;
        var 1..5: Ar;
      
        % all different
        constraint all_different([Du, Gr, Mi, Bl, Ar]);
      
        % (1) "the artist lives at number 10, next to C"
        constraint n[Ar] = 10;
        constraint n[C] = 8 \/ n[C] = 12;
      
        % (2) "nobody lives next to the grocer, although B is only 2 doors away"
        constraint forall (i in 1..5 where i != Gr) (abs(n[Gr] - n[i]) != 2);
        constraint abs(n[Gr] - n[B]) = 4;
      
        % (3) "B is the only one whose age is not divisible by 9"
        constraint forall (i in 1..5) (a[i] mod 9 != 0 <-> i = B);
      
        % (4) "B is 4 years older than A"
        constraint a[B] = a[A] + 4;
      
        % (5) "Bl age is 5x his house number"
        constraint a[Bl] = 5 * n[Bl];
      
        % (6) "Mi lives 4 houses further up the road than D"
        constraint n[Mi] = n[D] + 8;
      
        % (7) "Mi's age is 3x Du's house number; but Mi is 2/3 Du's age"
        constraint a[Mi] = 3 * n[Du];
        constraint 3 * a[Mi] = 2 * a[Du];
      
        % (8) "Du is 2x D's age"
        constraint a[Du] = 2 * a[D];
      
        % (9) "E is the oldest"
        constraint forall (i in 1..5 where i != E) (a[i] < a[E]);
      
        solve satisfy;
      
      """
      
      p = MiniZinc(model)
      
      name = [ "", "Ashley", "Bill", "Charles", "David", "Edward" ]
      for (n, a, Du, Gr, Mi, Bl, Ar) in p.solve(result="n a Du Gr Mi Bl Ar"):
        j = { Du: "Dustman", Gr: "Grocer", Mi: "Miner", Bl: "Blacksmith", Ar: "Artist" }
        for i in irange(1, 5):
          printf("{name}: house = {n}; age = {a}; job = {j}", name=name[i], n=n[i], a=a[i], j=j[i])
      

      Solution: Ashley lives at number 18. The grocer is 63. David is the artist.

      The complete solution:

      Ashley: house = 18; age = 36; job = Miner
      Bill: house = 8; age = 40; job = Blacksmith
      Charles: house = 12; age = 54; job = Dustman
      David: house = 10; age = 27; job = Artist
      Edward: house = 4; age = 63; job = Grocer

      Like

      • Frits's avatar

        Frits 11:52 am on 11 February 2021 Permalink | Reply

        @Jim, you didn’t code that B’s age may not be not divisible by 9.

        Like

        • Jim Randell's avatar

          Jim Randell 11:59 am on 11 February 2021 Permalink | Reply

          @Frits: Oh yes.

          I’ll update line 48 to:

            % (3) "B is the only one whose age is not divisible by 9"
            constraint forall (i in 1..5) (a[i] mod 9 != 0 <-> i = B);
          

          Like

      • Frits's avatar

        Frits 9:38 pm on 11 February 2021 Permalink | Reply

        “if stop: continue”

        My first version started looping over house numbers (where a maximum house number had to be specified). Looping over ages first eliminates this problem.

        from itertools import permutations
        
        # indices Ashley ... Edward
        (A, B, C, D, E) = (0, 1, 2, 3, 4)
        
        #  "B is 4 years older than A (which is a multiple of 9)"
        b_ages = [x for x in range(22, 65) if x % 9 != 0 and (x - 4) % 9 == 0]
        
        acde_ages = tuple(permutations(range(18, 65, 9), 4))    
        
        jobs_indices = list(permutations(range(5)))
        
        # update list hn for key <k> if value <v> doesn't exist yet in hn
        def update(k, v):
          if v in hn: 
            return False
          else:  
            hn[k] = v
            return True
          
        # start with ages divisble by 9
        for p4 in acde_ages:
          #  "Bl age is 5x his house number"
          if all(x % 5 != 0 for x in p4): 
            # at least one age must be a multiple of 5
            b_ages = [x for x in b_ages if x % 5 == 0]
        
          for b in b_ages:
            #  "B is 4 years older than A"
            if b != p4[0] + 4: continue
            
            # b must be placed at 2nd spot
            ag = (p4[0], b) + p4[1:]
            
            #  "E is the oldest"
            if ag[E] != max(ag): continue
            
            #  "Du is 2x D's age"
            if (2 * ag[D]) not in ag: continue
          
            # indices for Du, Gr, Mi, Bl, Ar
            for (Du, Gr, Mi, Bl, Ar) in jobs_indices:
              #  "Mi is 2/3 Du's age"
              if 3 * ag[Mi] != 2 * ag[Du]: continue
              
              #  "Bl age is 5x his house number"
              if ag[Bl] % 5 != 0: continue
              
              #  "Mi's age is 3x Du's house number"
              if ag[Mi] % 3 != 0: continue
             
              #  "Du is 2x D's age"
              if ag[Du] != 2 * ag[D]: continue
              
              # initialize house numbers
              hn = [0, 0, 0, 0, 0]
              
              #  "the artist lives at number 10, next to C"
              hn[Ar] = 10
              if not update(Du, ag[Mi] // 3): continue
              if not update(Bl, ag[Bl] // 5): continue
              
              if hn[C] and hn[C] not in {8, 12} : continue
              
              for loop in range(2):
                #  "Mi lives 4 houses further up the road than D"
                if hn[Mi] == 0 and hn[D]:
                  if not update(Mi, hn[D] + 8): break
                elif hn[Mi] and hn[D] == 0:
                  hn[D] = hn[Mi] - 8
                  if hn[Mi] < 10 or not update(D, hn[Mi] - 8): 
                    break
                elif hn[Mi] and hn[D]:  
                  if hn[Mi] != hn[D] + 8: break
        
                # B is only 2 doors away from grocer"
                if hn[B]:
                  if hn[Gr]:
                    if abs(hn[B] - hn[Gr]) != 4: break
                  else:
                    Gr_opts = []
                    if hn[B] + 4 not in hn: 
                      Gr_opts.append(hn[B] + 4)
                    if hn[B] > 5 and hn[B] - 4 not in hn:
                      Gr_opts.append(hn[B] - 4)
                    if len(Gr_opts) == 1:
                      if not update(Gr, Gr_opts[0]): break
                    elif len(Gr_opts) == 0: break
                else:
                  if hn[Gr]:
                    B_opts = []
                    if hn[Gr] + 4 not in hn: 
                      B_opts.append(hn[Gr] + 4)
                    if hn[Gr] > 5 and hn[Gr] - 4 not in hn:
                      B_opts.append(hn[Gr] - 4)
                    if len(B_opts) == 1:
                      if not update(B, B_opts[0]): break
                    elif len(B_opts) == 0: break
                
                #  "nobody lives next to the grocer"
                if hn[Gr] and any(abs(x - hn[Gr]) == 2 and x != 0 for x in hn): break
                
                #  "the artist lives at number 10, next to C"
                if hn[C] and hn[C] not in {8, 12} :  break
                
              else: # no break
                
                # all house numbers determined? 
                if 0 in hn: continue
        
                jobs = {Du:  "Dustman", Gr: "Grocer", Mi: "Miner", 
                        Bl: "Blacksmith", Ar: "Artist"}
        
                print("              Ashley", ) 
                print("              |   Bill") 
                print("              |   |   Charles") 
                print("              |   |   |   David") 
                print("              |   |   |   |   Edward") 
                print("house number", tuple(hn))
                print("age         ", ag)
                print("              |   |   |   |  ", jobs[4]) 
                print("              |   |   |  ", jobs[3]) 
                print("              |   |  ", jobs[2]) 
                print("              |  ", jobs[1]) 
                print("             ", jobs[0]) 
        

        Like

      • Frits's avatar

        Frits 12:58 pm on 10 August 2021 Permalink | Reply

        @Jim,

        Did you choose for MiniZinc because it allows for an unrestrained house number range?

        Like

        • Jim Randell's avatar

          Jim Randell 3:35 pm on 10 August 2021 Permalink | Reply

          @Frits: That was probably the main reason. Also using a declarative solver like MiniZinc meant I didn’t have to worry about what order to attack the problem in.

          Like

      • Frits's avatar

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

        @Jim,

        If I add

        var 2..6: q = Ar + 1;

        then variable q is not in the result set.

        Is this a MiniZinc issue or a wrapper issue?

        Like

        • Jim Randell's avatar

          Jim Randell 3:29 pm on 10 August 2021 Permalink | Reply

          @Frits:

          The wrapper just parses the MiniZinc output and turns it into Python objects. So if MiniZinc doesn’t output a value it is not available in Python.

          But you should be able to use a constraint to get the value output by default:

          var 2..6: q;
          constraint q = Ar + 1;
          

          Like

  • Unknown's avatar

    Jim Randell 8:33 am on 2 February 2021 Permalink | Reply
    Tags:   

    Teaser 2774: Loaded dice 

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

     I have two traditional-looking dice, but only one of them is fair. In the other there is an equal chance of getting a 1, 2, 3, 4 or 5, but the die is loaded so that a 6 is thrown more than half the time. I threw the two dice and noted the total. It turned out that with my dice the chance of getting that total was double what it would have been with two fair dice.

    What (as a simple fraction) is the chance of getting a 6 with the loaded die?

    [teaser2774]

     
    • Jim Randell's avatar

      Jim Randell 8:35 am on 2 February 2021 Permalink | Reply

      The fair die has a probability of 1/6 of showing any particular face (scores 1-6).

      The unfair die has a probability of p of showing 6, and (1 − p) / 5 of showing the other faces (scores 1-5).

      If we consider the probability of throwing a total of t (scores 2-12) using both dice:

      There are { n = (t − 1) if t ≤ 6; otherwise n = (13 − t) } different ways of achieving the total.

      And with two fair dice (which have 36 equally like outcomes) the probability of throwing t is n / 36.

      And we want to find when the probability of throwing t with one fair and one unfair dice is twice this value: i.e. when the probability is n / 18.

      If t ≤ 6, then neither die can show a 6, so the probability of throwing t is:

      P = n ((1 − p) / 5) (1/6)

      which we can solve for P = n / 18:

      n ((1 − p) / 5) (1/6) = n / 18
      3(1 − p) = 5
      p = −2/3 (not possible)

      If t > 6, then 6 can show on the unfair dice, so we have two ways to make the total:

      P = [(n − 1) ((1 − p) / 5) (1/6)] + [p (1/6)]

      and we can solve this for P = n / 18:

      (n − 1) ((1 − p) / 5) (1/6) + p / 6 = n / 18
      3(n − 1)(1 − p) + 15p = 5n
      18p − 3np − 3 = 2n
      p = (2n + 3) / (18 − 3n)

      Also for t > 6 we have: n = 13 − t, so:

      p = (2(13 − t) + 3) / (18 − 3(13 − t))
      p = (29 − 2t) / (3t − 21)

      And we are interested in cases where t = 7 .. 12 and 0.5 < p ≤ 1:

      t = 7 ⇒ p = undefined (not possible)
      t = 8 ⇒ p = 13/3 (not possible)
      t = 9 ⇒ p = 11/6 (not possible)
      t = 10 ⇒ p = 1
      t = 11 ⇒ p = 7/12
      t = 12 ⇒ p = 1/3 (not permitted)

      So we find there are 2 possible situations:

      (1) The loaded die always shows 6, and the setter threw 10.
      The probability of throwing 10 with two fair dice is 1/12.
      The probability of throwing 10 with one fair die and the loaded die is 1/6.

      (2) The loaded die has a probability 7/12 of showing 6, and the setter threw 11.
      The probability of throwing 11 with two fair dice is 1/18.
      The probability of throwing 11 with one fair die and the loaded die is 1/9.

      I suspect we are to ignore that case where the loaded die always shows 6, as this would be quite obvious. (Although I would have thought a die that shows 6 more than half the time would be fairly obvious anyway). The setter could have explicitly excluded this case by specifying “an equal non-zero chance of scoring 1 – 5″ for the loaded die.

      Solution: The probability of scoring 6 with loaded die is 7/12.

      Like

    • John Crabtree's avatar

      John Crabtree 2:16 am on 4 February 2021 Permalink | Reply

      I was fortunate and took the probability of the throwing 6 with the biased die as n times that of any other number ie n>5 and p = n/(n+5).
      I reached t = 10 + 9/(n+2) when Jim’s two situations are immediately apparent.

      Like

  • Unknown's avatar

    Jim Randell 5:01 pm on 29 January 2021 Permalink | Reply
    Tags:   

    Teaser 3045: Let Tel play BEDMAS hold ’em! 

    From The Sunday Times, 31st January 2021 [link] [link]

    Awaiting guests on poker night, Tel placed (using only clubs, face-up, in order left-to-right) the Ace (=1) to 9 (representing numerals), then interspersed the Ten, Jack, Queen and King (representing –, +, × and ÷ respectively) in some order, but none together, among these.

    This “arithmetic expression” included a value over 1000 and more even than odd numbers. Applying BEDMAS rules, as follows, gave a whole-number answer. “No Brackets or powErs, so traverse the expression, left-to-right, doing each Division or Multiplication, as encountered, then, again left-to-right, doing each Addition or Subtraction, as encountered.”

    Tel’s auntie switched the King and Queen. A higher whole-number answer arose. Tel displayed the higher answer as a five-card “flush” in spades (the Jack for + followed by four numeral cards).

    Give each answer.

    [teaser3045]

     
    • Jim Randell's avatar

      Jim Randell 5:51 pm on 29 January 2021 Permalink | Reply

      I took “whole number” to be any integer, although you get the same answer if you just use positive integers.

      It was fun writing the [[ evaluate() ]] routine to perform the calculations in the described fashion (although it is just standard arithmetic precedence rules).

      The following Python program runs in 59ms.

      Run: [ @replit ]

      from enigma import (Rational, irange, subsets, tuples, peek, as_int, catch, update, multiset, join, printf)
      
      # implementation of rationals
      Q = Rational()
      
      # map operators to implementation
      fn = {
        '+': (lambda a, b: a + b),
        '-': (lambda a, b: a - b),
        '*': (lambda a, b: a * b),
        '/': (lambda a, b: Q(a, b)),
      }
      
      # evaluate the expression <ss>
      def evaluate(ss):
        ss = list(ss)
        # precedence order
        for ops in [set('*/'), set('+-')]:
          while True:
            # find the leftmost occurrence of an operator
            i = peek((i for (i, op) in enumerate(ss) if op in ops), default=-1)
            if i == -1: break
            # apply the operation
            assert 0 < i < len(ss) - 1
            n = fn[ss[i]](ss[i - 1], ss[i + 1])
            ss[i - 1:i + 2] = [n]
        assert len(ss) == 1
        return ss[0]
      
      # interleave 2 sequences (until one runs out)
      def interleave(s1, s2):
        (i1, i2) = (iter(s1), iter(s2))
        while True:
          try:
            yield next(i1)
            yield next(i2)
          except StopIteration:
            break
      
      # the string of digits
      digits = "123456789"
      ds = multiset.from_seq(digits)
      
      # slice up the string of digits
      n = len(digits)
      for ps in subsets(irange(1, n - 1), size=4, fn=list):
        ns = tuple(int(digits[i:j]) for (i, j) in tuples([0] + ps + [n]))
        # at least one number is greater than 1000
        if not any(n > 1000 for n in ns): continue
        # there are more even numbers than odd numbers
        if not (len(ns) > 2 * sum(n % 2 for n in ns)): continue
      
        # insert the operators
        for ops in subsets("+-*/", size=4, select="P"):
          # make the first expression
          ss1 = list(interleave(ns, ops))
          # evaluate it
          r1 = catch(as_int, evaluate(ss1))
          if r1 is None: continue
      
          # swap * and /
          ss2 = update(ss1, [(ss1.index('*'), '/'), (ss1.index('/'), '*')])
          r2 = catch(as_int, evaluate(ss2))
          if r2 is None or not(r1 < r2): continue    
      
          # the result is a positive 4-digit number
          if r2 < 1000 or r2 > 9999: continue
          # that can be represented using the available digits
          if not ds.issuperset(str(r2)): continue
      
          # output solution
          printf("{ss1} = {r1}; {ss2} = {r2}", ss1=join(ss1, sep=" "), ss2=join(ss2, sep=" "))
      

      Solution: Tel’s answer was 1274. Auntie’s answer was 1289.

      The expressions are:

      Tel: 1234 + 56 × 7 ÷ 8 − 9 = 1274
      Auntie: 1234 + 56 ÷ 7 × 8 − 9 = 1289

      There are only 6 ways to split up the digits in the required fashion:

      1 | 2 | 34 | 5678 | 9
      1 | 2 | 3456 | 78 | 9
      12 | 3 | 4 | 5678 | 9
      12 | 3456 | 7 | 8 | 9
      1234 | 5 | 6 | 78 | 9
      1234 | 56 | 7 | 8 | 9

      Which lead to 22 expressions that evaluate to a positive integer.

      There are 2 further candidate solutions resulting in 4-digit positive numbers, but Tel would not be able to display the result as described:

      Tel: 12 × 3 ÷ 4 + 5678 − 9 = 5678
      Auntie: 12 ÷ 3 × 4 + 5678 − 9 = 5685

      Tel: 1234 − 56 ÷ 7 × 8 + 9 = 1179
      Auntie: 1234 − 56 × 7 ÷ 8 + 9 = 1194

      Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 11:02 am on 31 January 2021 Permalink | Reply

        I was going mad trying to find my error but when I compared my solution to yours I get the same. I guess the puzzle allows for an Ace in the flush.

        Like

        • Jim Randell's avatar

          Jim Randell 2:05 pm on 31 January 2021 Permalink | Reply

          @Tony: There is only one candidate solution that is positive, and is composed of 4 distinct positive digits. And I was happy enough that this could be represented using the described cards.

          Fortunately I don’t know enough about poker to know if this counts as a “5 card flush” or not.

          Like

    • Frits's avatar

      Frits 9:23 pm on 30 January 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      from itertools import  permutations
      
      def check(li):
        # credit: B. Gladman
        # there must be more even than odd numbers
        if sum([x & 1 for x in li]) > 2:
          return ""
          
        li2 = [str(x) for x in li]  
        for p in permutations("*/+-", 4):
          exp = ''.join(a + b for a, b in zip(li2, p + ('',)))
          ev = eval(exp) 
          if ev % 1 == 0 and ev >= 0:
            exp2 = ''.join(chr(ord(c) ^ 5) if c in '/*' else c for c in exp)
            ev2 = eval(exp2) 
            if ev2 > ev and ev2 % 1 == 0:
              ev2 = int(ev2)
              # larger answer has distinct non-zero digits
              if "0" not in str(ev2) and len(set(str(ev2))) == len(str(ev2)):
                return exp + " ; " + exp2
            
        return ""
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        "T + 1 == U or T == 0",
        "S + 1 == T or S == 0",
        "R + 1 == S or R == 0",
        "[x for x in [R, S, T, U] if x > 0][0] == Q + 1",
      
        "P + 1 == Q or P == 0",
        "O + 1 == P or O == 0",
        "N + 1 == O or N == 0",
        "[x for x in [N, O, P, Q] if x > 0][0] == M + 1",
      
        "L + 1 == M or L == 0",
        "K + 1 == L or K == 0",
        "J + 1 == K or J == 0",
        "[x for x in [J, K, L, M] if x > 0][0] == H + 1",
      
        "G + 1 == H or G == 0",
        "F + 1 == G or F == 0",
        "E + 1 == F or E == 0",
        "[x for x in [E, F, G, H] if x > 0][0] == D + 1",
      
        "C + 1 == D or C == 0",
        "B + 1 == C or B == 0",
        "A + 1 == B or A == 0",
        "[x for x in [A, B, C, D] if x > 0][0] == 1",
        
        # included a value over 1000
        "sum([A > 0, E > 0, J > 0, N > 0, R > 0]) = 1",
        
        "len(check([ABCD, EFGH, JKLM, NOPQ, RSTU])) > 0",
        
        ],
        answer="check([ABCD, EFGH, JKLM, NOPQ, RSTU])", 
        distinct="",
        env=dict(check=check),
        d2i=dict([(0, "DHMQU")] + 
                 [(1, "EFGHJKLMNOPQRSTU")] + 
                 [(2, "JKLMNOPQRSTU")] + 
                 [(3, "NOPQRSTU")] +
                 [(4, "RSTU")] +
                 [(5, "RSTU")] +
                 [(k, "U") for k in range(6, 9)]
                 ),
      
        #reorder=0,
        verbose=0
        )   
        
      # Print answer
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:19 am on 28 January 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 685: Overseas mail 

    From The Sunday Times, 1st September 1974 [link]

    We were visiting the island state of Kimbu and had come to the post-office to send off some parcels to friends at home. The island’s currency is the pim, and the postmaster told us that he had only stamps of five different face-values, as these had to be used up before a new issue of stamps was introduced.

    These stamps were black, red, green, violet, and yellow, in descending order of values, the black being the highest denomination and the yellow the lowest.

    One parcel required stamps to the value of 100 pims and we were handed 9 stamps; 5 black, one green, and 3 violet. The other two parcels required 50 pims’ worth each, and for these we were given two different sets of 9 stamps.

    One consisted of 1 black and 2 of each of the other colours, and the other set contained 5 green and 1 of each of the others.

    What would be been the smallest number of stamps needed for a 50-pim parcel, and of which colours?

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

    [teaser685]

     
    • Jim Randell's avatar

      Jim Randell 9:20 am on 28 January 2021 Permalink | Reply

      I assumed the denominations of the stamps were all positive integer values.

      We can place lower and upper limits on the values of the stamps.

      The stamps are ordered: B > R > G > V > Y, which gives minimum values of: 5, 4, 3, 2, 1.

      And 5 black stamps are used in making a value of 100, so black must have a value of less than (100 − (3 + 3×2)) / 5 = 18.2.

      Similarly, 5 green stamps are used in making a value of 50, so green must have a value of less than (50 − (5 + 4 + 2 + 1)) / 5 = 7.6.

      So we can write down the following ranges for each denomination:

      B: 5..18
      R: 4..17
      G: 3..7
      V: 2..6
      Y: 1..5

      And we have three equations relating the values. So choosing values for V and Y will give us a set of 5 simultaneous equations that can be solved to find the other values.

      This Python program use the [[ matrix.linear() ]] solver for systems of linear equations from the enigma.py library. It runs in 63ms.

      Run: [ @repl.it ]

      from enigma import (irange, subsets, matrix, as_int, express, group, printf)
      
      # choose values for Y and V
      for (Y, V) in subsets(irange(1, 6), size=2):
      
        # make the equations
        eqs = [
          # B  R  G  V  Y = ???
          ((5, 0, 1, 3, 0), 100), # 100-pim parcel
          ((1, 2, 2, 2, 2),  50), # 1st 50-pim parcel
          ((1, 1, 5, 1, 1),  50), # 2nd 50-pim parcel
          ((0, 0, 0, 0, 1),   Y), # Y value
          ((0, 0, 0, 1, 0),   V), # V value
        ]
      
        # solve the equations for integer values
        # (either matrix.linear() or as_int() may raise a ValueError)
        try:
          (B, R, G, V, Y) = (as_int(x) for x in matrix.linear(*zip(*eqs)))
        except ValueError:
          continue
      
        # check ordering
        if not (B > R > G > V > Y > 0): continue
      
        # find combinations that make 50, grouped by the total number of stamps
        d = group(express(50, [Y, V, G, R, B]), by=sum)
        # output minimal configurations
        k = min(d.keys())
        for (y, v, g, r, b) in d[k]:
          printf("{k} stamps; 50 = {b}x {B} (black), {r}x {R} (red), {g}x {G} (green), {v}x {V} (violet), {y}x {Y} (yellow)")
      

      Solution: A 50-pim parcel can be sent using 5 stamps: 2 black, 1 red, 1 green, 1 yellow.

      The values of the stamps are:

      black = 18 pim
      red = 9 pim
      green = 4 pim
      violet = 2 pim
      yellow = 1 pim

      So each stamp is half (rounded down) the next highest value.

      There are 621 different ways to make 50 using the above denominations.


      Here’s a manual derivation of the denominations:

      [1] B + 2R + 2G + 2V + 2Y = 50
      [2] B + R + 5G + V + Y = 50

      Taking 2×[2][1] gives:

      B + 8G = 50

      And we know G is in the range 3 .. 7, and B is in the range 5 .. 18 and G < B:

      G = 3 ⇒ B = 26 [not possible]
      G = 4 ⇒ B = 18
      G = 5 ⇒ B = 10
      G = 6 ⇒ B = 2 [not possible]
      G = 7 ⇒ B = −6 [not possible]

      And:

      5B + G + 3V = 100

      Where V is in the range 2 .. 6 and V < G:

      G = 4, B = 18 ⇒ V = 2
      G = 5, B = 10 ⇒ V = 15 [not possible]

      So we have: V = 2, G = 4, B = 18.

      From which we see Y = 1, and R = 9.

      Like

      • Jim Randell's avatar

        Jim Randell 5:55 pm on 28 January 2021 Permalink | Reply

        Here’s a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library. It runs in 122ms.

        It operates in base 19 as B ≤ 18.

        The only other restriction I used was that we already know that 50 is possible with 9 stamps, so I limit the numbers used for any single denomination to 8. (We can also do this in my Python program above, by passing [[ qs=irange(0, 8) ]] to the call to [[ express() ]] and that saves a little bit of time).

        We could restrict the other denominations in line with the ranges given above, but it is not necessary for a decent run time.

        Run: [ @repl.it ]

        #! python -m enigma -rr
        
        SubstitutedExpression
        
        --base=19
        --symbols="BRGVYbrgvy"
        --distinct="BRGVY"
        
        # work out the denominations
        "Y > 0"
        "V > Y"
        "G > V"
        "R > G"
        "B > R"
        
        "5 * B + G + 3 * V == 100" # 100 pim parcel
        "B + 2 * (R + G + V + Y) == 50" # 1st 50 pim parcel
        "5 * G + (B + R + V + Y) == 50" # 2nd 50 pim parcel
        
        # find minimal amount to make 50
        "b * B + r * R + g * G + v * V + y * Y == 50"
        
        --answer="((b, r, g, v, y), (B, R, G, V, Y))"
        --code="fmin = lambda ss: min(ss, key=unpack(lambda ns, ds: sum(ns)))"
        --accumulate="fmin"
        
        --verbose=16
        
        # [optional] we already know 50 is possible with 9 stamps
        --invalid="9-18,brgvy"
        

        Like

    • Frits's avatar

      Frits 12:24 pm on 28 January 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        # try to avoid looping over BL and RE
        "G > V",
        "V > Y",
        # (5, 0, 1, 3, 0)  100-pim parcel
        "(100 - G - 3*V) // 5 = BL",
        
        # (1, 2, 2, 2, 2)  1st 50-pim parcel
        "(50 - BL) // 2  - (G + V + Y) = RE",
        "RE > G",
        "BL > RE",
        # (1, 1, 5, 1, 1)  2nd 50-pim parcel
        "BL + RE + 5*G + V + Y = 50",
        
        # Pick M blacks, N reds, O greens, P violets and Q yellows
        # we already know a 50-pim parcel can be formed with 9 stamps
        "M + N < 10",
        "M + N + O < 10",
        "M + N + O + P < 10",
        "M + N + O + P + Q < 10",
        
        "M*BL + N*RE + O*G + P*V + Q*Y = 50", 
        ],
        answer="M + N + O + P + Q, M, N, O, P, Q",
        distinct="",
        accumulate=min,
        # BL: 5..18  RE: 4..12  G: 3..7  V: 2..6  Y: 1..5 
        d2i=dict([(0, "GVY")] + 
                 [(1, "VG")] + 
                 [(2, "RBG")] + 
                 [(k, "RB") for k in range(3, 6)] +
                 [(6, "YRB")] +
                 [(7, "YVRB")] +
                 [(k, "YVGRBMNOPQ") for k in range(8, 10)]),
      
        reorder=0,
        verbose=0
        )   
        
        
      # solve the puzzle
      r = p.run()
      
      colors = ["black", "red", "green", "violet", "yellow"]
      for (x, y) in zip(list(r.accumulate)[1:], colors):
        if x > 0:
          print(x, y)
      

      Like

    • GeoffR's avatar

      GeoffR 3:04 pm on 28 January 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % black, red, green, violet, yellow stamps
      % ... using Jim's analysis values for all stamp ranges
      var 5..18:B; var 4..17:R; var 3..7: G;
      var 2..6: V; var 1..5:Y;
      
      % stamp values all different and in colour descending values
      constraint all_different([B, R, G, V, Y]);
      constraint B > R /\ R > G /\ G > V /\ V > Y;
      
      % numbers of each colour of stamp to make 50 pim
      var 0..9:NumB; var 0..9:NumG; var 0..9:NumV;
      var 0..9:NumR; var 0..9:NumY;
      
      % three stamp equations stated in teaser description
      % 1. 5 black, one green, and 3 violet stamps for 100 pim
      constraint 5*B + G + 3*V == 100;
      
      % two different equations for stamps of 50 pim
      % 2. one set consisted of 1 black and 2 of each of the other colours
      constraint B + 2*R + 2*G + 2*V + 2*Y == 50;
      
      % 3. other set contained 5 green and 1 of each of other colours 
      constraint B + R + 5*G + V + Y == 50;
      
      % General equation to make 50 pims value
      constraint NumB*B + NumR*R +  NumG*G + NumV*V + NumY*Y == 50;
      
      % minimise total number of coins used to make 50 units value
      solve minimize(NumB + NumR + NumG + NumV + NumY);
      
      output [ "Black stamp value = " ++ show(B) ++ " pim"
      ++ ", Red stamp value = " ++ show(R) ++ " pim"
      ++ "\n" ++ "Green stamp value = " ++ show(G) ++ " pim"
      ++ ", Violet stamp value = " ++ show(V) ++ " pim"
      ++ "\n" ++ "Yellow stamp value = " ++ show(Y) ++ " pim"
      ++ "\n"
      ++ "\n" ++ "Smallest number of stamps needed for a 50-pim parcel" 
      ++ "\n" ++ "[B, R, G, V, Y] =  "
      ++ show (NumB),", ",show(NumR),", ",show(NumG),", ",show(NumV),", ",show(NumY) ];
      
      % Black stamp value = 18 pim, Red stamp value = 9 pim
      % Green stamp value = 4 pim, Violet stamp value = 2 pim
      % Yellow stamp value = 1 pim
      
      % Smallest number of stamps needed for a 50-pim parcel
      % [B, R, G, V, Y] =  2, 1, 1, 0, 1
      % ----------
      % ==========
      
      
      
      

      Like

    • John Crabtree's avatar

      John Crabtree 12:07 am on 29 January 2021 Permalink | Reply

      Eq. 1: 5.B + 0.R + 1.G +3.V + 0.Y = 100
      Eq. 2: 1.B + 2.R + 2.G +2.V + 2.Y = 50
      Eq. 3 : 1.B + 1.R + 5.G +1.V + 1.Y = 50
      10 * Eq. 3 – 5 * Eq. 2 – Eq. 1 gives 39.G – 3.V = 150, or 13.G – V = 50
      Assuming integer solutions, G = 4, V = 2 and then B = 18, R + Y = 10 etc

      Like

  • Unknown's avatar

    Jim Randell 8:16 am on 26 January 2021 Permalink | Reply
    Tags: ,   

    Teaser 2772: Madam I’m Adam 

    From The Sunday Times, 8th November 2015 [link] [link]

    Adam noticed that today’s Teaser number was a four-figure palindrome. He told me that he had written down [a] four-figure palindrome and he asked me to guess what it was. I asked for some clues regarding its prime factors and so he told me, in turn, whether his number was divisible by 2, 3, 5, 7, 11, 13, … Only after he had told me whether it was divisible by 13 was it possible to work out his number.

    What was his number?

    When this was originally published “another” was used where I have placed “[a]”, but this makes the puzzle unsolvable. However, as presented above, there is a unique solution.

    [teaser2772]

     
    • Jim Randell's avatar

      Jim Randell 8:17 am on 26 January 2021 Permalink | Reply

      The use of “another” in the puzzle text implies that the palindrome 2772 is excluded from the set of possibilities, but if this is the case the puzzle is not solvable. So instead we just consider that Adam has told the setter that he has written down some 4-digit palindrome (which may be 2772), and the setter has to guess it.

      This Python program considers increasing prime numbers p, and looks for palindromes that can be uniquely identified by knowing if it is divisible by primes up to (and including) p.

      To solve this puzzle we look up to p = 13, but the maximum prime can be specified on the command line. (We have to go to 859 to be sure of determining the palindrome).

      The program runs in 45ms.

      Run: [ @repl.it ]

      from enigma import Primes, irange, filter_unique, diff, join, arg, printf
      
      # max prime
      N = arg(13, 0, int)
      
      # 4-digit palindromes
      palindromes = sorted(1001 * a + 110 * b for a in irange(1, 9) for b in irange(0, 9))
      #palindromes.remove(2772) # puzzle text says 2772 is excluded, but that makes it impossible
      
      # palindromes unique by divisibility of primes, but not unique by shorter sequences
      r = set()
      # consider sequences of primes by increasing length
      ps = list()
      for p in Primes(N + 1):
        ps.append(p)
        # find unique palindromes by this sequence of primes
        s = filter_unique(palindromes, lambda x: tuple(x % p == 0 for p in ps)).unique
        # unique palindromes we haven't seen before
        s = diff(s, r)
        if s:
          printf("{p} -> {s}", s=join(s, sep=", ", enc="()"))
        # update the list of unique palindromes
        r.update(s)
      

      Solution: The number was 6006.


      If 2772 is removed from the set of possible palindromes (by removing the comment from line 8), we see that 8778 would also be uniquely identified by the divisibility values for primes up to 13.

      So although the setter would know the answer (because he knows if the number is divisible by 13 or not), we can’t work it out:

      6006 → 2=Y, 3=Y, 5=N, 7=Y, 11=Y, 13=Y
      8778 → 2=Y, 3=Y, 5=N, 7=Y, 11=Y, 13=N, [17=N, 19=Y]
      2772 → 2=Y, 3=Y, 5=N, 7=Y, 11=Y, 13=N, [17=N, 19=N]

      But, if 2772 is included in the set of possible palindromes, then 8778 and 2772 cannot be distinguished until we are told the answer for divisibility by 19, so the fact that the setter can work out the number at p = 13 means that it must be 6006.

      There are two palindromes that can be distinguished with a shorter sequence of answers:

      5005 → 2=N, 3=N, 5=Y, 7=Y
      5775 → 2=N, 3=Y, 5=Y, 7=Y

      Note that all palindromes are divisible by 11.

      Like

    • Frits's avatar

      Frits 8:08 pm on 26 January 2021 Permalink | Reply

      With fewer modulo calculations.

      from collections import Counter
       
      # max prime
      N = 13
      
      # list of prime numbers
      P = [2, 3, 5, 7, 11, 13]
       
      # 4-digit palindromes
      palindromes = sorted(1001 * a + 110 * b for a in range(1, 10) 
                                              for b in range(0, 10))
      
      # initialize dictionary (divisibility by prime)                                    
      d = {key: [] for key in palindromes}
       
      singles = []
      # consider sequences of primes by increasing length
      for p in P:
        # maintain dictionary (divisibility by prime)   
        for x in palindromes:
          d[x].append(x % p == 0)
         
        c = Counter(map(tuple, d.values()))
        # get combinations unique by divisibility of primes
        # but not unique by shorter sequences
        c2 = [k for k, v in c.items() if v == 1 and 
              all(k[:-i] not in singles for i in range(1, len(k)))]
        
        if len(c2) == 1:
          for x in palindromes:
            if d[x] == list(c2[0]):
              print(f"{p} -> {x}")
              if p != N:
                print("Error: palindrome already known before prime", N)
              exit()  
              
        # add "unique by shorter sequences" to singles
        for x in [k for k, v in c.items() if v == 1]:
          singles.append(x)
      

      Like

  • Unknown's avatar

    Jim Randell 10:07 am on 24 January 2021 Permalink | Reply
    Tags: by: A. Bowie   

    Brain-Teaser 19: Hillside Farm 

    From The Sunday Times, 9th July 1961 [link]

    My friend, Mr. Little, is a farmer whose chief interest is in sheep, but keeps a number of Shorthorn and Redpoll (polled) cattle too.

    Mr. Little is six years older than his wife and 33 years older than his son, George. George is twice as old as Mary, the daughter of the house, whose birthday happens to be today.

    There is no tractor at Hillside Farm and Mr. Little does the ploughing behind his two horses leaving a furrow nine inches wide. The number of acres ploughed this year happens to be the same as the number of cats on the farm.

    Readers are invited to determine the information required to complete the following “cross-number” puzzle:

    Across:

    1a: Number of Mr. Little’s Redpoll cows.
    3a: Square of the number of Redpoll cows.
    5a: Total number of cows.
    6a: Mr. Little’s age.
    7a: Number of ewes per ram in Mr. Little’s flock. This also happens to be the number of miles walked by Mr. Little in doing this year’s ploughing.
    9a: Acreage of rough grass land. (1.5 acres per ewe).
    11a: Age of Mrs. Little.
    12a: Number of ewes in flock (in scores).
    13a: Cube of the number of collies on the farm.

    Down:

    1d: Area of rectangular farmyard in square yards.
    2d: Length of farmyard in yards.
    3d: Number of ewes on the farm.
    4d: Age at which Mr. Little intends to retire.
    7d: Seven times Mr. Little’s age.
    8d: Total number of sheep.
    10d: Number of rams on the farm.
    12d: Total number of horns possessed by all the cattle.

    [teaser19]

     
    • Jim Randell's avatar

      Jim Randell 10:12 am on 24 January 2021 Permalink | Reply

      I didn’t need to use all the information given to solve the puzzle. But I did need to look up what “polled” meant – it means “lacking horns”.

      I used the [[ SubstitutedExpression ]] solver from the enigma.py library to solve the puzzle as a collection of alphametic expressions.

      The following run file executes in 131ms.

      Run: [ @repl.it ]

      #! python -m enigma -rr
      
      # consider the grid:
      #
      #  A B # C D E
      #  F G # H I #
      #  J # K L # M
      #  N P Q R # S
      #  # T U # V W
      #  # # # X Y Z
      
      SubstitutedExpression
      
      --distinct=""
      
      # 1a. AB = "Number of redpoll cows"
      # 3a. CDE = "Square of the number of redpoll cows"
      # 5a. FG = "Total number of cows"
      # 12d. VY = "Total number of horns possessed by all the cattle"
      "AB * AB = CDE"
      "AB < FG"
      "2 * (FG - AB) = VY"
      
      # 6a. HI = "Mr Little's age" (= wife + 6)
      # 11a. TU = "Age of Mrs. Little"
      # 7d. KQU = "Seven times Mr Little's age"
      # 4d. DI = "Age at which Mr Little intends to retire"
      "TU + 6 = HI"
      "HI * 7 = KQU"
      "HI < DI"
      
      # 7a. KL = "Number of ewes per ram."
      # 9a. NPQR = "Acreage of rough grassland. 1.5 acres per ewe"
      # 12a. VW = "Number of ewes in flock (in scores)
      # 3d. CHLR = "Number of ewes on the farm"
      # 10d. PT = "Number of rams on the farm"
      # 8d. MSWZ = "Total number of sheep"
      "VW * 20 = CHLR"
      "2 * NPQR == 3 * CHLR"
      "CHLR + PT = MSWZ"
      "PT * KL = CHLR"
      
      # 13a. XYZ = "Cube of the number of collies on the farm"
      "is_cube(XYZ)"
      
      # 1d. AFJN = "Area of farmyard in square yards"
      # 2d. BG = "Length of farmyard in yards"
      "AFJN % BG = 0"
      
      # across / down
      --template="(AB CDE FG HI KL NPQR TU VW XYZ) (AFJN BG CHLR DI KQU MSWZ PT VY)"
      --solution=""
      

      Solution: The completed grid looks like this:

      From which we get the following information

      Number of redpoll cows = 13
      Number of shorthorn cows = 36
      (Total number of cows = 49)
      (Total number of horns = 72)

      Number of ewes = 1540 (= 77× 20)
      Number of rams = 35
      (Ewes/ram = 44)
      (Total number of sheep = 1575)
      Area of rough grassland = 2310

      Mr. Little’s age = 59
      Retirement age = 69
      (Mrs. Little’s age = 53)

      Area of farmyard = 1482
      Length of farmyard = 39
      (Width of farmyard = 38)

      Number of collies = 5

      And from the text we can deduce the following further information:

      (George’s age = 26)
      (Mary’s age = 13)

      (Number of cats = 4)

      Like

    • Frits's avatar

      Frits 2:06 pm on 24 January 2021 Permalink | Reply

       
       consider the grid:
      
        A B # C D E
        F G # H I #
        J # K L # M
        N P Q R # S
        # T U # V W
        # # # X Y Z
      
      Rules:
      
      AB * AB = CDE
      AB < FG
      2 * (FG - AB) = VY
      TU + 6 = HI
      HI * 7 = KQU
      HI < DI
      VW * 20 = CHLR
      2 * NPQR = 3 * CHLR
      CHLR + PT = MSWZ
      PT * KL = CHLR
      is_cube(XYZ)
      AFJN % BG = 0
      
      
      VW * 20 = CHLR -> R=0 C=1 L=even
      AB * AB = 1DE --> A=1 
      HI>33 and DI>HI ->  CDE in (144, 169, 196)
      PT * KL = CHL0 -> T=5 or L=0
      1 mile=63360 inches, 1 acre=6272640 square inches
      no cats = KL * 63360 * 9 / 6272640 = KL / 11 
      KL multiple of 11 -> K=L, K!=0 so T=5
      CHL0 + PT = MSWZ -> Z=T so Z=5 -> XYZ=125 
      Age George is even so TU and HI are odd
      TU HI KQU
      51 57 399 U's inconsistent 
      53 59 413 OK
      55 61 427 U's inconsistent 
      57 53 441 U's inconsistent 
      59 65 455 U's inconsistent 
      So TU=53, HI=59, KQU=413 -> L=4
      VW * 20 = 1540 -> VW=77
      2 * NPQR = 3 * 1540 -> NPQR=2310
      CHLR + PT = MSWZ  -> MSWZ=1575
      2 * (FG - AB) = 72 -> FG = AB + 36
      HI=59 -> DI>59 and CDE is 169 or 196
      so AB/FG is 13/49 or 14/50
      1FJ2 % BG = 0 -> AB=13 FG=49 CDE=169
      14J2 % 39 = 0 -> J=8
      

      Like

    • Frits's avatar

      Frits 6:12 pm on 24 January 2021 Permalink | Reply

      Similar.

      from functools import reduce
       
      # convert numbers to digit sequences and vice versa
      # Credit: B. Gladman
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s) 
      n2d = lambda n: [n] if n < 10 else n2d(n // 10) + [n % 10] 
      
      # consider the grid:
      #
      #  A B # C D E
      #  F G # H I #
      #  J # K L # M
      #  N P Q R # S
      #  # T U # V W
      #  # # # X Y Z
      
      # "VW * 20 = CHLR"
      for V in range(1, 10):
        for W in range(10):
          CHLR = d2n([V, W]) * 20
          if len(str(CHLR)) != 4: continue
          (C, H, L, R) = n2d(CHLR)
          
          # "AB * AB = CDE"
          for A in range(1, 10):
            for B in range(1, 10):
              AB = d2n([A, B])
              CDE = AB * AB
              if len(str(CDE)) != 3: continue
              (c, D, E) = n2d(CDE)
              if c != C: continue
              
              # "2 * NPQR == 3 * CHLR"
              NPQR = int(1.5 * (CHLR))
              if len(str(NPQR)) != 4: continue
              (N, P, Q, r) = n2d(NPQR)
              if r != R: continue
              
              # "PT * KL = CHLR"
              for K in range(1, 10):
                PT = CHLR // d2n([K, L])
                if len(str(PT)) != 2: continue
                (p, T) = n2d(PT)
                if p != P: continue
                
                # "CHLR + PT = MSWZ"
                MSWZ = CHLR + PT
                if len(str(MSWZ)) != 4: continue
                (M, S, W, Z) = n2d(MSWZ)
                
                # "2 * (FG - AB) = VY"
                for Y in range(10):
                  FG = AB + d2n([V, Y]) // 2
                  if len(str(FG)) != 2: continue
                  (F, G) = n2d(FG)
                  
                  # "TU + 6 = HI"
                  for U in range(10):
                    HI = d2n([T, U]) + 6
                    if len(str(HI)) != 2: continue
                    (h, I) = n2d(HI)
                    if h != H: continue
                    
                    # "HI < DI"
                    if not HI < d2n([D, I]): continue
                    
                    # "HI * 7 = KQU"
                    KQU = HI * 7
                    if len(str(KQU)) != 3: continue
                    (k, q, u) = n2d(KQU)
                    if k != K or q != Q or u != U: continue
                    
                    # "is_cube(XYZ)"
                    for X in [1, 2, 3, 5, 7]:
                      XYZ = d2n([X, Y, Z])
                      if not XYZ in {125, 216, 343, 512, 729}: continue
                      
                      # "AFJN % BG = 0"
                      for J in range(10):
                        AFJN = d2n([A, F, J, N])
                        if not AFJN % d2n([B, G]) == 0: continue
                        
                        print("CHLR CDE AB PT FG HI KQU MSWZ XYZ")
                        print(CHLR, CDE, AB, PT, FG, HI, KQU, MSWZ, XYZ)
                    
                        print("A B C D E F G H I J K L")
                        print(A, B, C, D, E, F, G, H, I, J, K, L)
                        print("M N P Q R S T U V W X Z")
                        print(M, N, P, Q, R, S, T, U, V, W, X, Z)
      

      Like

  • Unknown's avatar

    Jim Randell 5:02 pm on 22 January 2021 Permalink | Reply
    Tags:   

    Teaser 3044: Building blocks 

    From The Sunday Times, 24th January 2021 [link] [link]

    The kids had used all the blocks each of them owned (fewer than 100 each) to build triangular peaks — one block on the top row, two on the next row, and so on.

    “My red one’s nicer!”

    “My blue one’s taller!”

    “Why don’t you both work together to make one bigger still?” I said.

    I could see they could use all these blocks to make another triangle.

    This kept them quiet until I heard, “I bet Dad could buy some yellow blocks to build a triangle bigger than either of ours was, or a red and yellow triangle, or a yellow and blue triangle, with no blocks ever left over.”

    This kept me quiet, until I worked out that I could.

    How many red, blue and yellow blocks would there be?

    [teaser3044]

     
    • Jim Randell's avatar

      Jim Randell 5:18 pm on 22 January 2021 Permalink | Reply

      The following Python program runs in 43ms.

      Run: [ @repl.it ]

      from enigma import tri, irange, inf, first, subsets, is_triangular, printf
      
      # generate triangular numbers from tri(a) to tri(b)
      tris = lambda a=1, b=inf: (tri(i) for i in irange(a, b))
      
      def solve():
        # collect triangular numbers less than 100
        ts = first(tris(), (lambda x: x < 100))
      
        # choose (R, B) pairs that sum to another triangular number
        RBs = list(s for s in subsets(ts, size=2) if is_triangular(sum(s)))
      
        # consider values for Y
        for Y in tris(is_triangular(min(B for (R, B) in RBs)) + 1):
          # look for R, B pairs that work with Y
          for (R, B) in RBs:
            if B < Y and is_triangular(R + Y) and is_triangular(B + Y):
              yield (R, B, Y)
      
      # look for the first solution
      for (R, B, Y) in solve():
        printf("R={R} B={B} Y={Y}")
        break
      

      Solution: There were 45 red blocks; 91 blue blocks; 990 yellow blocks.

      We have:

      R = 45 = T(9)
      B = 91 = T(13)
      R + B = 136 = T(16)

      Y = 990 = T(44)
      R + Y = 1035 = T(45)
      B + Y = 1081 = T(46)


      It makes me wish Python allowed something like a [[ while ... ]] clause in list comprehensions, so they could be terminated early:

      ts = (t for t in tris() while t < 100)
      

      This exact syntax was actually proposed [ PEP 3142 ], but not implemented.

      I’ve folded the functionality into the [[ first() ]] function in the enigma.py library, so it can take a function and collection of items will stop when the function returns a false value for an item.

      Like

      • Frits's avatar

        Frits 11:11 pm on 22 January 2021 Permalink | Reply

        @Jim, I can imagine a similar puzzle in 3D. Didn’t we have such a puzzle before?

        Like

      • Jim Randell's avatar

        Jim Randell 10:58 am on 23 January 2021 Permalink | Reply

        Here’s an exhaustive version, based on the observation that if Y = tri(y) then R ≥ y + 1, otherwise it will not be possible to complete an additional row of the Y triangle using R blocks.

        It still runs in 46ms.

        Run: [ @repl.it ]

        from enigma import tri, irange, inf, first, subsets, is_triangular, printf
        
        # generate triangular numbers from tri(a) to tri(b)
        tris = lambda a=1, b=inf: (tri(i) for i in irange(a, b))
        
        # collect triangular numbers less than 100
        ts = first(tris(), (lambda x: x < 100))
        
        # choose (R, B) pairs that sum to another triangular number
        for (R, B) in subsets(ts, size=2):
          if not is_triangular(R + B): continue
        
          # consider values for Y
          for Y in tris(is_triangular(B) + 1, R - 1):
            if is_triangular(R + Y) and is_triangular(B + Y):
              printf("R={R} B={B} Y={Y}")
        

        This code can also be used to explore larger solutions by increasing the limit at line 7.

        There is 1 further solution below 1275:

        R = T(23) = 276
        B = T(30) = 465
        Y = T(90) = 4095

        Like

    • Frits's avatar

      Frits 10:55 pm on 22 January 2021 Permalink | Reply

      Nothing has been implemented regarding upper limits of RE and BL, the program runs fast enough (between 31 ms and 46 ms).

      from enigma import SubstitutedExpression 
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        # number of reds    = RE * (RE + 1)/2
        # number of blues   = BL * (BL + 1)/2
        # number of yellows = YW * (YW + 1)/2
        "RE > 0",
        "BL > 0", 
        
        # My blue one's taller
        "BL > RE",
        
        # fewer than 100 each
        "BL * (BL + 1) // 2 < 100",
        
        # they could use all these blocks to make another triangle
        "RE * (RE + 1) // 2 + BL * (BL + 1) // 2 == JK * (JK + 1) // 2",
        
        # Dad could buy some yellow blocks to build a triangle bigger 
        # than either of ours was, or a red and yellow triangle, 
        # or a yellow and blue triangle, with no blocks ever left over
        "YW > BL",
        
        "YW * (YW + 1) // 2 + RE * (RE + 1) // 2 == CD * (CD + 1) // 2",
        "YW * (YW + 1) // 2 + BL * (BL + 1) // 2 == FG * (FG + 1) // 2",
        
        ],
        answer="RE * (RE + 1) // 2, BL * (BL + 1) // 2, YW * (YW + 1) // 2",
        distinct="",
        reorder=0,
        d2i={},
        verbose=0)   
      
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

      • Frits's avatar

        Frits 8:52 am on 23 January 2021 Permalink | Reply

        Similar.

        from enigma import SubstitutedExpression, is_square 
        
        # the alphametic puzzle
        p = SubstitutedExpression(
          [
          # If n is the mth triangular number, then n = m*(m+1)/2
          # and m = (sqrt(8n+1) - 1) / 2
        
          # number of reds    = RE * (RE + 1)/2
          # number of blues   = BL * (BL + 1)/2
          # number of yellows = YW * (YW + 1)/2
          "RE > 0",
          "BL > 0", 
          
          # My blue one's taller
          "BL > RE",
          
          # fewer than 100 each
          "BL * (BL + 1) // 2 < 100",
          
          # they could use all these blocks to make another triangle
          "is_square(4 * (BL * (BL + 1) + RE * (RE + 1)) + 1)",
          
          # Dad could buy some yellow blocks to build a triangle bigger 
          # than either of ours was, or a red and yellow triangle, 
          # or a yellow and blue triangle, with no blocks ever left over
          "YW > BL",
          
          "is_square(4 * (YW * (YW + 1) + RE * (RE + 1)) + 1)",
          "is_square(4 * (YW * (YW + 1) + BL * (BL + 1)) + 1)",
          
          ],
          answer="RE * (RE + 1) // 2, BL * (BL + 1) // 2, YW * (YW + 1) // 2",
          distinct="",
          reorder=0,
          d2i={},
          verbose=0)   
        
        for (_, ans) in p.solve():
          print(f"{ans}")
        

        Like

      • Frits's avatar

        Frits 10:53 am on 23 January 2021 Permalink | Reply

        One (ugly) statement, no imports.

        The “is_square” method “n**.5 % 1 == 0” will fail for large numbers of n (like 152415789666209426002111556165263283035677490).

        print([(r * (r + 1) // 2, b * (b + 1) // 2, y * (y + 1) // 2) 
          # Triangular root <m> of n = m*(m+1)/2  is (sqrt(8n+1) - 1) / 2
          # combinations of red and blue triangular roots and number of blocks < 100
          for (r, b) in [(c1, c2) 
            for c1 in range(1, [m for m in range(1, 50) if m * (m + 1)> 198][0] - 1) 
            for c2 in range(c1 + 1, [m for m in range(1, 50) if m * (m + 1)> 198][0]) 
            # they could use all these blocks to make another triangle
            if (4 * (c1 * (c1 + 1) + c2 * (c2 + 1)) + 1)**.5 % 1 == 0] 
          for y in range(b + 1, 99) 
          # red and yellow triangle, with no blocks ever left over
          # blue and yellow triangle, with no blocks ever left over
          if (4 * (r * (r + 1) + y * (y + 1)) + 1)**.5 % 1 == 0 and
             (4 * (b * (b + 1) + y * (y + 1)) + 1)**.5 % 1 == 0])  
        

        Like

        • Frits's avatar

          Frits 12:44 pm on 23 January 2021 Permalink | Reply

          [m for m in range(1, 50) if m * (m + 1)> 198][0]
          

          can be achieved more efficiently (probably) by:

          next(filter(lambda m: m * (m + 1)> 198, range(1, 50)))
          

          Like

  • Unknown's avatar

    Jim Randell 8:11 am on 21 January 2021 Permalink | Reply
    Tags: by: Richard Furner   

    Brain-Teaser 683: Powerless 

    From The Sunday Times, 18th August 1974 [link]

    I have here two positive single figure numbers, each less than 9. Neither is a factor of the other. I add the larger number to the smaller.

    Then, to that total I again add the original larger number, and to the new total I again add the original larger number and may, if I like, continue this process indefinitely, but never shall I obtain a total which is a “power” of any whole number whatsoever.

    What are my two numbers?

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

    [teaser683]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 21 January 2021 Permalink | Reply

      This Python program narrows down the candidate pairs until there is only one left, so if there is a solution this must be it.

      It runs in 49ms.

      Run: [ @replit ]

      from enigma import (subsets, irange, prime_factor, mgcd, unpack, printf)
      
      # check a number is _not_ an exact power
      check = lambda n, gcd=unpack(mgcd): gcd(e for (p, e) in prime_factor(n)) == 1
      
      # record totals and pairs of numbers
      ss = list((A + B, A, B) for (A, B) in subsets(irange(2, 8), size=2) if B % A != 0)
      
      # for each total that is not an exact power, add in B to the total
      while len(ss) > 1:
        ss = list((t + B, A, B) for (t, A, B) in ss if check(t))
      
      # output candidate solutions
      for (t, A, B) in ss:
        printf("A={A} B={B}")
      

      Solution: The two numbers are 6 and 8.

      To show that this is indeed a solution we need to show that (6 + 8k) can never be a non trivial exact power.

      To find a counter example, we are looking for for some positive integers k, a, n, where n ≥ 2, such that:

      6 + 8k = a^n
      2(3 + 4k) = a^n

      Clearly the LHS is a multiple of 2. So the RHS must also be a multiple of 2, so let: a = 2b.

      2(3 + 4k) = (2b)^n
      2(3 + 4k) = (2^n)(b^n)
      3 + 4k = 2^(n − 1)(b^n)

      Clearly the RHS is divisible by 2, but the LHS is not divisible by 2.

      So (6 + 8k) can never be an exact power of an integer.


      Most of the pairs drop out fairly quickly, but (3, 7) hangs in until we get to 2187 = 3 + 312×7 = 3^7.

      Leaving (6, 8) as the only remaining candidate.

      Like

      • Jim Randell's avatar

        Jim Randell 11:06 am on 21 January 2021 Permalink | Reply

        Here is a more efficient program that reuses the code I wrote for Enigma 1777 to generate powers in numerical order.

        Then for each power we remove pairs of numbers that can be used to make that power, until there is only a single pair remaining.

        Run: [ @replit ]

        from enigma import (Primes, irange, subsets, div, printf)
        
        # generate powers in numerical order (starting from 2 ** 2)
        def powers():
          base = { 2: 2 }
          power = { 2: 4 }
          maxp = 2
          primes = Primes(32, expandable=1).generate(maxp + 1)
          while True:
            # find the next power
            n = min(power.values())
            yield n
            # what powers are involved
            ps = list(p for (p, v) in power.items() if v == n)
            # increase the powers
            for p in ps:
              base[p] += 1
              power[p] = pow(base[p], p)
            # do we need to add in a new power?
            if maxp in ps:
              maxp = next(primes)
              base[maxp] = 2
              power[maxp] = pow(2, maxp)
        
        # possible pairs of numbers
        ss = list((A, B) for (A, B) in subsets(irange(2, 8), size=2) if B % A != 0)
        
        # consider increasing powers
        for n in powers():
          # remove any pairs that can make n
          ss = list((A, B) for (A, B) in ss if div(n - A, B) is None)
          if len(ss) < 2:
            printf("solution: {ss}")
            break
        

        Like

      • Frits's avatar

        Frits 8:15 pm on 21 January 2021 Permalink | Reply

        Python 3.9 introduced multiple arguments version of math.gcd.

        Without imports.

        def prime_factors(n):
          i = 2
          factors = []
          while i * i <= n:
            if n % i:
              i = 3 if i == 2 else i + 2
            else:
              n //= i
              factors.append(i)
          if n > 1:
            factors.append(n)
            
          return factors
           
        def is_a_power(n):
          pf = prime_factors(n)
          if len(pf) < 2: 
            return False
            
          # occurences of digits  
          oc = {pf.count(x) for x in set(pf)}
          if mult_gcd(list(oc)) == 1:
            return False
          return True
        
        # calculate greatest common divisor of multiple numbers  
        def mult_gcd(li):
          if len(li) == 1:
            return li[0]
        
          for i in range(len(li) - 1):
            a = li[i]
            b = li[i+1]
            while b:
              (a, b) = (b, a % b)
            
            if a == 1: return a
            
            li[i+1] = a
          return a  
        
        # record totals and pairs of numbers
        ss = [(A + B, A, B) for A in range(2, 8) for B in range(A, 9) if B % A != 0]
         
        # for each total that is not an exact power, add in B to the total
        while len(ss) > 1:
          ss = list((t + B, A, B) for (t, A, B) in ss if not is_a_power(t))
          
        # output candidate solutions
        for (t, A, B) in ss:
          print(f"A={A} B={B}")  
        

        Like

  • Unknown's avatar

    Jim Randell 8:10 am on 19 January 2021 Permalink | Reply
    Tags:   

    Teaser 2771: All Saints Day 

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

    I have written down three even numbers and then consistently replaced digits by letters with different letters used for different digits. In this way I get:

    ALL
    THE
    SAINTS

    In fact multiplying together the first two of these numbers gives the third.

    What number is my SAINT?

    [teaser2771]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 19 January 2021 Permalink | Reply

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

      The following run file executes in

      Run: [ @repl.it ]

      #! python -m enigma -rr
      
      SubstitutedExpression
      
      # all numbers are even
      --invalid="1|3|5|7|9,LES"
      
      "ALL * THE = SAINTS"
      
      --answer="SAINT"
      

      Solution: SAINT = 69357.

      Like

    • GeoffR's avatar

      GeoffR 10:40 am on 19 January 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 0..9:A; var 0..9:L; var 0..9:T; var 0..9:H; 
      var 0..9:E; var 0..9:S; var 0..9:I; var 0..9:N; 
        
      constraint A != 0 /\ T != 0 /\ S != 0;
      constraint all_different ([A,L,T,H,E,S,I,N]);
      
      var 100..999: ALL = 100*A + 11*L;
      var 100..999: THE = 100*T + 10*H + E;
      var 100000..999999: SAINTS = 100001*S + 10000*A + 1000*I + 100*N + 10*T;
      var 10000..99999: SAINT == 10000*S + 1000*A + 100*I + 10*N + T;
      
      constraint ALL mod 2 == 0 /\ THE mod 2 == 0 /\ SAINTS mod 2 == 0;
      constraint ALL * THE == SAINTS;
      
      solve satisfy;
      
      output ["SAINT = " ++ show(SAINT)
      ++"\nSum is " ++ show(ALL) ++ " x " ++ show(THE) ++ " = " ++ show(SAINTS)];
      
      % SAINT = 69357
      % ----------
      % Sum is 988 x 702 = 693576
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:31 pm on 15 January 2021 Permalink | Reply
    Tags:   

    Teaser 3043: An odd selection 

    From The Sunday Times, 17th January 2021 [link] [link]

    I wrote an odd digit in each of the sixteen cells of a four-by-four grid, with no repeated digit in any row or column, and with each odd digit appearing three or more times overall. Then I could read four four-figure numbers across the grid and four four-figure numbers down. I calculated the average of the four across numbers and the larger average of the four down numbers. Each was a whole number consisting entirely of odd digits, and each used an odd number of different odd digits.

    What were those two averages?

    [teaser3043]

     
    • Jim Randell's avatar

      Jim Randell 4:54 pm on 15 January 2021 Permalink | Reply

      Here’s a quick (to write) constructive solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It runs in 687ms.

      Run: [ @repl.it ]

      #! python3 -m enigma -rr
      
      # suppose the layout is:
      #
      #  A B C D
      #  E F G H
      #  J K L M
      #  N P Q R
      
      SubstitutedExpression
      
      --digits="1,3,5,7,9"
      
      # no repeated digit in any row or column
      --distinct="ABCD,EFGH,JKLM,NPQR,AEJN,BFKP,CGLQ,DHMR"
      
      # don't reorder the expressions (they all use all 16 symbols)
      --reorder=0
      
      # each average uses an odd number of different odd digits
      --code="avg = lambda *ns: ediv(sum(ns), len(ns))"
      --code="odds = {1, 3, 5, 7, 9}"
      --code="check = lambda s: len(s) in odds and odds.issuperset(s)"
      
      # row average = WXYZ
      "avg(ABCD, EFGH, JKLM, NPQR) = WXYZ"
      "check({W, X, Y, Z})"
      
      # col average = STUV, is greater than row average
      "avg(AEJN, BFKP, CGLQ, DHMR) = STUV"
      "STUV > WXYZ"
      "check({S, T, U, V})"
      
      # each digit appears at least 3 times
      --code="count = lambda s: all(s.count(d) > 2 for d in odds)"
      "count([A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R])"
      
      # required answer (row avg, col avg)
      --answer="(WXYZ, STUV)"
      
      # [optional] less verbose output
      --template="({ABCD}, {EFGH}, {JKLM}, {NPQR}) -> {WXYZ}, {STUV}"
      --solution=""
      

      Solution: The row average is 5159. The column average is 5951.

      There are 56 possible arrangements of digits in the grid. Here is one:

      1 9 7 5
      3 5 1 7
      5 3 9 1
      9 7 5 3

      Row average = (1975 + 3517 + 5391 + 9753) / 4 = 5159.

      Column average = (1359 + 9537 + 7195 + 5713) / 4 = 5951.

      Like

    • Frits's avatar

      Frits 12:31 pm on 16 January 2021 Permalink | Reply

      @Jim,

      First time I see a nested lambda.

      I would have used

      “check({W, X, Y, Z})” etc

      and

      –code=”check = lambda s: len(s) in odds and odds.issuperset(s)”

      Like

      • Jim Randell's avatar

        Jim Randell 1:48 pm on 16 January 2021 Permalink | Reply

        @Frits: Yes, that would probably be clearer. I’ll change it.

        I wrote that bit of code before I was using STUV and WXYZ for the averages, so I didn’t already have them broken out into separate digits.


        You can use a lambda expression to implement a sort of let clause in Python.

        So a hypothetical:

        let x=<expr1>, y=<expr2>: <expr3>
        

        construct can be implemented as:

        (lambda x, y: <expr3>)(<expr1>, <expr2>)
        

        or:

        (lambda x=<expr1>, y=<expr2>: <expr3>)()
        

        This last form is almost exactly the same as the let form, but with a few extra brackets.

        Like

    • Jim Randell's avatar

      Jim Randell 8:18 am on 17 January 2021 Permalink | Reply

      And here is a much faster solution that uses some analysis:

      If we start with the four 4-digit numbers that form the rows, we can construct a fifth 4-digit number using the missing digit from each column.

      When these five numbers are added together, each possible digit now appears in each position, so we get:

      T = 1111 × (1 + 3 + 5 + 7 + 9) = 27775

      We can then make possible actual totals by subtracting a 4-digit number composed of different odd digits from T.

      (Each possible digit appears 4 times in our collection of five 4-digit numbers, and in the actual grid we want each digit to appear at least 3 times, so we can only remove at most 1 copy of each digit).

      A viable total must be divisible by 4 to give the average, which itself must consist of an odd number of different odd digits.

      And for any pair of averages the row average must be less than the column average.

      The following program uses this technique to find possible viable pairs of averages. It turns out there is only one viable pair, so this gives us our solution.

      It runs in 44ms.

      Run: [ @repl.it ]

      from enigma import subsets, nconcat, div, nsplit, printf
      
      # allowable digits
      digits = {1, 3, 5, 7, 9}
      
      # generate possible averages for 4x 4-digit numbers
      # with no digit repeated in the same position
      def avgs():
        # if all digits appeared in each position then 4 copies
        # of each digit would be used, and the sum would be ...
        T = 1111 * sum(digits)
      
        # now delete a digit from each position
        # at least 3 copies of each digit must remain, so we can only
        # delete at most one instance of any digit
        for xs in subsets(digits, size=4, select="P"):
          # calculate the average
          avg = div(T - nconcat(xs), 4)
          if avg is None: continue
          # check it uses an odd number of different odd digits
          s = nsplit(avg, fn=set)
          if not(len(s) in digits and digits.issuperset(s)): continue
          # return the average
          yield avg
      
      # choose possible row and column averages
      for (ravg, cavg) in subsets(sorted(avgs()), size=2):
        printf("row avg = {ravg}, col avg = {cavg}")
      

      All that remains is to show that a grid can be constructed using the required averages. Fortunately my first program (above) finds many possible ways to do this.

      Like

      • Frits's avatar

        Frits 3:39 pm on 17 January 2021 Permalink | Reply

        @Jim, Very clever. I also was thinking to start with the missing digits but would probably not have come up with this solution.

        nconcat(xs) modulo 4 must be 3 but this will not help an already very fast program.

        Like

    • GeoffR's avatar

      GeoffR 12:36 pm on 17 January 2021 Permalink | Reply

      This programme is a bit verbose and an array type solution might well be shorter.
      Hovever, it gets the same answer as Jim’s first solution and runs in about the same time as that solution – about 0.7 sec.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % four-by-four grid
      %  A B C D
      %  E F G H
      %  J K L M
      %  N P Q R
      
      set of int: odds = {1,3,5,7,9};
      
      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: J; var 1..9: K; var 1..9: L; var 1..9: M; 
      var 1..9: N; var 1..9: P; var 1..9: Q; var 1..9: R; 
      
      % Row 1 digits
      constraint A in odds /\ B in odds /\ C in odds /\ D in odds;
      constraint all_different ([A,B,C,D]);
      % Row 2 digits
      constraint E in odds /\ F in odds /\ G in odds /\ H in odds;
      constraint all_different ([E,F,G,H]);
      % Row 3 digits
      constraint J in odds /\ K in odds /\ L in odds /\ M in odds;
      constraint all_different ([J,K,L,M]);
      % Row 4 digits
      constraint N in odds /\ P in odds /\ Q in odds /\ R in odds;
      constraint all_different ([N,P,Q,R]);
      
      % Each odd digit appears three or four times in the grid
      % Row 1 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], A, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], A, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], B, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], B, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], C, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], C, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], D, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], D, 4) ;
      
      % Row 2 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], E, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], E, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], F, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], F, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], G, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], G, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], H, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], H, 4) ;
      
      % Row 3 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], J, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], J, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], K, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], K, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], L, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], L, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], M, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], M, 4) ;
      
      % Row 4 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], N, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], N, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], P, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], P, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], Q, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], Q, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], R, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], R, 4) ;
      
      % Down column digits are all different
      constraint all_different([A,E,J,N]);
      constraint all_different([B,F,K,P]);
      constraint all_different([C,G,L,Q]);
      constraint all_different([D,H,M,R]);
      
      % Across numbers in grid
      var 1000..9999:ABCD = 1000*A + 100*B + 10*C + D;
      var 1000..9999:EFGH = 1000*E + 100*F + 10*G + H;
      var 1000..9999:JKLM = 1000*J + 100*K + 10*L + M;
      var 1000..9999:NPQR = 1000*N + 100*P + 10*Q + R;
      
      % Down numbers in grid
      var 1000..9999:AEJN = 1000*A + 100*E + 10*J + N;
      var 1000..9999:BFKP = 1000*B + 100*F + 10*K + P;
      var 1000..9999:CGLQ = 1000*C + 100*G + 10*L + Q;
      var 1000..9999:DHMR = 1000*D + 100*H + 10*M + R;
      
      % Each average (across and down) is a whole number(see description)
      % Average of across numbers 
      var 1000..9999: Av_Across;
      Av_Across = (ABCD + EFGH + JKLM + NPQR) div 4;
      
      % Across average digits (A1, A2, A3, A4)
      var 1..9:A1; var 1..9:A2; var 1..9:A3; var 1..9:A4;
      
      constraint A1 == Av_Across div 1000 /\ A1 mod 2 == 1;
      
      constraint A2 = Av_Across div 100 mod 10 /\ A2 mod 2 == 1;
      
      constraint A3 = Av_Across div 10 mod 10 /\ A3 mod 2 == 1;
      
      constraint A4 = Av_Across mod 10 /\ A4 mod 2 == 1;
      
      % Average of down numbers
      var 1000..9999: Av_Down;
      Av_Down = (AEJN + BFKP + CGLQ + DHMR) div 4;
      
      % Down average digits (D1, D2, D3, D4)
      var 1..9:D1; var 1..9:D2; var 1..9:D3; var 1..9:D4;
      
      constraint D1 == Av_Down div 1000 /\ D1 mod 2 == 1;
      
      constraint D2 = Av_Down div 100 mod 10 /\ D2 mod 2 == 1;
      
      constraint D3 = Av_Down div 10 mod 10 /\ D3 mod 2 == 1;
      
      constraint D4 = Av_Down mod 10 /\ D4 mod 2 == 1;
      
      % Larger average is down average
      constraint Av_Down > Av_Across;
      
      % Each average used an odd number of digits
      constraint card({A1,A2,A3,A4}) mod 2 == 1 
      /\ card({D1,D2,D3,D4}) mod 2 == 1;
      
      solve satisfy;
      
      output ["Average across = " ++ show(Av_Across)
      ++ "\nAverage down = " ++ show(Av_Down) 
      ++ "\nTypical grid:"
      ++ "\n" ++ show(ABCD) ++ "\n" ++ show(EFGH)
      ++ "\n" ++ show(JKLM) ++ "\n" ++ show(NPQR)];
      
      
      

      Like

      • Frits's avatar

        Frits 1:54 pm on 17 January 2021 Permalink | Reply

        @GeoffR,

        You don’t have to the count for each variable A-R, only for 1,3,5,7,9.
        However, the program doesn’t seem to run faster.

        % Each odd digit appears three or four times in the grid
        array[1..5] of int: nbrs = [1,3,5,7,9];
        constraint forall (i in 1..5) (count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
        N,P,Q,R], nbrs[i], 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
        N,P,Q,R], nbrs[i], 4));
        

        Like

    • GeoffR's avatar

      GeoffR 3:26 pm on 17 January 2021 Permalink | Reply

      @Frits: Good point. That will shorten the code.
      The original code took 240 ms to print the answer, and 667 msec to complete the search on my I7 laptop.

      Like

  • Unknown's avatar

    Jim Randell 8:28 am on 14 January 2021 Permalink | Reply
    Tags:   

    Teaser 2770: Football fans 

    From The Sunday Times, 25th October 2015 [link] [link]

    The clubs Barnet, Exeter, Gillingham, Plymouth, Southend and Walsall need to attract more fans. So each has persuaded one of the players Aguero, Ibrahimovic, Lampard, Neymar, Schweinsteiger and Suarez to join them. Also, each club has persuaded one of the managers Conte, Mourinho, Pellegrini, Terim, Van Gaal and Wenger to take control. For each club, if you look at the club, player and manager, then for any two of the three there are just two different letters of the alphabet that occur in both (with the letters possibly occurring more than once).

    In alphabetical order of the teams, list their new players.

    [teaser2770]

     
    • Jim Randell's avatar

      Jim Randell 8:29 am on 14 January 2021 Permalink | Reply

      The [[ grouping ]] functionality was added to the enigma.py library to solve this kind of puzzle.

      This Python program runs in 45ms.

      Run: [ @repl.it ]

      from enigma import grouping
      
      clubs = ('Barnet', 'Exeter', 'Gillingham', 'Plymouth', 'Southend', 'Walsall')
      players = ('Aguero',  'Ibrahimovic', 'Lampard', 'Neymar', 'Schweinsteiger', 'Suarez')
      managers = ('Conte', 'Mourinho', 'Pellegrini', 'Terim', 'Van Gaal', 'Wenger')
      
      grouping.solve([clubs, players, managers], grouping.share_letters(2))
      

      Solution: The new players are Lampard (Barnet), Suarez (Exeter), Aguero (Gillingham), Neymar (Plymouth), Ibrahimovic (Southend) and Schweinsteiger (Walsall).

      The groups are:

      Barnet, Lampard, Mourinho
      Exeter, Suarez, Wenger
      Gillingham, Aguero, Terim
      Plymouth, Neymar, Conte
      Southend, Ibrahimovic, Pellegrini
      Walsall, Schweinsteiger, Van Gaal

      Like

  • Unknown's avatar

    Jim Randell 9:04 am on 12 January 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 660: An efficient type 

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

    My typewriter had the standard keyboard:

    row 1: QWERTYUIOP
    row 2: ASDFGHJKL
    row 3: ZXCVBNM

    until I was persuaded by a time-and-motion expert to have it “improved”. When it came back I found that none of the letters was in its original row, though the number of letters per row remained unchanged. The expert assured me that, once I got used to the new system, it would save hours.

    I tested it on various words connected with my occupation — I am a licensed victualler — with the following results. The figures in parentheses indicate how many rows I had to use to produce the word:

    BEER (1)
    STOUT (1)
    SHERRY (2)
    WHISKY (3)
    HOCK (2)
    LAGER (2)
    VODKA (2)
    CAMPARI (2)
    CIDER (3)
    FLAGON (2)
    SQUASH (2, but would have been 1 but for a single letter)

    Despite feeling a trifle MUZZY (a word which I was able to type using two rows) I persevered, next essaying CHAMBERTIN.

    Which rows, in order, did I use?

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

    [teaser660]

     
    • Jim Randell's avatar

      Jim Randell 9:05 am on 12 January 2021 Permalink | Reply

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this puzzle. Although the program generated does not run under the standard CPython interpreter, as it exceeds the limit on statically nested blocks, but it runs fine with the PyPy interpreter, which does not have this limitation, or we can use the recently added [[ --denest ]] option, which generates code that is not as deeply nested, and allows the program to run under the standard Python interpreter.

      The following run file executes in 76ms. (Internal runtime of the generated code is 468µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --distinct=""
      --digits="1,2,3"
      
      # no letter is in its original row
      --invalid="1,QWERTYUIOP"
      --invalid="2,ASDFGHJKL"
      --invalid="3,ZXCVBNM"
      
      # number of letters per row remained unchanged
      --code="count = multiset.from_seq"
      "count([A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]) == {1: 10, 2: 9, 3: 7}"
      
      # rows used for each word
      "len({B, E, E, R}) = 1"
      "len({S, T, O, U, T}) = 1"
      "len({S, H, E, R, R, Y}) = 2"
      "len({W, H, I, S, K, Y}) = 3"
      "len({H, O, C, K}) = 2"
      "len({L, A, G, E, R}) = 2"
      "len({V, O, D, K, A}) = 2"
      "len({C, A, M, P, A, R, I}) = 2"
      "len({C, I, D, E, R}) = 3"
      "len({F, L, A, G, O, N}) = 2"
      "len({S, Q, U, A, S, H}) = 2"
      "len({M, U, Z, Z, Y}) = 2"
      
      # SQUASH is typed on one row, except for a single letter
      "sorted(count([S, Q, U, A, S, H]).values()) == [1, 5]"
      
      # answer is the rows involved in typing CHAMBERTIN
      --answer="(C, H, A, M, B, E, R, T, I, N)"
      
      # [optional] less verbose output
      --template=""
      
      # [experimental] work around static block limit
      --denest=-1  # apply fix for CPython
      

      Solution: The rows involved in typing CHAMBERTIN are: 1, 3, 1, 2, 2, 2, 2, 3, 2, 1.

      The new assignment of rows to keys is:

      Row 1: A C F G J K L N V X
      Row 2: B E I M P R W Y Z
      Row 3: D H O Q S T U

      Although we can’t tell what order the keys are in on a row.

      Liked by 1 person

      • Frits's avatar

        Frits 9:54 am on 12 January 2021 Permalink | Reply

        @Jim, repl.it isn’t working for this puzzle

        Like

        • Jim Randell's avatar

          Jim Randell 10:07 am on 12 January 2021 Permalink | Reply

          @Frits: Odd. It works for me if I’m logged in, but not if I try to run the repl anonymously. You have to jump through some hoops to get PyPy running in repl.it, so it is not surprising the end result is a bit fragile.

          Like

          • Frits's avatar

            Frits 10:21 am on 12 January 2021 Permalink | Reply

            Yesterday I updated PyPy. I didn’t realize repl.it depended on it.

            (error: Makefile:52: recipe for target ‘exec_pip’ failed). I’ll try to reinstall Pip under PyPy.

            Have you already used the 64 bit PyPy nightly build?

            Liked by 1 person

            • Jim Randell's avatar

              Jim Randell 10:49 am on 12 January 2021 Permalink | Reply

              @Frits: I don’t think your local installation of PyPy will affect repl.it. I think the problem is running PyPy under repl.it (which is not directly supported). Maybe the template I used to allow you to do this only works for the owner of the repl. (You could try forking it and see if that works for you).

              Like

            • Frits's avatar

              Frits 10:57 am on 12 January 2021 Permalink | Reply

              I have now installed the latest nightly build of PyPy ([PyPy 7.3.4-alpha0 with MSC v.1927 64 bit (AMD64)] on win32).

              It seems to be a recent error in PyPy.

               
              https://foss.heptapod.net/pypy/pypy/-/issues/3342 
              

              “pypy3 -m ensurepip” doesn’t give errors anymore but repl.it still fails.

              Like

        • Jim Randell's avatar

          Jim Randell 2:09 am on 13 January 2021 Permalink | Reply

          @Frits: It should be working now.

          Like

          • Frits's avatar

            Frits 10:53 am on 13 January 2021 Permalink | Reply

            Thanks, it works but it takes approx 30 seconds.
            The program itself takes 39ms.

            Like

      • Frits's avatar

        Frits 10:14 am on 12 January 2021 Permalink | Reply

        @Jim,

        If the S of SQUASH was the single letter in a different row the multiset line would not recognize it (it depends how you interpret “a single letter”).

        Do you know an easy way how I can execute the run file like a normal py file in a Command Prompt (without creating a main each and every time)?

        Like

        • Jim Randell's avatar

          Jim Randell 10:41 am on 12 January 2021 Permalink | Reply

          @Frits: I would have thought if the S was on a different line then typing SQUASH would involve typing 2 letters that were on a different row. But the puzzle text is open to interpretation there. To try the other interpretation you can just remove one of the S‘s from the list on line 32 and check for a [1, 4] allocation of letters. Fortunately it doesn’t change the answer.


          On Unix like operating systems the #! line tells the OS how to execute a file, so the file just needs to be executable. (And a while ago I made a command called run, to make it even easier to run such files).

          Otherwise you can just run the command manually. So for this file:

          % pypy -m enigma -rr teaser660.run
          

          Or if you haven’t put enigma.py in your $PYTHONPATH, just put them in the same directory (or specify full paths):

          % pypy enigma.py -rr teaser660.run
          

          Alternatively you can run it from the Python shell, using the [[ enigma.run() ]] function:

          % pypy
          >>>> from enigma import run
          >>>> run("teaser660.run")
          A=1 B=2 C=1 D=3 E=2 F=1 G=1 H=3 I=2 J=1 K=1 L=1 M=2 N=1 O=3 P=2 Q=3 R=2 S=3 T=3 U=3 V=1 W=2 X=1 Y=2 Z=2
          (C, H, A, M, B, E, R, T, I, N) = (1, 3, 1, 2, 2, 2, 2, 3, 2, 1) [1 solution]
          

          (I have [[ from enigma import * ]] in my .pythonrc file, so code from enigma.py is always available in interactive Python shells, and I don’t need to import them separately).

          Like

          • Frits's avatar

            Frits 11:08 am on 12 January 2021 Permalink | Reply

            @Jim, thanks.

            It doesn’t work if the filetype is py but that’s OK.

            Like

    • Frits's avatar

      Frits 11:05 am on 13 January 2021 Permalink | Reply

      @Jim, 4th run still takes 15 second, it doesn’t matter.

      I am busy with a program to solve the puzzle without using brute force but am kind of stuck….

      Like

    • Jim Randell's avatar

      Jim Randell 6:18 pm on 13 January 2021 Permalink | Reply

      People who use the [[ SubstitutedExpression ]] solver from the enigma.py library might be interested in the experimental version of enigma.py in this repl [ @repl.it ].

      It adds the denest parameter to [[ SubstitutedExpression]] (which available as --denest or -X from the command line or in run files), that changes the way the code is generated to reduce the amount of nesting in the generated code.

      The upshot of this is, if you are using [[ SubstitutedExpression ]] and you run foul of the "SyntaxError: too many statically nested blocks" issue, you can just use [[ SubstitutedExpression(..., denest=1) ]] (or add the --denest or -X parameter to a run file or on the command line).

      The run file given above, with the additional --denest parameter runs in 93ms using CPython 2.7.

      Assuming no problems show up in further testing, this will be rolled out in the next enigma.py update.

      Update: I have rolled this out in the 2021-01-14 version of enigma.py.

      Like

      • Frits's avatar

        Frits 7:05 pm on 13 January 2021 Permalink | Reply

        @Jim Thanks, it works.

        However, –verbose=256 doesn’t seem to work properly anymore.

        Like

        • Frits's avatar

          Frits 10:55 pm on 16 January 2021 Permalink | Reply

          @Jim, I don’t know if this is caused by your latest update.

          from enigma import SubstitutedExpression
          
          # the alphametic puzzle
          p = SubstitutedExpression(
            [
            "WXYZ = 3713",
            ],
            distinct="",
            digits="1,3,5,7,9",
            answer="W, XYZ",
            d2i={},
            verbose=256
            )   
          
          for (_, ans) in p.solve():  
            print(f"{ans}")
          
          

          No solution is given, see generated code:

           
          [SubstitutedExpression: replacing ({WXYZ} = 3713) -> (3713 = {WXYZ})]
          -- [code language="python"] --
          def _substituted_expression_solver1():
            try:
              _x_ = int(3713)
            except NameError:
              raise
            except:
              raise
            if _x_ >= 0:
              _Z = _x_ % 10
              if _Z != 0 and _Z != 1 and _Z != 2 and _Z != 3 and _Z != 4 and _Z != 5 and _Z != 6 and _Z != 7 and _Z != 8 and _Z != 9:
                _x_ //= 10
                _Y = _x_ % 10
                if _Y != 0 and _Y != 1 and _Y != 2 and _Y != 3 and _Y != 4 and _Y != 5 and _Y != 6 and _Y != 7 and _Y != 8 and _Y != 9:
                  _x_ //= 10
                  _X = _x_ % 10
                  if _X != 0 and _X != 1 and _X != 2 and _X != 3 and _X != 4 and _X != 5 and _X != 6 and _X != 7 and _X != 8 and _X != 9:
                    _x_ //= 10
                    _XYZ = _Z + _Y*10 + _X*100
                    _W = _x_ % 10
                    if _W != 0 and _W != 1 and _W != 2 and _W != 3 and _W != 4 and _W != 5 and _W != 6 and _W != 7 and _W != 8 and _W != 9:
                      _x_ //= 10
                      if _x_ == 0:
                        _WXYZ = _Z + _Y*10 + _X*100 + _W*1000
                        _r_ = (_W), (_XYZ)
                        yield ({ 'W': _W, 'X': _X, 'Y': _Y, 'Z': _Z }, _r_)
          

          Like

          • Jim Randell's avatar

            Jim Randell 11:08 pm on 16 January 2021 Permalink | Reply

            @Frits

            The digits parameter should be a collection of permissible digits (not a string), e.g.:

            digits=(1,3,5,7,9)
            

            Like

            • Frits's avatar

              Frits 11:40 pm on 16 January 2021 Permalink | Reply

              Thanks, I copied it from the run version (something which I normally don’t do).

              Like

    • Frits's avatar

      Frits 6:17 pm on 14 January 2021 Permalink | Reply

      Value 0b110 means letter can reside in row 1 or 2.

      from itertools import combinations as comb
      
      letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      
      orig = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
      sol  = ["ACFGJKLNVX", "BEIMPRWYZ", "DHOQSTU"]     # only used for printing
      
      common1 = [set("BEER"), set("STOUT")]
      common2 = [set("LAGER"), set("CAMPARI"), set("SHERRY"), set("HOCK"), 
                 set("VODKA"), set("FLAGON"), set("SQUASH"), set("MUZZY")]
      common3 = [set("WHISKY"), set("CIDER")]           
      
      r = {4 : "1", 2 : "2", 1 : "3"}   # row number
      
      # return 0 if all letters have been uniquely placed in a row
      def unsolved():
        c = 0
        for let in letters:
          c += d[let]  
          if d[let] not in {1, 2, 4}:
            return c
        return 0
        
      # remove <val> from letter <let> 
      def remove(let, val, text = ""):
        if d[let] & val == 0: return   # already removed
        
        d[let] = d[let] & (7 - val)
        if text != "":
          print(f"remove letter {let} from row {r[val]} {text}")
      
      # one row in common
      def cond1(s):
        # skip if all letters in <s> are known 
        if all(d[x] in {1, 2, 4} for x in s): return
        
        # check if letters in <s> have only one common bit
        common = 7
        for x in s:
          common &= d[x]
        
        if common in {1, 2, 4}:
          print(f"place letters {','.join(s)} in row {r[common]} ", end = "")
          print("(as they have to share 1 row)")
          for x in s:
            d[x] = common   # set all letters to the same value
            
      # two rows in common
      def cond2(s):
        known = [x for x in s if d[x] in {1, 2, 4}]
        rows_used = set(d[x] for x in known)
        
        if len(rows_used) == 2:
          # we have solved letters in 2 rows
          # so all letters in <s> have to be in these 2 rows
          twoRows = sum(rows_used)
          missing_row = 7 - twoRows
          
          for x in s:
            if x in known: continue
            # letter can be in either one of the 2 rows?
            if d[x] == twoRows: continue
            
            rows_used = sorted(list(rows_used), reverse=1)
            text = "(letters "+ ",".join(s) +" must occur in rows " 
            text += ", ".join(r[x] for x in rows_used) + ")"
            remove(x, missing_row, text)
       
      # three rows in common
      def cond3(s):
        # for each row check candidates
        for i in [1, 2, 4]:
          li = [x for x in s if d[x] & i]
           
          if len(li) == 1 and d[li[0]] != i:   # one candidate for row i
            print(f"place letter {li[0]} in row {r[i]} (other ", end = "")
            print(f"letters in {','.join(s)} are not possible in row {r[i]})")
            d[li[0]] = i
            return
      
      # check if all letters in a row are known
      def row_full():
        for i, n in enumerate([4, 2, 1]):
          c = 0
          for let in letters:
            if d[let] == n:
              c += 1
          if c == len(orig[i]):
            # row <i> is full so remove <i> from other candidates
            for let in letters:
              if d[let] == n: continue
              remove(let, n, "(already " + str(len(orig[i])) + 
                     " letters have to be in row " + r[n] +")")
      
      # check whether the number of letters in bottom 2 rows exceeds 16        
      def bottom2(c1 ,c2):  
        common_letters = c1 & c2  
        if len(common_letters) < 2: return False
        
        known = [x for x in common_letters if d[x] in {1, 2, 4}]
        rows_used = list(set(d[x] for x in known))
        if len(rows_used) != 1: return False
        
        bot2 = [x for x in common_letters if x != known[0] and 
                x not in orig[0] and   
                d[x] & d[known[0]] == 0] # different row from known
                
        # known[0] in one bottom row and <bot2> in the other bottom row?       
        if len(bot2) > 0:
          # suppose <bot2> is not in top row, so <bot2> and <known> encompass 
          # 2 rows, this means that all letters in c1 and c2 are in bottom 2 rows
          # check if letters in bottom 2 rows exceeds 16 (9 + 7)
          
          # count entries in 2 bottom rows if <bot2> is not in top row
          li = [x for x in letters 
                if x in orig[0] or        # the 10 QWERTYUIOP entries
                ((d[x] & 4) == 0 or       # not in top row
                  x in (c1 | c2) )]       # in c1 or in c2
                  
          max = len(orig[1]) + len(orig[2])
          if len(li) > max:      
            # put QWERTY... up front
            notTop = {x for x in orig[0]} | {x for x in letters if d[x] & 4 == 0}
            # letter <bot2[0]> can not be in bottom 2 rows
            text = "(assuming letter " + bot2[0] + " in bottom 2 rows leads to\n"
            text += "letters " + ",".join(notTop | c1 | c2) 
            text += " in bottom 2 rows (exceeding total of " + str(max) + "))"
            remove(bot2[0], 1, text)
            remove(bot2[0], 2, text)
            return True
            
        return False   
           
      # print rows  
      def report(li):
        print()
        for i, x in enumerate(li): 
          for y in x: 
            if d[y] == (1 << (2 - i)):  # letter is known
              print(f"{y} = {d[y]:<5}", end=" ")
            else:  
              print(f"{y} = {d[y]:#05b}", end=" ")   
          print()
        print()  
      
      
      d = dict()
      # initialize letters 
      for x in letters:
        d[x] = 7
        
      # remove original row number for each letter
      for i, y in enumerate(orig):
        for x in y:
          remove(x, (1 << (2-i)))
       
      prev = 0
      for loop in range(99):
        for x in common1:
          cond1(x)
        for x in common2:
          cond2(x)
        for x in common3:
          cond3(x)
         
        row_full()         # check if all letters in a row are known
         
        nr_unsolved = unsolved()
        if nr_unsolved == 0:   # are we done?
          report(sol)
          
          squash_rows = sorted(d[x] for x in "SQUASH")
          if squash_rows[0] != squash_rows[1] or \
             squash_rows[-1] != squash_rows[-2]:
            print("letters in SQUASH are all in one row but for a single letter")
          break
        
        report(sol)
       
        if nr_unsolved == prev:    # nothing has been solved in this loop 
          # check all combinations of words which reside on 2 rows
          for c1, c2 in comb(common2, 2):
            # check whether the number of letters in bottom 2 rows exceeds 16
            if bottom2(c1, c2): break
        prev = nr_unsolved
      

      Like

    • John Crabtree's avatar

      John Crabtree 1:04 am on 15 January 2021 Permalink | Reply

      Q, W, E, R, T, Y U, I, O, P (10)
      A, S, D, F, G, H, J, K, L (9)
      Z, X, C, V, B, N, M (7)

      BEER (1) B, E, R -> row 2
      STOUT (1) S, T, O, U -> row 3
      only 3 more letters can go to row 3, and from CIDER, at least one must be D or I
      LAGER (2) A, G, L -> row 1
      SQUASH (2*) Q, H -> row 3 (full except for D or I)
      first row P, W, Y – > row 2
      second row F, J, K -> row 1
      HOCK (2) C -> row 1
      CIDER (3) D -> row 3 (full), I -> row 2
      MUZZY (2) M, Z -> row 2 (full)
      third row N, X, V -> row 1

      CHAMBERTIN comes from rows 1, 3, 1, 2, 2, 2, 2, 3, 2, 1

      In this method WHISKY (3), VODKA (2), CAMPARI (2) and FLAGON (2) are redundant

      Like

      • Jim Randell's avatar

        Jim Randell 5:22 am on 15 January 2021 Permalink | Reply

        If I remove the WHISKY, VODKA, CAMPARI, FLAGON conditions in my program it finds 4 possible solutions. So they can’t all be redundant.

        But adding just CAMPARI back in gets us back to a unique solution.

        Like

        • Frits's avatar

          Frits 9:52 am on 15 January 2021 Permalink | Reply

          True, the problem lies with the CIDER (3) D …. deduction.

          Like

          • John Crabtree's avatar

            John Crabtree 3:33 pm on 15 January 2021 Permalink | Reply

            Jim and Frits, thank you. originally I used CAMPARI. In my incorrect CIDER (3) deduction I thought that E and R were in row 1. 🙂

            Like

    • GeoffR's avatar

      GeoffR 4:13 pm on 15 January 2021 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: rows = 1..3;
      
      var rows: A; var rows: B; var rows: C; var rows: D;
      var rows: E; var rows: F; var rows: G; var rows: H;
      var rows: I; var rows: J; var rows: K; var rows: L;
      var rows: M; var rows: N; var rows: O; var rows: P;
      var rows: Q; var rows: R; var rows: S; var rows: T;
      var rows: U; var rows: V; var rows: W; var rows: X;
      var rows: Y; var rows: Z; 
      
      % Standard keyboard layout
      % row1 = Q, W, E, R, T, Y, U, I, O, P (10 letters)
      % row2 = A, S, D, F, G, H, J, K, L  (9 letters)
      % row3 = Z, X, C, V, B, N, M  (7 letters)
      
      % 1st row letters must be in a different row
      constraint Q != 1 /\ W != 1 /\ E != 1 /\ R != 1
      /\ T != 1 /\ Y != 1 /\ U != 1 /\ I != 1
      /\ O != 1 /\ P != 1;
      
      % 2nd row letters must be in a different row
      constraint A != 2 /\ S != 2 /\ D != 2 /\ F != 2
      /\ G != 2 /\ H != 2 /\ J != 2 /\ K != 2 /\ L != 2;
      
      % 3rd row letters must be in a different row
      constraint Z != 3 /\ X != 3 /\ C != 3 /\ V != 3
      /\ B != 3 /\ N != 3 /\ M != 3;
      
      % Letter constraints for three new rows
      % New 1st row
      constraint count_eq ( [A,B,C,D,E,F,G,H,I,J,K,L,M,N,
      O,P,Q,R,S,T,U,V,W,X,Y,Z], 1, 10); 
      
      % New 2nd row
      constraint count_eq ( [A,B,C,D,E,F,G,H,I,J,K,L,M,N,
      O,P,Q,R,S,T,U,V,W,X,Y,Z], 2, 9);
      
      % New 3rd row
      constraint count_eq ( [A,B,C,D,E,F,G,H,I,J,K,L,M,N,
      O,P,Q,R,S,T,U,V,W,X,Y,Z], 3, 7); 
      
      % (Rows) I had to use to produce the words
      % BEER(1)
      constraint card({B,E,E,R}) == 1;
      % STOUT (1)
      constraint card ({S,T,O,U,T}) == 1;
      % SHERRY (2)
      constraint card({S,H,E,R,R,Y}) == 2;
      % WHISKY (3)
      constraint card({W,H,I,S,K,Y}) == 3;
      % HOCK (2)
      constraint card({H,O,C,K}) == 2;
      % LAGER (2)
      constraint card( {L,A,G,E,R}) == 2;
      % VODKA (2)
      constraint card({V,O,D,K,A}) == 2;
      % CAMPARI (2)
      constraint card ({C,A,M,P,A,R,I}) == 2;
      % CIDER (3)
      constraint card ({C,I,D,E,R}) == 3;
      % FLAGON (2)
      constraint card({F,L,A,G,O,N}) == 2;
      
      % SQUASH (2, but would have been 1 but for a single letter)
      
      % There are two rows for these letters. 
      % If four letters in 'QUASH' (i.e. missing first S out)
      % have a cardinality of 1, the other letter one must be
      % in a different row on its own. 
      
      constraint 
           (card ({U,A,S,H}) == 1 \/ card ({Q,A,S,H}) == 1
           \/ card ({Q,U,S,H}) == 1 \/ card ({Q,U,A,H}) == 1
           \/ card ({Q,U,A,S}) == 1);
      
      % MUZZY(2 rows)
      constraint card({M,U,Z,Z,Y}) == 2;
      
      output [ "CHAMBERTIN rows are " ++ 
      show([C,H,A,M,B,E,R,T,I,N]) ++ "\n"
      ++ "\nLETTERS : [A, B, C, D, E, F, G, H, I, J, K, L, M]"
      ++ "\nROWS :    " ++ show( [A, B, C, D, E, F, G, H, I, J, K, L, M])
      
      ++ "\nLETTERS : [N, O, P, Q, R, S, T, U, V, W, X, Y, Z]"
      ++ "\nROWS :    " ++ show([N, O, P, Q, R, S, T, U, V, W, X, Y, Z])
      ++ "\n"
      ++ "\n" ++ "New Row 1 is [A,C,F,G,J,K,L,N,V,X] " 
      ++ "\n" ++ "New Row 2 is [B,E,I,M,P,R,W,Y,Z] " 
      ++ "\n" ++ "New Row 3 is [D,H,O,Q,S,T,U]" ];
      
      % CHAMBERTIN rows are [1, 3, 1, 2, 2, 2, 2, 3, 2, 1]
      
      % LETTERS : [A, B, C, D, E, F, G, H, I, J, K, L, M]
      % ROWS      [1, 2, 1, 3, 2, 1, 1, 3, 2, 1, 1, 1, 2]
      % LETTERS : [N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
      % ROWS      [1, 3, 2, 3, 2, 3, 3, 3, 1, 2, 1, 2, 2]
      % New rows follow from above values
      % New Row 1 is [A,C,F,G,J,K,L,N,V,X] 
      % New Row 2 is [B,E,I,M,P,R,W,Y,Z] 
      % New Row 3 is [D,H,O,Q,S,T,U]
      % ----------
      % ==========
      

      Like

  • Unknown's avatar

    Jim Randell 10:25 am on 10 January 2021 Permalink | Reply
    Tags: by: P. R. Scott   

    Brain-Teaser 18: [Postal deliveries] 

    From The Sunday Times, 2nd July 1961 [link]

    Mr Simpson, who lives at No. 1453 Long Street, is a keen mathematician, and so he was most interested when, [while delivering a letter], his postman mentioned a strange coincidence. If the numbers of [any] two houses to which he made consecutive deliveries were added together, the result came to the number of the next house to which he delivered a letter.

    Mr Simpson asked him which houses he had visited, but the postman could only remember that some of them had single digits.

    To which house did the postman deliver a letter immediately before delivering Mr Simpson’s letter?

    I have changed the wording of this puzzle slightly for clarity.

    This puzzle was originally published with no title.

    [teaser18]

     
    • Jim Randell's avatar

      Jim Randell 10:26 am on 10 January 2021 Permalink | Reply

      I think the wording in this puzzle could have been made a bit clearer. (I have attempted to clarify the wording from the originally published text).

      Evidently, if we write down the numbers of the houses that the postman delivered to, in the order he delivered, we are told that for every house he visited (after the first two houses), the number of that house is the sum of the numbers of the previous two houses delivered to.

      So the numbers of the houses visited form a Fibonacci sequence. And we are told the sequence starts with two single digit numbers, and the number 1453 is in the sequence.

      This Python program runs in 45ms.

      from enigma import (irange, subsets, fib, printf)
      
      # collect solutions
      r = set()
      
      # choose a single digit number to start the sequence
      for (a, b) in subsets(irange(1, 9), size=2, select="P"):
        ns = list()
        for n in fib(a, b):
          if n > 1453: break
          ns.append(n)
          if n == 1453:
            r.add(ns[-2])
            printf("[({a}, {b}) -> {ns}]")
      
      printf("answer = {r}")
      

      Solution: The postman had just come from house number 898.

      There are in fact three possible starting pairs of house numbers:

      (2, 5) → (7, 12, 19, 31, 50, …)
      (3, 2) → (5, 7, 12, 19, 31, 50, …)
      (5, 7) → (12, 19, 31, 50, …)

      but they all end up settling down to generating the same sequence of higher valued house numbers, which does eventually reach 1453:

      …, 5, 7, 12, 19, 31, 50, 81, 131, 212, 343, 555, 898, 1453, …

      And the number immediately preceding 1453 is 898.

      As the ratio of consecutive terms in a Fibonacci sequence approaches the value of the Golden Ratio (𝛗 = (1 + √5) / 2), we can immediately calculate a good guess to the answer:

      % python
      >>> phi = 0.5 + sqrt(5, 4)
      >>> 1453 / phi
      898.0033856535972
      

      Like

    • Hugh Casement's avatar

      Hugh Casement 2:02 pm on 11 January 2021 Permalink | Reply

      So for every third house visited, the postman crossed the road and back again.
      That’s not the way our posties (male or female) work.
      I think a better puzzle could have put Simpson in an even-numbered house,
      all houses receiving post that day being on the one side of the street.

      That was a good idea to divide by phi. Repeated division, each time rounding to the nearest integer, would get us most of the way to the start of the sequence, though it would be safer after a while to take S(n) = S(n+2) – S(n+1) .
      Did we need to know that it starts with two single-digit terms? 1453/phi has a unique value!

      Like

    • John Crabtree's avatar

      John Crabtree 4:26 pm on 11 January 2021 Permalink | Reply

      Good question, but I think that the answer is yes. Otherwise you can get shorter sequences which reach 1453, ie:
      1453, 897, 556, 341, 215, 126, 89, 37, 52
      1453, 899, 554, 345, 209, 136, 73, 63, 10
      The ratio of successive terms oscillates about phi, and converges quite slowly.

      Like

    • Hugh Casement's avatar

      Hugh Casement 10:24 am on 12 January 2021 Permalink | Reply

      Thanks, John. The best approximations to phi are given by the original Fibonacci sequence.
      So if we double each term we get numbers all on the same side of the street.
      But perhaps the puzzle would then be too easy!

      Like

  • Unknown's avatar

    Jim Randell 4:56 pm on 8 January 2021 Permalink | Reply
    Tags:   

    Teaser 3042: A question of scale 

    From The Sunday Times, 10th January 2021 [link] [link]

    The modernist music of Skaredahora eschewed traditional scales; instead he built scales up from strict mathematical rules.

    The familiar major scale uses 7 notes chosen from the 12 pitches forming an octave. The notes are in (1) or out of (0) the scale in the pattern 101011010101, which then repeats. Six of these notes have another note exactly 7 steps above (maybe in the next repeat).

    He wanted a different scale using 6 notes from the 12 pitches, with exactly two notes having another note 1 above, and one having another 5 above. Some notes could be involved in these pairings more than once.

    His favourite scale was the one satisfying these rules that came first numerically when written out with 0s & 1s, starting with a 1.

    What was Skaredahora’s favourite scale?

    [teaser3042]

     
    • Jim Randell's avatar

      Jim Randell 5:14 pm on 8 January 2021 Permalink | Reply

      By choosing the excluded pitches we ensure that the scales are generated in the required order, so we only need to find the first scale that satisfies the specified condition.

      This Python program runs in 45ms.

      Run: [ @repl.it ]

      from enigma import (subsets, irange, join, printf)
      
      # count notes with an interval of k
      interval = lambda ps, k: sum((p + k) % 12 in ps for p in ps)
      
      # choose six notes to exclude
      for xs in subsets(irange(1, 11), size=6):
        ps = tuple(p for p in irange(0, 11) if p not in xs)
      
        # count notes with an interval of 1 and 5
        if not (interval(ps, 1) == 2 and interval(ps, 5) == 1): continue
      
        # the solution is the first value we find
        printf("{s}", s=join("01"[i in ps] for i in irange(0, 11)))
        break
      

      Solution: Skaredahora’s favourite scale is 100010101110.

      We can consider this as a binary representation of the number 2222.

      There are 12 possible scales starting with 1 that have exactly 2 notes with another note 1 above, and exactly one having another note 5 above.

      There are also 12 possible scales starting with 0.

      Like

      • Jim Randell's avatar

        Jim Randell 11:04 am on 9 January 2021 Permalink | Reply

        Here is another solution that treats the scales as numbers represented in binary.

        We can use the [[ bit_permutations() ]] function from the enigma.py library (described here [link]) to generate patterns with 6 bits set in lexicographic order.

        It runs in 45ms overall, and has an internal runtime of 55µs.

        Run: [ @repl.it ]

        from enigma import (bit_permutations, dsum2, printf)
        
        # count notes with an interval of k
        def interval(n, k):
          m = n << (12 - k) # notes shifted up an interval of k
          v = n | (n << 12) # wrapped values
          return dsum2(v & m) # count the set bits
          
        # choose a bit pattern with 6 of the 12 digits set
        for n in bit_permutations(0b100000011111, 0b111111111111):
          # count notes with an interval of 1 and 5
          if interval(n, 1) == 2 and interval(n, 5) == 1:
            printf("{n} -> {n:012b}")
            # we only need the first value
            break
        

        And here is an “unrolled” version that has an internal runtime of only 24µs [ link ].

        Like

        • Tony Brooke-Taylor's avatar

          Tony Brooke-Taylor 12:54 pm on 9 January 2021 Permalink | Reply

          I tried the binary approach first and was annoyed that this took more than twice as long as your first solution Jim. Having swapped other bits of my algorithm for yours I think the biggest time difference comes from this: your first approach generates trials with only the 6 notes in; my binary approach generates trials with all 12 positions included, and then I have to use an if statement or similar to test only the 6.

          Like

    • Brian Gladman's avatar

      Brian Gladman 10:24 am on 9 January 2021 Permalink | Reply

      You get the speed record this week Jim. And you can gain a few percent more by using a set for in place of a tuple. Maybe I am being stupid but it was not immediately obvious to me why lexically sorted zero digit positions produced numerically sorted values.

      Like

      • Jim Randell's avatar

        Jim Randell 11:02 am on 9 January 2021 Permalink | Reply

        @Brian: The lowest number will have the largest leftmost grouping of 0’s. And fortunately combinations are generated in lexicographic order, so that gives us what we want.

        Like

    • Frits's avatar

      Frits 12:22 pm on 9 January 2021 Permalink | Reply

      As the puzzle is not yet fully clear to me I publish a solution which does the same as Jim’s code.
      I tried to use different and probably less efficient code.

      check1 = lambda s: sum(y == (x + 1) % 12 \
                             for (x, y) in list(zip(s, s[1:] + s[:1])))
      
      check5 = lambda s: sum(y - x == 5 or 12 + x - y == 5 
                             for x in s for y in s if y > x)    
      
      # loop backwards as first found solution will have maximal value
      # and thus the 2nd 1 bit will be as far to the right as possible
      for A in range(7, 0, -1):
        for B in range(8, A, -1):
          for C in range(9, B, -1):
            for D in range(10, C, -1):
               for E in range(11, D, -1):
                 if check1([0, A, B, C, D, E]) != 2: continue
                 if check5([0, A, B, C, D, E]) != 1: continue
                 
                 # print solution
                 for k in range(12):
                   print("1", end="") if k in {0, A, B, C, D, E} \
                                      else print("0", end="")
                 print()  
                 exit()
      

      Like

      • Frits's avatar

        Frits 1:00 pm on 9 January 2021 Permalink | Reply

        check5 can also be written as:

        check5 = lambda s: sum(y - x in {5, 7} for x in s for y in s if y > x)
        

        Like

    • Hugh Casement's avatar

      Hugh Casement 1:24 pm on 17 January 2021 Permalink | Reply

      Using Basic (oh horror!) I found the same solution as Jim.
      Five other scales can be found by transposing the scale up or down by appropriate numbers of semitones, always with 1 in first position. Each can be reversed to give the other six.

      I think the solution corresponds to C E F# G# A A#.
      Some of those sharps may be better designated as flats of the note above
      (though in an equitempered scale it makes no difference).

      Five semitones apart is the interval known as a fourth; that leaves seven semitones, known as a fifth, to the octave note. Those are generally considered the most harmonious intervals, so a person with normal hearing would want as many as possible in the scale. I bet S’s so-called music sounds even worse than the stuff they play in the supermarket so that we don’t linger over our shopping a moment longer than necessary. But then I’ve always said that the only good composer is a dead composer — preferably one who’s been dead for a couple of centuries.

      Like

  • Unknown's avatar

    Jim Randell 8:47 am on 7 January 2021 Permalink | Reply
    Tags:   

    Teaser 2768: In the bag 

    From The Sunday Times, 11th October 2015 [link] [link]

    A set of snooker balls consists of fifteen reds and seven others. From my set I put some [of the balls] into a bag. I calculated that if I picked three balls out of the bag simultaneously at random, then there was a one in a whole-number chance that they would all be red. It was more likely that none of the three would be red – in fact there was also a one in a whole-number chance of this happening.

    How many balls did I put in the bag, and how many of those were red?

    [teaser2768]

     
    • Jim Randell's avatar

      Jim Randell 8:48 am on 7 January 2021 Permalink | Reply

      (See also: Enigma 1551).

      This Python program runs in 47ms.

      Run: [ @repl.it ]

      from itertools import product
      from enigma import Rational, irange, multiply, printf
      
      F = Rational()
      
      # probability of choosing n balls with the same quality from t
      # where q of the t balls have the quality
      def P(n, t, q):
        return multiply(F(q - i, t - i) for i in irange(0, n - 1))
      
      # choose the number of non-red and red balls
      for (n, r) in product(irange(3, 7), irange(3, 15)):
        # there are r red and n non-red balls
        t = r + n
      
        # probability of 3 being red
        p1 = P(3, t, r)
      
        # probability of 3 being non-red
        p2 = P(3, t, n)
      
        # check the conditions
        if p1.numerator == 1 and p2.numerator == 1 and p1 < p2:
          printf("n={n} r={r}, t={t}, p1={p1} p2={p2}")
      

      Solution: There were 10 balls in the bag, 4 of them were red.

      So, the bag contained 6 red balls, and 4 non-red balls.

      When picking three balls, there is a 1/30 chance that they are all red, and a 1/6 chance of them all being non-red.

      Like

    • Frits's avatar

      Frits 6:37 pm on 7 January 2021 Permalink | Reply

      from enigma import gcd
      
      # pick at least one red
      for R in range(1, 7):
        # pick more others than reds
        for T in range(2 * R + 1, 14):
          den = T * (T - 1) * (T - 2)
         
          num = R * (R - 1) * (R - 2) 
          if num != gcd(num, den): continue
         
          num = (T - R) * (T - R - 1) * (T - R - 2)
          if num != gcd(num, den): continue
      
          print(f"{T} balls in the bag, {R} of them were red ")
          
      # 10 balls in the bag, 4 of them were red   
      

      Like

  • Unknown's avatar

    Jim Randell 9:29 am on 5 January 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 658: Fiendish device 

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

    “Moriarty speaking”, said the voice on the telephone to the Prime Minister. “As you have rejected my demands, a hidden bomb with destroy London. I’m particularly pleased with the detonating device”, he went on, chuckling fiendishly, “it’s designed to give me time to get away before the explosion. There are 60 switches (all turned OFF at the moment) arranged in a ring so that No. 60 is next to No. 1. Whenever any switch changes from ON to OFF it causes the following switch to change over virtually instantaneously (from OFF to ON or vice-versa). As soon as I put down this phone I’ll activate the device. This will automatically put switch No. 1 to ON, then one minute later to OFF, then one minute later still to ON, carrying on in this way after each minute changing switch No. 1 over. As soon as every switch has remained in the OFF position for 10 seconds simultaneously the bomb explodes. So goodbye now — for ever!”

    The Prime Minister turned anxiously to Professor D. Fuse who had been listening in. “When will the activating device set off the bomb?” he asked.

    What was the Professor’s reply?

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

    [teaser658]

     
    • Jim Randell's avatar

      Jim Randell 9:29 am on 5 January 2021 Permalink | Reply

      If there were only 3 switches, this what would happen:

      1 2 3
      1 0 0 (0m)
      0 1 0 (1m)
      1 1 0 (2m)
      0 0 1 (3m)
      1 0 1 (4m)
      0 1 1 (5m)
      1 1 1 (6m)
      1 0 0 (7m)

      If we read the switches backwards we see the system of switches operates as a binary counter starting at 001 (= 1) and counting up to 111 (= 7) after 6 minutes.

      After 7 minutes the counter would reach 8 = (000 + overflow), but the switches are arranged in a circle, so the overflow bit feeds into the least significant bit of the counter, so state 8 corresponds to state 1 (= 001) and we are back where we started.

      The counter cycles through the non-zero states 1-7 every 7 minutes, so never achieves state 0 and the bomb will never go off.

      So, with 60 switches we have a 60-bit counter which cycles through the states 1 – (2^60 − 1) every 1152921504606846975 minutes (= 2.2e+12 years), without ever reaching zero.

      Solution: The Professor’s reply is: “Never”.

      Like

  • Unknown's avatar

    Jim Randell 8:39 am on 3 January 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 17: [Knight’s tour] 

    From The Sunday Times, 25th June 1961 [link]

    In “Amusements in Mathematics” (Nelson, 1917), the late Henry Ernest Dudeney published a magic knight’s tour of the chessboard. That is to say, a knight placed on the square numbered 1 could, by ordinary knight’s moves, visit every square of the board in the ordered numbered, and the numbers themselves in each row and column added up to 260. Yet it was not a fully magic square, for the diagonals did not add to the same constant. After much trying Dudeney came to the conclusion that it is not possible to devise such a square complete with magic diagonals, but, as he said, a pious opinion is not a proof.

    You are invited to try your skill in devising a magic knight’s tour of a square 7×7, with or without magic diagonals.

    Dudeney’s Amusements in Mathematics is available on Project Gutenberg [link].

    This puzzle was originally published with no title.

    [teaser17]

     
    • Jim Randell's avatar

      Jim Randell 8:39 am on 3 January 2021 Permalink | Reply

      A 7×7 chessboard has 49 squares. So if we colour the corners black there will be 25 black squares and only 24 white squares. So the knight will have to start on a black square, and end on a black square. (So it is not possible for a tour to form a closed loop).

      So, 1 is on a black square. The next square in the tour will be a white square (as a knight always moves to a square of the other colour). So, 2 will be on a white square, 3 on a black square, etc. When the tour is complete all the black squares will be odd numbers and all the white squares will be even numbers.

      The rows/columns alternate between (4 black + 3 white) and (4 white + 3 black) squares, but the total of a row/column consisting of (4 black + 3 white) numbers will be (4 odd + 3 even) = even. And the total of a row/column consisting of (4 white + 3 black) will be (4 even + 3 odd) = odd. And we require the sum of each row and column to be the same (it should be T(49) / 7 = 175),

      So it will not be possible for us to make a magic squares as the sums of the values in the rows and columns will necessarily alternate between even and odd, so they can’t all be the same.

      Hence it is not possible to construct a magic knight’s tour on a 7×7 board.

      Like

    • Hugh Casement's avatar

      Hugh Casement 1:35 pm on 3 January 2021 Permalink | Reply

      Dudeney was by no means the first: see
      https://www.mayhematics.com/t/1d.htm
      https://mathworld.wolfram.com/MagicTour.html

      It has been proved (by computer) to be impossible on an 8×8 board,
      though there are many semi-magic tours.

      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