Tagged: by: Angela Newing Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:25 pm on 28 January 2022 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 3097: Crazy golf 

    From The Sunday Times, 30th January 2022 [link] [link]

    Ian was trying to entertain John and Ken, his 2 young nephews, in the local park, which had a 9-hole crazy golf course. He decided to play a round with each of them separately, and gave them money for each hole he lost on a rising scale. He would pay £1 if he lost the first hole (or the first hole after winning one), then £2 if he lost the next consecutive hole, and £3 for the third, and so on.

    In the event, Ian won only 5 holes in total between the two rounds, including the first hole against John and the last hole against Ken. There were no ties. At the reckoning after both rounds, both boys received equal amounts of money.

    How much did it cost Uncle Ian?

    [teaser3097]

     
    • Jim Randell's avatar

      Jim Randell 4:47 pm on 28 January 2022 Permalink | Reply

      Each boy loses a hole at one end of the course, so we can just consider course with 8 holes, as we are only interested in groups of consecutive wins.

      This Python program runs in 47ms.

      Run: [ @replit ]

      from enigma import (irange, subsets, intersect, printf)
      
      # consider a course with 8 holes
      holes = irange(1, 8)
      
      # calculate the winnings for a nephew who wins holes <ws>
      def winnings(ws):
        (t, h) = (0, 0)
        for n in holes:
          if n in ws:
            h += 1
            t += h
          else:
            h = 0
        return t
      
      # choose 5 to 8 extra holes for J and K
      X = dict()
      for n in irange(5, 8):
        X[n] = set(winnings(ws) for ws in subsets(holes, size=n))
      
      # consider number of holes won by J
      for j in irange(5, 8):
        k = 13 - j
        # look for equal amounts
        for w in intersect([X[j], X[k]]):
          printf("I pays {t} [J wins {j}; K wins {k}]", t=w + w)
      

      Solution: Ian paid £32 to the boys.

      Ian lost to one of the boys on 5 consecutive holes and 1 other, paying (1 + 2 + 3 + 4 + 5) + 1 = £16.

      And to the other boy on 4 consecutive holes and another 3 consecutive holes, playing (1 + 2 + 3 + 4) + (1 + 2 + 3) = £16.

      So, in total Ian pays £32 for the 13 holes he lost.

      From 8 holes there are 6 ways that runs of 5 and 1 can be made:

      (1 2 3 4 5) (7)
      (1 2 3 4 5) (8)
      (2 3 4 5 6) (8)
      (1) (3 4 5 6 7)
      (1) (4 5 6 7 8)
      (2) (2 5 6 7 8)

      And there are 2 ways that runs of 4 and 3 can be made:

      (1 2 3 4) (6 7 8)
      (1 2 3) (5 6 7 8)

      So if John wins 5+1 and Ken wins 4+3 there are 12 possibilities.

      And if John wins 4+3 and Ken wins 5+1 there are also 12 possibilities.

      Giving 24 possible ways to achieve the required games.


      Manually, we see that the boys won 13 holes between them, so the possible splits are (8, 5) or (7, 6).

      Winning 8 holes would give a prize of T(8) which must be more than the winnings from 5 holes. So the splits are (7, 6).

      Possible consecutive runs for 7 (and corresponding prizes) are:

      (7) → T(7) = 28
      (1, 6) → T(1) + T(6) = 22
      (2, 5) → T(2) + T(5) = 18
      (3, 4) → T(3) + T(4) = 16

      Possible runs for 6 (and corresponding prizes) are:

      (6) → T(6) = 21
      (1, 5) → T(1) + T(5) = 16
      (2, 4) → T(2) + T(4) = 13
      (3, 3) → T(3) + T(3) = 12
      (1, 1, 4) → T(1) + T(1) + T(4) = 12
      (1, 2, 3) → T(1) + T(2) + T(3) = 10
      (2, 2, 2) → T(2) + T(2) + T(2) = 9

      The only overlap is for a prize of £16, with consecutive runs of (3, 4) and (1, 5).

      Like

      • Jim Randell's avatar

        Jim Randell 11:04 am on 29 January 2022 Permalink | Reply

        Here’s an alternative approach, which instead of generating the numbers of the holes won, finds possible runs of wins among the 8 holes.

        It is slightly faster than my original solution (above).

        Run: [ @replit ]

        from enigma import (irange, decompose, tri, intersect, printf)
        
        # generate possible runs of <n> wins for <k> holes
        def generate(n, k):
          # split n into j parts
          for j in irange(1, k + 1 - n):
            yield from decompose(n, j, sep=0)
        
        # consider winning runs totalling 5-8 holes on an 8 hole course
        X = dict()
        for n in irange(5, 8):
          X[n] = set(sum(tri(x) for x in ws) for ws in generate(n, 8))
        
        # consider number of holes won by J
        for j in irange(5, 8):
          k = 13 - j
          # look for equal winnings
          for w in intersect([X[j], X[k]]):
            printf("I pays {t} [J wins {j}; K wins {k}]", t=w + w)
        

        Like

  • Unknown's avatar

    Jim Randell 11:18 am on 16 December 2021 Permalink | Reply
    Tags: by: Angela Newing,   

    Teaser 2844: Children’s children 

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

    The head teacher’s retirement celebration was attended by many former pupils. These included Adam, Brian, Colin and David, whose sons and grandsons had also attended the school – actually different numbers of grandsons for each of them, with Adam having the most. Their sons were Eric, Fred, George, Harry and Ivan, and the sons of the sons were John, Keith, Lawrence, Michael, Norman and Oliver.

    Altogether Adam and Brian had sons Eric, Fred and one other; altogether Adam and Colin had sons George and one other; altogether Eric, George and Harry had sons Keith, Michael, Norman and one other; and altogether Harry and Ivan had sons Lawrence, Norman and one other.

    The retirement gift was presented by the fathers of John’s and Oliver’s fathers.

    Who were they?

    As stated there are multiple solutions to this puzzle.

    This puzzle was not included in the published collection of puzzles The Sunday Times Brainteasers Book 1 (2019).

    There are now 600 puzzles available on S2T2.

    [teaser2844]

     
    • Jim Randell's avatar

      Jim Randell 11:19 am on 16 December 2021 Permalink | Reply

      As stated the puzzle has multiple solutions.

      The following Python program builds the 8 possible family trees that fit the described situation.

      It runs in 85ms.

      Run: [ @replit ]

      from enigma import (subsets, peek, product, map2str, join, printf)
      
      # map ks to vs
      def make_map(ks, vs):
        for ss in subsets(ks, size=len(vs), select='M'):
          d = dict((k, list()) for k in ks)
          for (k, v) in zip(ss, vs):
            d[k].append(v)
          yield d
      
      # find viable father -> sons mappings
      def check_fs(fs):
      
        # "A, B have sons E, F and one other"
        s = fs['A'] + fs['B']
        if not (len(s) == 3 and 'E' in s and 'F' in s): return False
      
        # "A, C have sons G and one other"
        s = fs['A'] + fs['C']
        if not (len(s) == 2 and 'G' in s): return False
      
        # looks OK
        return True
      
      # collect viable maps
      ffs = list(fs for fs in make_map('ABCD', 'EFGHI') if check_fs(fs))
      
      # find viable son -> grandsons maps
      def check_sg(sg):
          
        # "E, G, H have sons K, M, N and one other"
        s = sg['E'] + sg['G'] + sg['H']
        if not (len(s) == 4 and 'K' in s and 'M' in s and 'N' in s): return False
      
        # "H, I have sons L, N and one other"
        s = sg['H'] + sg['I']
        if not (len(s) == 3 and 'L' in s and 'N' in s): return False
      
        # looks OK
        return True
      
      # collect viable maps
      sgs = list(sg for sg in make_map('EFGHI', 'JKLMNO') if check_sg(sg))
      
      # find the father of x in map d
      def father(x, d):
        return peek(k for (k, v) in d.items() if x in v)
      
      # choose the maps
      for (fs, sg) in product(ffs, sgs):
      
        # A, B, C, D have different numbers of grandsons
        s = list(sum(len(sg[s]) for s in fs[f]) for f in 'ABCD')
        if len(set(s)) != 4: continue
        # A has the most
        if s[0] != max(s): continue
      
        # grandfather of J and O
        gJ = father(father('J', sg), fs)
        gO = father(father('O', sg), fs)
        # they must be different people
        if gJ == gO: continue
      
        # output solution
        f = lambda s: join(s, sep="+")
        fmt = lambda m: map2str((k, f(v)) for (k, v) in m.items())
        printf("{gs} [grandfather(J) = {gJ}; grandfather(O) = {gO}]", gs=f(sorted([gJ, gO])))
        printf("  father -> sons: {fs}", fs=fmt(fs))
        printf("  son -> grandsons: {sg}", sg=fmt(sg))
        printf()
      

      Solution: One of the presenters is Adam, but the other can be any of: Brian, Colin, or David.

      The originally published solution is: “Adam and David”, but a later correction was made (with Teaser 2846) noting there are three valid solutions to the puzzle (given above).


      However, there is a unique solution if the end of the puzzle is changed to:

      The retirement gift was presented by the paternal grandfather of John and Oliver.

      Who was he?

      We can change the sense of the test at line 68 to ensure that the grandfathers of John and Oliver are the same person, and we find there are 2 possible family trees that lead to this situation, but in both cases the grandfather is Brian.

      Like

    • Hugh+Casement's avatar

      Hugh+Casement 8:34 am on 17 December 2021 Permalink | Reply

      Without trying Jim’s program, I think it’s like this:
      A’s son is H whose sons are L, N, and either K or M. D’s son is I who has no son.
      C’s son is G whose son is M or K (whichever is not H’s son).
      Brian has sons E (who has no sons) and F who is the father of J and O.

      Like

  • Unknown's avatar

    Jim Randell 5:23 pm on 24 September 2021 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 3079: Hall of residence 

    From The Sunday Times, 26th September 2021 [link] [link]

    Oak Hall at Woodville University has groups of five study bedrooms per flat and they share a kitchen/diner. In one flat live language students Andy, Bill, Chris, Dave and Ed. Bill, whose home town is Dunstable is reading French. The person in room 5 comes from Colchester and Dave comes from Brighton. The chap reading German has the room with a number one greater than the man from Gloucester. Chris occupies room 3, and Ed is reading Italian. The man in room 2 is reading Spanish, and the man reading English has a room whose number is two different from the student from Reigate.

    What is Andy’s subject and where is his home?

    [teaser3079]

     
    • Jim Randell's avatar

      Jim Randell 9:42 pm on 24 September 2021 Permalink | Reply

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to assign room numbers to the names, subjects and towns according to the given conditions.

      This run-file executes in 68ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # assign room numbers 1-5 to each of the groups:
      #
      #   Andy(A) Bill(B) Chris(C) Dave(D) Ed(E)
      #   English(F) French(G) German(H) Italian(I) Spanish(J)
      #   Brighton(K) Colchester(L) Dunstable(M) Gloucester(N) Reigate(P)
      
      SubstitutedExpression
      
      --digits="1-5"
      --distinct="ABCDE,FGHIJ,KLMNP"
      
      # "Bill, whose home town is Dunstable is reading French"
      "B = M"
      "B = G"
      
      # "The person in room 5 comes from Colchester"
      "L = 5"
      
      # "Dave comes from Brighton"
      "D = K"
      
      # "The chap reading German has the room with a number one greater than the man from Gloucester"
      "N + 1 = H"
      
      # "Chris occupies room 3"
      "C = 3"
      
      # "Ed is reading Italian"
      "E = I"
      
      # "The man in room 2 is reading Spanish"
      "J = 2"
      
      # "The man reading English has a room whose number is two different from the student from Reigate"
      "abs(F - P) = 2"
      
      # mention A (so it gets assigned a value)
      "A > 0"
      
      --template="(A B C D E) (F G H I J) (K L M N P)"
      --solution=""
      
      

      Solution: Andy is reading Spanish, and is from Gloucester.

      The complete allocations are:

      Room 1: Dave, English, Brighton
      Room 2: Andy, Spanish, Gloucester
      Room 3: Chris, German, Reigate
      Room 4: Bill, French, Dunstable
      Room 5: Ed, Italian, Colchester


      Manually we find that there are only two possible room values for the (Bill/French/Dunstable) group. Trying both possible values quickly leads to a solution in one case and a contradiction in the other.

      Like

      • Jim Randell's avatar

        Jim Randell 4:20 pm on 25 September 2021 Permalink | Reply

        I’ve seen people talk about solving these kind of puzzles like a jigsaw, and as it only takes a few minutes to write a program or solve this one manually, I thought it might be interesting to look at transforming the constraints into an “exact cover” problem.

        It’s a bit more work, but it does run quickly:

        from enigma import union, irange, chunk, update, join, printf
        
        ###############################################################################
        
        # in-place algorithmX implementation (X, soln are modified)
        def algorithmX(X, Y, soln):
          if not X:
            yield soln
          else:
            c = min(X.keys(), key=lambda k: len(X[k]))
            # copy X[c], as X is modified (could use sorted(X[c]) for stability)
            for r in list(X[c]):
              soln.append(r)
        
              # cols = select(X, Y, r)
              cols = list()
              for j in Y[r]:
                for i in X[j]:
                  for k in Y[i]:
                    if k != j:
                      X[k].remove(i)
                cols.append(X.pop(j))
        
              yield from algorithmX(X, Y, soln)
        
              # deselect(X, Y, r, cols)
              for j in reversed(Y[r]):
                X[j] = cols.pop()
                for i in X[j]:
                  for k in Y[i]:
                    if k != j:
                      X[k].add(i)
        
              soln.pop()
        
        # input: ss = sequence of collections of sets [ [a0, a1, ...], [b1, b2, ...], [c1, c2, ...] ... ]
        # output: sequence of sets (a, b, c, ...) one from each collection
        def exact_cover(sss):
          # map elements to indices
          xs = sorted(union(union(ss) for ss in sss))
          n = len(xs)
          m = dict((x, i) for (i, x) in enumerate(xs))
        
          # set up Y, one row for each position
          Y = list()
          for (j, ss) in enumerate(sss, start=n):
            for s in ss:
              y = list(m[x] for x in s)
              y.append(j)
              Y.append(y)
        
          # set up X as a dict of sets
          X = dict((k, set()) for k in irange(0, j))
          for (i, y) in enumerate(Y):
            for k in y:
              X[k].add(i)
        
          # find exact covers using algorithmX
          k = len(sss)
          for rs in algorithmX(X, Y, list()):
            # turn the selected rows of Y, back into sets
            r = [None] * k
            for i in rs:
              y = Y[i]
              r[y[-1] - n] = list(xs[i] for i in y[:-1])
            yield r
        
        ###############################################################################
        
        # we fit the pieces into a grid:
        #
        #  1:  0   1   2
        #  2:  3   4   5
        #  3:  6   7   8
        #  4:  9  10  11
        #  5: 12  13  14
        
        # each piece represents a constraint
        pieces = [
        
          # "Bill, whose home town is Dunstable is reading French" [Bill, French, Dunstable]
          [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [12, 13, 14]],
        
          # "The person in room 5 comes from Colchester" [Colchester]
          [[14]],
        
          # "Dave comes from Brighton" [Dave, Brighton]
          [[0, 2], [3, 5], [6, 8], [9, 11], [12, 14]],
        
          # "The chap reading German has the room with a number one greater than the man from Gloucester" [German, Gloucester]
          [[4, 2], [7, 5], [10, 8], [13, 11]],
        
          # "Chris occupies room 3" [Chris]
          [[6]],
        
          # "Ed is reading Italian" [Ed, Italian]
          [[0, 1], [3, 4], [6, 7], [9, 10], [12, 13]],
        
          # "The man in room 2 is reading Spanish" [Spanish]
          [[4]],
        
          # "The man reading English has a room whose number is two different from the student from Reigate" [English, Reigate]
          [[1, 8], [4, 11], [7, 14], [7, 2], [10, 5], [13, 8]],
        
          # make sure A can be placed [Andy]
          [[0], [3], [6], [9], [12]],
        ]
        
        # the labels for each piece
        labels = [
          ['Bill', 'French', 'Dunstable'],
          ['Colchester'],
          ['Dave', 'Brighton'],
          ['German', 'Gloucester'],
          ['Chris'],
          ['Ed', 'Italian'],
          ['Spanish'],
          ['English', 'Reigate'],
          ['Andy'],
        ]
        
        # find exact covers
        for ss in exact_cover(pieces):
          # output solution
          g = ['???'] * 15
          for (ks, vs) in zip(ss, labels):
            g = update(g, ks, vs)
          for (i, x) in enumerate(chunk(g, 3), start=1):
            printf("{i}: {x}", x=join(x, sep=", "))
          printf()
        

        Like

    • Frits's avatar

      Frits 11:30 am on 30 September 2021 Permalink | Reply

      A lot slower.

        
      from enigma import matrix
      from itertools import product
      
      # assign room numbers 1-5 to each of the groups:
      #
      #   Andy(A) Bill(B) Chris(C) Dave(D) Ed(E)
      #   English(F) French(G) German(H) Italian(I) Spanish(J)
      #   Brighton(K) Colchester(L) Dunstable(M) Gloucester(N) Reigate(P)
      
      eqs = [
        # [     NAMES     ]   [    STUDY      ]   [    TOWNS      ]  
        # A   B   C   D   E   F   G   H   I   J   K   L   M   N   P
        ( 0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -1,  0,  0 ),  # B = M
        ( 0,  1,  0,  0,  0,  0, -1,  0,  0,  0,  0,  0,  0,  0,  0 ),  # B = G
        ( 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0 ),  # L = 5
        ( 0,  0,  0,  1,  0,  0,  0,  0,  0,  0, -1,  0,  0,  0,  0 ),  # D = K
        ( 0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,  0,  0, -1,  0 ),  # N + 1 = H
        ( 0,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 ),  # C = 3
        ( 0,  0,  0,  0,  1,  0,  0,  0, -1,  0,  0,  0,  0,  0,  0 ),  # E = I
        ( 0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,  0,  0 ),  # J = 2
        ( 1,  1,  1,  1,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 ),  # A + B + C + D + E = 15
        ( 0,  0,  0,  0,  0,  1,  1,  1,  1,  1,  0,  0,  0,  0,  0 ),  # F + G + H + I + J = 15
        ( 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,  1 ),  # K + L + M + N + P = 15
        ( 1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 ),  # A = ?
        ( 0,  0,  0,  0,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0 ),  # F = ?
        ( 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0 ),  # N = ?
        ( 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1 ),  # P = ?
       ]
      
      towns = ["Brighton", "Colchester", "Dunstable", "Gloucester", "Reigate"]
      langs = ["English", "French", "German", "Italian", "Spanish"]
      
      for (A, F, N, P) in product(range(1, 6), repeat = 4):
        # "The man reading English has a room whose number is two different
        # from the student from Reigate"
        if abs(F - P) != 2 or N == P: continue
      
        # solve equations
        s = matrix.linear(eqs, [0, 0, 5, 0, 1, 3, 0, 2, 15, 15, 15, A, F, N, P])
        
        # check that all numbers lie between 1 and 5
        if sorted(s[:5]) != [1, 2, 3, 4, 5]: continue
        if sorted(s[5:10]) != [1, 2, 3, 4, 5]: continue 
        if sorted(s[10:]) != [1, 2, 3, 4, 5]: continue
        
        lang = langs[s[5:10].index(A)]
        town = towns[s[10:].index(A)]
        print(f"Andy's home town is {town} and studies {lang}")
      

      Like

      • Frits's avatar

        Frits 12:13 pm on 30 September 2021 Permalink | Reply

        @Jim,

        Is there a more efficient way to enforce that 5 variables have distinct values 1, 2, 3, 4 and 5 (using matrix.linear)?

        Like

        • Jim Randell's avatar

          Jim Randell 1:20 pm on 30 September 2021 Permalink | Reply

          @Frits: The [[ matrix.linear() ]] solver finds solutions for a system of linear equations over a field (in this case the rationals), so if you want to further restrict the values of the solutions you have to check them after they are returned.

          In this case though there aren’t very many possible sets of permissible values, so we can just consider them directly:

          from enigma import (subsets, diff, singleton, printf)
          
          # start with towns (D, 5, B, H - 1, P)
          for (D, B, H1, P) in subsets((1, 2, 3, 4), size=4, select="P"):
            H = H1 + 1
            if H > 5: continue
          
            # then subjects (F = P +/- 2, B, H = H1 - 1, E, 2)
            ss = diff([1, 3, 4, 5], [B, H])
            if len(ss) != 2: continue
            for (F, E) in subsets(ss, size=2, select="P"):
              if abs(F - P) != 2: continue
          
              # calculate A
              A = singleton(diff([1, 2, 4, 5], [B, D, E]))
              if A is not None:
                printf("names = ({A}, {B}, 3, {D}, {E}); subjs = ({F}, {B}, {H}, {E}, 2); towns = ({D}, 5, {B}, {H1}, {P})")
          

          Like

  • Unknown's avatar

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

    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:20 am on 15 December 2020 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 1967: Domino effect 

    From The Sunday Times, 28th May 2000 [link]

    I have placed a full set of 28 dominoes on an eight-by-seven grid, with some of the dominoes horizontal and some vertical. The array is shown above with numbers from 0 to 6 replacing the spots at each end of the dominoes.

    Fill in the outlines of the dominoes.

    This puzzle is included in the book Brainteasers (2002). The puzzle text above is taken from the book.

    [teaser1967]

     
    • Jim Randell's avatar

      Jim Randell 9:20 am on 15 December 2020 Permalink | Reply

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

      Run: [ @repl.it ]

      from enigma import DominoGrid
      
      DominoGrid(8, 7, [
        1, 6, 3, 3, 5, 6, 6, 0,
        2, 6, 2, 5, 4, 4, 3, 3,
        3, 6, 6, 5, 0, 4, 1, 3,
        2, 4, 5, 0, 0, 1, 1, 4,
        4, 4, 1, 1, 0, 2, 0, 6,
        0, 4, 3, 2, 0, 2, 2, 6,
        2, 1, 5, 5, 5, 5, 1, 3,
      ]).run()
      

      Solution: The completed grid is shown below:

      Like

    • Frits's avatar

      Frits 2:00 pm on 19 December 2020 Permalink | Reply

      You have to press a key each time for finding new dominoes/stones.

      import os
      from collections import defaultdict
        
      # grid dimensions (rows, columns)
      (R, C) = (7 ,8)
       
      g = [
          1, 6, 3, 3, 5, 6, 6, 0,
          2, 6, 2, 5, 4, 4, 3, 3,
          3, 6, 6, 5, 0, 4, 1, 3,
          2, 4, 5, 0, 0, 1, 1, 4,
          4, 4, 1, 1, 0, 2, 0, 6,
          0, 4, 3, 2, 0, 2, 2, 6,
          2, 1, 5, 5, 5, 5, 1, 3,
          ]
      
      # return coordinates of index number <n> in list <g>
      coord = lambda n: (n // C, n % C)
      
      # return index number in list <g>
      gnum = lambda x, y: x * C + y
      
      # return set of stone numbers in list <li>, like {'13', '15', '12'}
      domnums = lambda li: set("".join(sorted(str(li[i][1]) + str(li[i+1][1]))) 
                           for i in range(0, len(li), 2))
      
      # build dictionary of coordinates per stone
      def collect_coords_per_stone()  : 
        # empty the dictionary
        for k in coords_per_stone: coords_per_stone[k] = []
        
        for (i, d) in enumerate(g):
          if d < 0: continue           # already placed?
          # (x, y) are the coordinates in the 2 dimensional matrix
          (x, y) = divmod(i, C) 
          js = list()
          # horizontally
          if y < C - 1 and not(g[i + 1] < 0): js.append(i + 1)
          # vertically
          if x < R - 1 and not(g[i + C] < 0): js.append(i + C)
          
          # try possible placements
          for j in js:
            stone = tuple(sorted((g[i], g[j])))
            if ((coord(i), coord(j))) in ex: continue
           
            if not stone in done:
              coords_per_stone[stone] += [[coord(i), coord(j)]]
           
      # coordinate <mand> has to use stone {mandstone> so remove other possibilities
      def remove_others(mand, mandstone, reason):  
        global stop
        mandval = g[gnum(mand[0], mand[1])]
        stones = stones_per_coord[mand]
        for i in range(0, len(stones), 2):
          otherval = stones[i+1][1] if stones[i][0] == mand else stones[i][1]
          stone = sorted([mandval, otherval])
          # stone at pos <mand> unequal to <mandstone> ?
          if stone != mandstone:
            otherco = stones[i+1][0] if stones[i][0] == mand else stones[i][0]
            tup = tuple(sorted([mand, otherco]))
            if tup in ex: continue
            print(f"stone at {yellow}{str(tup)[1:-1]}{endc} "
                  f"cannot be {stone} {reason}")
            ex[tup] = stone
            stop = 0
      
      # check for unique stones and for a coordinate occurring in all entries 
      def analyze_coords_per_stone():
        global step, stop
        for k, vs in sorted(coords_per_stone.items()): 
          k_stone = tuple(sorted(k))
          if len(vs) == 1:
            pair = vs[0]
            x = gnum(pair[0][0], pair[0][1])
            y = gnum(pair[1][0], pair[1][1])
            if list(k_stone) in done: continue
      
            print(f"----  place stone {green}{k_stone}{endc} at coordinates "
                  f"{yellow}{pair[0]}, {pair[1]}{endc} (stone occurs only once)")
            done.append(k_stone)
            g[x] = g[y] = step
            step -= 1
            stop = 0
          elif len(vs) != 0:
            # look for a coordinate occurring in all entries
            common = [x for x in vs[0] if all(x in y for y in vs[1:])]
            if len(common) != 1: continue
            reason = " (coordinate " + yellow + str(common)[1:-1] + \
                     endc + " has to use stone " + str(k) + ")"
            # so remove <common> from other combinations
            remove_others(common[0], sorted(k), reason)
      
      # build dictionary of stones per coordinate
      def collect_stones_per_coord():
        stones = defaultdict(list)
        # collect stones_per_coord per cell
        for (i, d) in enumerate(g):
          if d < 0: continue      # already placed?
          # (x, y) are the coordinates in the 2 dimensional matrix
          (x, y) = divmod(i, C) 
          
          js = list()
          # horizontally
          if y < C - 1 and not(g[i + 1] < 0): js.append(i + 1)
          if y > 0     and not(g[i - 1] < 0): js.append(i - 1)
          # vertically
          if x < R - 1 and not(g[i + C] < 0): js.append(i + C)
          if x > 0     and not(g[i - C] < 0): js.append(i - C)
          # try possible placements
          for j in js:
            t = [[coord(i), g[i]], [coord(j), g[j]]]
            t2 = tuple(sorted((coord(i), coord(j))))
            if t2 not in ex:
              stones[(x, y)] += t
              
        return stones
        
      # check if only one stone is possible for a coordinate  
      def one_stone_left():
        global step, stop
        for k, vs in sorted(stones_per_coord.items()): 
          if len(vs) != 2: continue  
      
          pair = vs
          x = gnum(pair[0][0][0], pair[0][0][1])
          y = gnum(pair[1][0][0], pair[1][0][1])
          if g[x] < 0 or g[y] < 0: return
          
          stone = sorted([g[x], g[y]])  
          if stone in done: continue
      
          print(f"----  place stone {green}{tuple(stone)}{endc} at coordinates "
                f"{yellow}{str(sorted((pair[0][0], pair[1][0])))[1:-1]}{endc}, "
                f"(only one possible stone left)")
          done.append(stone)
          g[x] = g[y] = step
          step -= 1
          stop = 0
      
      
      # if n fields only allow for n stones we have a group
      def look_for_groups():
        global stop
        for n in range(2, 5):
          for group in findGroups(n, n, list(stones_per_coord.items())):
            # skip for adjacent fields 
            a1 = any(x[0] == y[0] and abs(x[1] - y[1]) == 1 
                     for x in group[1] for y in group[1])
            if a1: continue
            a2 = any(x[1] == y[1] and abs(x[0] - y[0]) == 1 
                     for x in group[1] for y in group[1])
            if a2: continue
      
            for d in group[0]:
              # get stone number
              tup = tuple(int(x) for x in d)
              for c in coords_per_stone[tup]:
                # skip coordinates in our group 
                if len(set(c) & set(group[1])) > 0: continue
                
                tup2 = tuple(c)
                if g[gnum(tup2[0][0], tup2[0][1])] < 0: continue
                if g[gnum(tup2[1][0], tup2[1][1])] < 0: continue
                if tup2 in ex: continue
                print(f"stone at {yellow}{str(tup2)[1:-1]}{endc} cannot be "
                      f"{list(tup)}, group ({', '.join(group[0])}) "
                      f"exists at coordinates {yellow}{str(group[1])[1:-1]}{endc}")
                ex[tup2] = list(tup)
                stop = 0
          if stop == 0: return    # skip the bigger groups
      
      # pick <k> elements from list <li> so that all picked elements use <n> values
      def findGroups(n, k, li, s=set(), f=[]):
        if k == 0:
          yield (list(s), f)
        else:
          for i in range(len(li)):
            # get set of stone numbers
            vals = domnums(li[i][1])
            if len(s | vals) <= n:
              yield from findGroups(n, k - 1, li[i+1:], s | vals, f + [li[i][0]])  
      
      def print_matrix(mat, orig):
        # https://en.wikipedia.org/wiki/Box-drawing_character
        global g_save
        nw = '\u250c'    #   N
        ne = '\u2510'    # W   E
        ho = '\u2500'    #   S
        ve = '\u2502' 
        sw = '\u2514' 
        se = '\u2518'
        
        numlist = "".join(["    " + str(i) for i in range(C)]) + "   "
        print("\n\x1b[6;30;42m" + numlist + "\x1b[0m")
        for i in range(R):
          top = ""
          txt = ""
          bot = ""
          prt = 0
          for j in range(C):
            v = mat[i*C + j]
            # original value
            ov = orig[i*C + j] if orig[i*C + j] > -1 else " "
            
            cb = green if v != g_save[i*C + j] else ""
            ce = endc if v != g_save[i*C + j] else ""
            
            o = orientation(mat, i*C + j, v)
            
            if o == 'N': 
              top += nw+ho+ho+ho+ne
              txt += ve+cb+ " " + str(ov) + " " + ce+ve
              bot += ve+ "   " + ve
            if o == 'S': 
              top += ve+ "   " + ve
              txt += ve+cb+ " " + str(ov) + " " + ce+ve
              bot += sw+ho+ho+ho+se  
            if o == 'W':   # already handle East as well
              top += nw+ho+ho+ho+ho+ho+ho+ho+ho+ne
              txt += ve+cb+ " " + str(ov) + "    " + str(orig[i*C + j + 1]) + " " + ce+ve
              bot += sw+ho+ho+ho+ho+ho+ho+ho+ho+se
            if o == '':  
              top += "     "
              txt += "  " + str(ov) + "  " 
              bot += "     "
          print("\x1b[6;30;42m \x1b[0m " + top + "\x1b[6;30;42m \x1b[0m")  
          print("\x1b[6;30;42m" + str(i) + "\x1b[0m " + txt + "\x1b[6;30;42m" + str(i) + "\x1b[0m")
          print("\x1b[6;30;42m \x1b[0m " + bot + "\x1b[6;30;42m \x1b[0m")  
        print("\x1b[6;30;42m" + numlist + "\x1b[0m")  
          
        g_save = list(g)
          
      def orientation(mat, n, v):
        if v > -1: return ""
        
        # (x, y) are the coordinates in the 2 dimensional matrix
        (x, y) = divmod(n, C) 
        # horizontally
        if y < C - 1 and mat[n + 1] == v: return "W"
        if y > 0     and mat[n - 1] == v: return "E"
        # vertically
        if x < R - 1 and mat[n + C] == v: return "N"
        if x > 0     and mat[n - C] == v: return "S"
        
        return ""
                     
       
      coords_per_stone = defaultdict(list)
      
      stop = 0
      step = -1
      green = "\033[92m"    
      yellow = "\033[93m"   
      endc = "\033[0m"    # end color
       
       
      done = []
      ex = defaultdict(list)
      
      os.system('cls||clear')
      
      g_orig = list(g)
      g_save = list(g)
      
      for loop in range(222):
        stop = 1
        prevstep = step
        
        stones_per_coord = collect_stones_per_coord()
        
        one_stone_left()
        
        if step == prevstep:
          collect_coords_per_stone()    
          analyze_coords_per_stone()
      
        if step < prevstep:
          print_matrix(g, g_orig) 
          if all(y < 0 for y in g):
            exit(0)
          letter = input('Press any key: ')
          os.system('cls||clear')
          print()
          continue
      
        # collect again as some possible placements may have been ruled out
        stones_per_coord = collect_stones_per_coord()
        look_for_groups()
      
        if stop: break
      
      
      print("----------------------- NOT SOLVED -----------------------------")
      
      print_matrix(g, g_orig) 
      

      Like

  • Unknown's avatar

    Jim Randell 4:21 pm on 15 October 2020 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2758: Square dance 

    From The Sunday Times, 2nd August 2015 [link]

    Dancers numbered from 1 to 9 were about to perform a square dance: five were dressed in red and the rest in blue. They stood in a 3-by-3 array with all three dancers in the first row wearing red and all three in another row wearing blue. Their numbers formed a magic square (i.e. the sum of the three numbers in any row, column or diagonal was the same). One of the dancers in red looked around and noted that the sum of the numbers of the four other dancers in red was the same as the sum of the numbers of the four dancers in blue.

    One of the dancers in blue was number 8, what were the numbers of the other three dancers in blue?

    [teaser2758]

     
    • Jim Randell's avatar

      Jim Randell 4:22 pm on 15 October 2020 Permalink | Reply

      The Magic Square uses the numbers 1 – 9 so we know that the magic sum is 15, and the central square is 5. (See: Enigma 1680, Enigma 1080).

      We can then choose two numbers (other than 5 and 8) for the reds on the top row, and that lets us work out all the remaining squares.

      This Python program runs in 46ms.

      Run: [ @replit ]

      # consider the square:
      #
      #   a b c
      #   d e f
      #   g h i
      #
      # where each latter stands for a number from 1 to 9
      #
      # the magic sum is: s = (1 + 2 + ... + 9) / 3 = 15
      #
      # and e = s / 3 = 5
      
      from enigma import (irange, div, subsets, printf)
      
      # the numbers
      numbers = set(irange(1, 9))
      
      # total sum, magic sum, centre square
      t = sum(numbers)
      s = div(t, 3)
      e = div(s, 3)
      
      # generate magic squares
      # 8 is a blue, so can't be in the first row
      for (a, b) in subsets(numbers.difference((e, 8)), size=2, select="P"):
        c = s - (a + b)
        if c in {8, a, b, e}: continue
      
        # fill out the rest of the square
        i = s - (a + e)
        f = s - (c + i)
        d = s - (e + f)
        g = s - (a + d)
        h = s - (g + i)
      
        # check it uses the numbers 1 - 9
        if numbers.difference({a, b, c, d, e, f, g, h, i}): continue
      
        # choose the row with mixed colours
        for row in ((d, e, f), (g, h, i)):
          # and choose two from that row to be red
          for red in subsets(row, size=2):
            if 8 in red: continue
            # so the reds are...
            reds = set((a, b, c) + red)
            # and if the observant red dancer is x...
            # we need 2(sum(reds) - x) = (t - x)
            x = 2 * sum(reds) - t
            if x not in reds: continue
            # so the blues are...
            blues = numbers.difference(reds)
            printf("{a} {b} {c} / {d} {e} {f} / {g} {h} {i}, reds={reds}, x={x}, blues={blues}")
      

      Solution: The other dancers wearing blue had numbers 1, 3 and 6.

      The dancers are arranged like this:

      (or mirrored about a vertical axis).

      Dancer number 9 notices that the sum of the other red dancers (2 + 4 + 7 + 5 = 18) is the same as the sum of the blue dancers (1 + 3 + 6 + 8 = 18).

      Like

    • Frits's avatar

      Frits 12:35 pm on 16 October 2020 Permalink | Reply

      # consider the square:
      #
      #   A B C
      #   D E F
      #   G H I
      #
      # where each latter stands for a number from 1 to 9
      #
      # the magic sum is: MS = (1 + 2 + ... + 9) / 3 = 15
      #
      # and E = MS / 3 = 5
      
      from enigma import  SubstitutedExpression, irange, is_prime
      
      p = SubstitutedExpression([
          # magic square
          "A+B+C == MS",
          "D+E+F == MS",
          "G+H+I == MS",
          "A+D+G == MS",
          "B+E+H == MS",
          "C+F+I == MS",
          "A+E+I == MS",
          "C+E+G == MS",
          # for red : pick 2 out of 1st row, 2 out of middle row
          # for blue: pick 1 out of middle row and 3 out of 3rd row 
          "a*A + b*B + c*C + (1-d)*D + (1-e)*E + (1-f)*F == G+H+I + d*D + e*E + f*F",
          # pick 1 out of middle row
          "d + e + f == 1",
          # pick 2 out of first row
          "a + b + c == 2",
       
          ],
          verbose=0,
          symbols="ABCDEFGHIMSabcdef",
          d2i=dict([(0, "ABCDEFGHI")]  + [(8, "ABCabcdef")] + [(k, "abcdef") for k in {2,3,4,5,6,7,9}]),
          answer="A,B,C,D,E,F,G,H,I, a, b, c, d, e, f, d*D + e*E + f*F  ",
          distinct=("ABCDEFGHI"),
          digits=range(0, 10)
      )
      
      # Print answers
      cnt = 0
      for (_, ans) in p.solve():
         sol = list(ans[6:9]) + [ans[15]]
         sol.remove(8) 
         print(f"Answer = {sol}\n")
         print(f"{ans[:3]} a,b,c {ans[9:12]}\n{ans[3:6]} d,e,f {ans[12:15]}\n{ans[6:9]}\n")
         
         
      # Answer = [6, 1, 3]
      # 
      # (2, 9, 4) a,b,c (1, 0, 1)
      # (7, 5, 3) d,e,f (0, 0, 1)
      # (6, 1, 8)
      # 
      # Answer = [1, 6, 3]
      # 
      # (4, 9, 2) a,b,c (1, 0, 1)
      # (3, 5, 7) d,e,f (1, 0, 0)
      # (8, 1, 6)
      

      Like

      • Jim Randell's avatar

        Jim Randell 11:58 am on 17 October 2020 Permalink | Reply

        @Frits: How do we know the mixed row is the second row and not the bottom row?

        Like

    • Frits's avatar

      Frits 2:57 pm on 17 October 2020 Permalink | Reply

      You are right, mixed row can be 2nd or 3rd row.

      # consider the square:
      #
      #   A B C 
      #   D E F    magic sum A + B + C
      #   G H I
      #
      # where each latter stands for a number from 1 to 9
      #
      # a, ..., i are bits
      
      from enigma import  SubstitutedExpression, irange, is_prime
      
      p = SubstitutedExpression([
          # magic square with magic sum A + B + C
          "D + E + F == A + B + C",
          "G + H + I == A + B + C",
          "A + D + G == A + B + C",
          "B + E + H == A + B + C",
          "C + F + I == A + B + C",
          "A + E + I == A + B + C",
          "C + E + G == A + B + C",
         
          # pick 4 out of 2nd row or 3rd row, including a whole row
          "d + e + f + g + h + i == 4",
          "d + e + f == 3 or g + h + i == 3",
          # pick 2 out of first row for 
          "a + b + c == 2",
          
          # for blue: pick 4 out of 2nd row and 3rd row, including a whole row 
          # for red : pick 2 out of 1st row, 2 out of 2nd row or 3rd row
          # (which is same as:  pick 2 out of 1st row plus
          #  2 times magic sum - the 4 items picked for blue)
          "(a+2)*A + (b+2)*B + (c+2)*C == 2 * (d*D + e*E + f*F + g*G + h*H + i*I)",
          
          # One of the dancers in blue was number 8
          "sum([d*D == 8, e*E == 8, f*F == 8, g*G == 8, h*H == 8, i*I == 8]) == 1",
          ],
          verbose=0,
          symbols="ABCDEFGHIMSabcdefghi",
          d2i=dict([(0, "ABCDEFGHI")]  + 
                   [(8, "ABCabcdefghi")] + 
                   [(k, "abcdefghi") for k in {2,3,4,5,6,7,9}]),
          answer="A, B, C, D, E, F, G, H, I, a, b, c, d, e, f, g, h, i, \
                  d*D + e*E + f*F + g*G + h*H + i*I",
          distinct=("ABCDEFGHI"),
          reorder=0
      )
      
      # Print answers
      for (_, ans) in p.solve():
        blue = [y[0] for (x,y) in enumerate(zip(ans[3:9], ans[12:18])) 
                if y[1] == 1 and y[0] != 8]
        print(f"Answer = {blue}\n")
        print(f"{ans[:3]} a,b,c: {ans[9:12]}\n{ans[3:6]} d,e,f: {ans[12:15]}\n"
              f"{ans[6:9]} g,h,i: {ans[15:18]}\n")
         
         
      # Answer = [3, 1, 6]
      #
      # (4, 9, 2) a,b,c: (1, 0, 1)
      # (3, 5, 7) d,e,f: (1, 0, 0)
      # (8, 1, 6) g,h,i: (1, 1, 1)
      #
      # Answer = [3, 6, 1]
      #
      # (2, 9, 4) a,b,c: (1, 0, 1)
      # (7, 5, 3) d,e,f: (0, 0, 1)
      # (6, 1, 8) g,h,i: (1, 1, 1)
      

      Like

      • Frits's avatar

        Frits 3:29 pm on 17 October 2020 Permalink | Reply

        @Jim,

        Instead of the sum() equation I could have used any() where it not for the fact that “any” contains an “a” which is a variable. Is there an easy way to use normal Python functions without having to worry about lowercase variables?

        It would be nice if the SubstitutedExpression solver could ignore consecutive lowercase characters as variables.

        Like

  • Unknown's avatar

    Jim Randell 8:42 am on 16 June 2020 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2662: Number please 

    From The Sunday Times, 29th September 2013 [link] [link]

    I finally got through to the operator of a large company and asked to be connected to an appropriate department. He tried eight different extension numbers before then finding the correct one. The eight numbers were 1933, 2829, 3133, 4630, 5089, 5705, 6358 and 6542. Curiously, each of these wrong numbers did have at least one correct digit of the correct extension in the correct position!

    What was the correct extension number?

    [teaser2662]

     
    • Jim Randell's avatar

      Jim Randell 8:42 am on 16 June 2020 Permalink | Reply

      Here is a recursive program that starts with unassigned digits in all positions, and then at each step looks for a number that doesn’t already match and uses it to fill out one of the unassigned digits. It runs in 53ms.

      Run: [ @repl.it ]

      from enigma import (update, join, printf)
      
      # find sequences that match ns in at least one position
      # ds - candidate digits ('?' for unassigned)
      # ns - remaining numbers to check
      def solve(ds, ns):
        # are we done?
        if not ns:
          yield join(ds)
        else:
          # consider the digits of the next number
          ms = list(zip(ds, ns[0]))
          # if any digits already match move on to the next one
          if any(a == b for (a, b) in ms):
            yield from solve(ds, ns[1:])
          else:
            # set one of the unassigned digits
            for (i, (a, b)) in enumerate(ms):
              if a == '?':
                yield from solve(update(ds, [(i, b)]), ns[1:])
      
      # numbers provided
      numbers = "1933 2829 3133 4630 5089 5705 6358 6542".split()
      
      # solve the puzzle, starting with 4 ?'s
      for n in solve("????", numbers):
        printf("number = {n}")
      

      Solution: The correct extension number is 6739.


      A brute force search of all possible extension numbers is shorter, and only slightly slower:

      from enigma import (subsets, join, printf)
      
      # the numbers tried (each has at least one correct digit in the correct position)
      numbers = "1933 2829 3133 4630 5089 5705 6358 6542".split()
      
      # consider digits for the actual number
      for ds in subsets("0123456789", size=4, select="M"):
        if all(any(a == b for (a, b) in zip(ds, n)) for n in numbers):
          printf("number = {n}", n=join(ds))
      

      Like

    • GeoffR's avatar

      GeoffR 10:29 am on 6 October 2020 Permalink | Reply

      
      % A Solution in MiniZinc 
      include "globals.mzn";
      
      var 0..9: A; var 0..9: B; var 0..9: C; var 0..9: D;
      
      constraint all_different ([A,B,C,D]);
      
      % Each of these wrong numbers did have at least one correct
      % digit of the correct extension in the correct position
      
      % Extn Try 1 - 1933 
      constraint sum ([A==1,B==9,C==3,D==3]) > 0;
      
      % Extn Try 2 - 2829 
      constraint sum ([A==2,B==8,C==2,D==9]) > 0;
      
      % Extn Try 3 - 3133 
      constraint sum ([A==3,B==1,C==3,D==3]) > 0;
      
      % Extn Try 4 - 4630 
      constraint sum ([A==4,B==6,C==3,D==0]) > 0;
      
      % Extn Try 5 - 5089 
      constraint sum ([A==5,B==0,C==8,D==9]) > 0;
      
      % Extn Try 6 - 5705 
      constraint sum ([A==5,B==7,C==0,D==5]) > 0;
      
      % Extn Try 7 - 6358  
      constraint sum ([A==6,B==3,C==5,D==8]) > 0;
      
      % Extn Try 8 - 6542 
      constraint sum ([A==6,B==5,C==4,D==2]) > 0;
      
      solve satisfy;
      
      output ["The correct extension number is " ++
      show(A),show(B),show(C),show(D) ];
      
      % The correct extension number is 6739
      % % time elapsed: 0.03 s
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:55 am on 17 December 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2887: A convivial dinner party 

    From The Sunday Times, 21st January 2018 [link] [link]

    Three married couples, the Blacks, the Grays and the Pinks, are having dinner together seated around a circular table. The three men had provided one course each towards the dinner. Each lady was sitting between two men, neither being her husband. The first names are Henry, Robin, Tom, Jenny, Monica and Naomi. Robin and Mr Pink go to the same gym as the wife of the provider of the starter and Mrs Gray. The main course provider, an only child, has Monica on his right. The starter provider doesn’t have a sister. The dessert came from someone sitting closer to Naomi than he is to Mrs Black. Henry is brother in law to the starter provider and his only sister is on his left. Tom’s wife made the coffee, while the other two ladies washed up.

    What were the full names of all the ladies?

    [teaser2887]

     
    • Jim Randell's avatar

      Jim Randell 9:56 am on 17 December 2019 Permalink | Reply

      In this Python program I use a circular list, so I don’t have to worry about going off the end of the list when indexing. It examines the possibilities in 77ms.

      Run: [ @replit ]

      from itertools import (permutations, product)
      from enigma import (join, printf)
      
      # implement a circular list
      class CircularList(list):
        def __getitem__(self, i):
          return list.__getitem__(self, i % len(self))
      
      # distance between positions <a> and <b>
      _distance = CircularList([0, 1, 2, 3, 2, 1])
      
      def distance(a, b): return _distance[abs(a - b)]
      
      # left of position i is i + 1, right of position i is i - 1
      
      # seat Mr B at position 0 (and Mrs B at position 3)
      
      # choose surnames for positions 2, 4
      for (s2, s4) in permutations('GP'):
        # the full list of surnames (each pair sit opposite each other)
        surnames = CircularList([ 'B', s4, s2 ] * 2)
      
        # assign forenames (unlike surnames these are unique)
        for ((f0, f2, f4), (f1, f3, f5)) in product(permutations('HRT'), permutations('JMN')):
          # the full list of forenames
          forenames = CircularList([ f0, f1, f2, f3, f4, f5 ])
      
          # assign the positions of the providers of the starter, main, dessert
          for (S, M, D) in permutations((0, 2, 4)):
      
            # 1. "R and Mr P go to the same gym as the wife of the provider of the starter and Mrs G"
            # we assume the people mentioned separately are different,
            # so: R is not Mr P
            if not (surnames[forenames.index('R')] != 'P'): continue
            # and: Mr G did not provide the starter
            if not (surnames[S] != 'G'): continue
      
            # 2. "The main course provider ... has M on his right"
            # so: whoever is left of M provided the main course
            if not (distance(forenames.index('M') + 1, M) == 0): continue
      
            # 4. "The dessert came from someone sitting closer to N than he is to Mrs B"
            if not (distance(D, forenames.index('N')) < distance(D, 3)): continue
      
            # 2. "The main course provider... is an only child"
            # 3. "The starter provider doesn't have a sister"
            # 5. "H is brother in law to the starter provider and his only sister is on his left."
            # so, H is not the starter provider
            if not (forenames[S] != 'H'): continue
            # also H cannot be the main course provider (as H has a
            # sister, and the main course provider is an only child), so it
            # follows that H must provide the dessert
            if not (forenames[M] != 'H'): continue
            # either: H's wife is the sister of the starter provider (contradicted by statement 3)
            # or: H's sister is married to the starter provider (which must be the case)
            # and this sister is sitting on his left
            if not (surnames[forenames.index('H') + 1] == surnames[S]): continue
      
            # output solution
            printf("({names}) S={S} M={M} D={D}", names=join((a + b for (a, b) in zip(forenames, surnames)), sep=" "))
      

      Solution: The ladies names are Monica Pink, Jenny Black, Naomi Grey.

      The seating plan is:

      Henry Black; Monica Pink; Robin Grey;
      Jenny Black; Tom Pink; Naomi Grey

      The starter was provided by Tom Pink.

      The main course was provided by Robin Grey.

      The dessert was provided by Henry Black.

      Like

  • Unknown's avatar

    Jim Randell 9:04 pm on 6 December 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2985: What’s my (land) line? 

    From The Sunday Times, 8th December 2019 [link] [link]

    My telephone has the usual keypad:

    My [11-digit] telephone number starts with 01 and ends in 0. All digits from 2 to 9 are used exactly once in between, and each pair of adjacent digits in the phone number appear in a different row and column of the keypad array.

    The 4th and 5th digits are consecutive as are the 9th and 10th and the 8th digit is higher than the 9th.

    What is my number?

    [teaser2985]

     
    • Jim Randell's avatar

      Jim Randell 9:25 pm on 6 December 2019 Permalink | Reply

      Suppose the phone number is: 01-ABCDEFGH-0.

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

      The following run file executes in 146ms.

      Run: [ @repl.it ]

      #! python -m enigma -rr
      
      SubstitutedExpression
      
      --digits="2-9"
      
      # 4th and 5th digits are consecutive numbers
      "abs(B - C) = 1"
      
      # as are 9th and 10th digits
      "abs(G - H) = 1"
      
      # 8th digit is greater than the 9th
      "F > G"
      
      # assign row/col values to the individual digits
      --code="row = [ 4, 1, 1, 1, 2, 2, 2, 3, 3, 3 ]"
      --code="col = [ 2, 1, 2, 3, 1, 2, 3, 1, 2, 3 ]"
      --code="check = lambda s: all(row[x] != row[y] and col[x] != col[y] for (x, y) in tuples(s, 2))"
      # adjacent digits appear in different rows/columns
      "check([1, A, B, C, D, E, F, G, H, 0])"
      
      # solution
      --code="number = lambda x: sprintf('01{x:08d}0')"
      --answer="number(ABCDEFGH)"
      

      Solution: The phone number is 01867295340.

      Like

    • Frits's avatar

      Frits 12:46 pm on 24 September 2020 Permalink | Reply

       
      # make sure loop variable value is not equal to previous ones
      def domain(v):
        # find already used loop values  ...
        vals = set()
        # ... by accessing previously set loop variable names
        for s in lvd[v]:
          vals.add(globals()[s])
        
        return [x for x in range(2,10) if x not in vals]
        
      def check(s):
        # row coord: (1 + (i - 1) // 3)
        # col coord: (1 + (i - 1) % 3)
        
        # Check adjacent fields
        for (x, y) in list(zip(s, s[1:])):
          # check if on same row 
          if (x - 1) // 3 == (y - 1) // 3:
            return False
          # check if on same col 
          if (x - y) % 3 == 0:
            return False  
        return True      
      
      # set up dictionary of for-loop variables
      lv = ['A', 'B', 'C', 'G', 'H', 'F', 'D', 'E']
      lvd = {v: lv[:i] for i, v in enumerate(lv)}
      
      sol = set()
      
      # Solve puzzle by testing all posssible values
      for A in {5, 6, 8, 9}:
        for B in domain('B'):
          if not check([A, B]): continue
          for C in domain('C'):
            if abs(B - C) != 1: continue
            if not check([B, C]): continue
            for G in domain('G'):
              for H in domain('H'):
                if abs(G - H) != 1: continue
                if not check([G, H]): continue
                for F in domain('F'):
                  if F < G: continue
                  if not check([F, G]): continue
                  for D in domain('D'):
                    for E in domain('E'):
                      if not check([C, D, E, F]): continue
                      sol.add((A, B, C, D, E, F, G, H))
      
      print("  ABCDEFGH ")             
      for s in sol:
        print(f"01{''.join(str(x) for x in s)}0")  
      
      # Output:
      #
      #   ABCDEFGH
      # 01867295340
      

      Like

  • Unknown's avatar

    Jim Randell 10:49 am on 23 October 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2819: An age-old problem 

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

    Five men of different ages under fifty are celebrating their joint birthday in the pub. Alan’s age is the average of their five ages. When Bob is three times Alan’s age today, the sum of Alan’s and Colin’s ages will equal the sum of their five ages today. Furthermore, Derek will then be three times his age today, and Eric will be one year more than double Bob’s age today.

    The landlord checked that the youngest of the five was allowed to buy alcohol.

    Who is the youngest, and how old is he?

    [teaser2819]

     
    • Jim Randell's avatar

      Jim Randell 10:50 am on 23 October 2019 Permalink | Reply

      The conditions in the puzzle text give us 4 equations in 5 variables:

      4A − B − C − D − E = 0
      6A − 3B − D − E = 0
      3A − B − 2D = 0
      3A − 3B + E = 1

      If we set a value for one of the ages, then the other 4 ages are determined.

      We can then look for a set of integer ages, where only the youngest member of the party is asked for proof of age.

      I supposed the barman might ask anyone who looked 21 or younger for proof of age, and this gives a unique solution.

      This Python program uses the [[ matrix.linear() ]] solver from the enigma.py library to solve the equations. It runs in 148ms.

      Run: [ @repl.it ]

      from enigma import irange, matrix, as_int, arg, printf
      
      # the constraints give the following four equations:
      #
      #   4A -  B -  C -  D - E = 0
      #   6A - 3B      -  D - E = 0
      #   3A -  B      - 2D     = 0
      #   3A - 3B           + E = 1
      
      eqs = [
        # A   B   C   D   E
        [ 4, -1, -1, -1, -1 ], # = 0
        [ 6, -3,  0, -1, -1 ], # = 0
        [ 3, -1,  0, -2,  0 ], # = 0
        [ 3, -3,  0,  0,  1 ], # = 1
        [ 1,  0,  0,  0,  0 ], # = A
      ]
      
      # age range for the men (min, max)
      m = arg( 1, 0, int)
      M = arg(49, 1, int)
      printf("[ages: min = {m}, max = {M}]")
      
      # choose a value for A
      for a in irange(m, M):
      
        # solve the equations for integers
        try:
          ss = tuple(as_int(x) for x in matrix.linear(eqs, [0, 0, 0, 1, a]))
        except ValueError:
          continue
      
        # discard any solutions with ages not in the required range
        if any(x < m or x > M for x in ss): continue
      
        # count how many are 21 or under?
        n = sum(x < 22 for x in ss)
        if n != 1: continue
      
        # output the solution
        s = min(zip(ss, "ABCDE"))
        (A, B, C, D, E) = ss
        printf("youngest={s[1]} age={s[0]} [A={A} B={B} C={C} D={D} E={E}]")
      

      Solution: Colin is the youngest. He is 20.

      There are three sets of ages (between 1 and 49) that satisfy the equations:

      A=6 B=8 C=4 D=5 E=7 (min=4)
      A=17 B=23 C=12 D=14 E=19 (min=12)
      A=28 B=38 C=20 D=23 E=31 (min=20)

      In the first two sets more than one of the “men” is below the legal age to buy alcohol (18 in the UK), so the third set is the most likely answer. The youngest man is aged 20, and so could reasonably be asked for proof of age.

      If we were to restrict the minimum age to 18 only the final set is possible. (And this remains true if we take the minimum age down as far as 13).

      Like

    • GeoffR's avatar

      GeoffR 3:04 pm on 23 October 2019 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Alan, Bob, Colin, Derek, Eric
      var 1..49: A; var 1..49: B; var 1..49: C;
      var 1..49: D; var 1..49: E; 
      
      % Jim's four equations
      constraint 4*A - B - C - D - E == 0;
      constraint 6*A - 3*B - D - E == 0;
      constraint 3*A - B - 2*D == 0;
      constraint 3*A - 3*B + E  == 1;
      
      % Minimum age to buy alcohol = 18
      constraint min([A,B,C,D,E]) > 17;
      
      solve satisfy;
      
      output ["Alan = " ++ show(A) ++ ", Bob = " ++ show(B) ++ ", Colin = " ++ show(C) ++ 
      ", Derek = " ++ show(D) ++ ", Eric = " ++ show(E)] ++ 
      ["\nYoungest of five people = " ++ show(min([A,B,C,D,E])) ++ " years"]
      
      % Alan = 28, Bob = 38, Colin = 20, Derek = 23, Eric = 31
      % Youngest of five people = 20 years
      % ----------
      % ==========
      % Finished in 151msec
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:30 am on 6 September 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2804: Spots before the eyes 

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

    At the opticians I was shown a sequence of six screens. On each there was a different number of spots ranging from 0 to 5 and I had to say how many I thought I could see. This was done with the left eye and then repeated with the right. The optician always used the same sequence and my answers were:

    (5 2 3 4 4 3)
    (4 1 4 5 2 3)

    In each case I got two correct. When I asked if this was the worst he’d seen, the optician showed me these three earlier sets of answers in which just one was correct in each:

    (2 2 1 2 1 4)
    (3 3 2 3 5 1)
    (0 4 5 1 3 2)

    What was the correct sequence?

    [teaser2804]

     
    • Jim Randell's avatar

      Jim Randell 10:33 am on 6 September 2019 Permalink | Reply

      Programatically we can consider all possible orderings of the numbers from 0 to 5, and look for a sequence that gives the appropriate number of matches in the 5 cases.

      This Python program runs in 39ms.

      Run: [ @repl.it ]

      from enigma import subsets, irange, printf
      
      # sequences, values to check
      ss = (
        ((5, 2, 3, 4, 4, 3), 2),
        ((4, 1, 4, 5, 2, 3), 2),
        ((2, 2, 1, 2, 1, 4), 1),
        ((3, 3, 2, 3, 5, 1), 1),
        ((0, 4, 5, 1, 3, 2), 1),
      )
      
      # count matches
      def check(s1, s2):
        return sum(a == b for (a, b) in zip(s1, s2))
      
      # consider possible sequences
      for s in subsets(irange(0, 5), size=len, select="P"):
        if all(check(s, t) == n for (t, n) in ss):
          printf("{s}")
      

      Solution: The correct sequence is (4 2 0 1 5 3).

      Like

    • GeoffR's avatar

      GeoffR 11:00 am on 6 October 2020 Permalink | Reply

      
      % A Solution in MiniZinc 
      include "globals.mzn";
      
      % Correct sequence is A,B,C,D,E,F
      var 0..5: A; var 0..5: B; var 0..5: C; var 0..5: D;
      var 0..5: E; var 0..5: F;
      
      constraint all_different ([A,B,C,D,E,F]);
      
      % Two sequences with two correct answers
      % Sequence 1 - 5 2 3 4 4 3
      constraint sum ([A==5,B==2,C==3,D==4,E==4,F==3]) == 2;
      
      % Sequence 2 - 4 1 4 5 2 3
      constraint sum ([A==4,B==1,C==4,D==5,E==2,F==3]) == 2;
      
      % Three sequences with one correct answers
      % Sequence 3 - 2 2 1 2 1 4)
      constraint sum ([A==2,B==2,C==1,D==2,E==1,F==4]) == 1;
      
      % Sequence 4 - (3 3 2 3 5 1)
      constraint sum ([A==3,B==3,C==2,D==3,E==5,F==1]) == 1;
      
      % Sequence 5 - (0 4 5 1 3 2)
      constraint sum ([A==0,B==4,C==5,D==1,E==3,F==2]) == 1;
      
      solve satisfy;
      
      output ["Correct sequence = " ++ show([A,B,C,D,E,F]) ];
      
      % Correct sequence = [4, 2, 0, 1, 5, 3]
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 8:08 am on 11 August 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Brainteaser 1649: Ages and ages! 

    From The Sunday Times, 17th April 1994 [link]

    Today is the birthday of both Mary and Nelly, so it is a good time to have a conundrum about their ages.

    When Mary was one third of the age she is today, Nelly was half the age that Mary was when Nelly was the age that Mary was when Nelly was the age that Mary was when Nelly was a quarter of the age she is today.

    If neither of them is yet eligible for an old-age pension, how old are they both today?

    This puzzle is included in the book Brainteasers (2002).

    [teaser1649]

     
    • Jim Randell's avatar

      Jim Randell 8:09 am on 11 August 2019 Permalink | Reply

      A lot of the puzzle text is concerned with the difference between the ages, so if we suppose N’s current age is n and M’s current age is (n + d), and then look at the expression:

      When Mary was one third of the age she is today, Nelly was half the age that Mary was when Nelly was the age that Mary was when Nelly was the age that Mary was when Nelly was a quarter of the age she is today

      We can successively refine it:

      → When Mary was (1/3)n + (1/3)d, Nelly was half the age that Mary was when Nelly was the age that Mary was when Nelly was the age that Mary was when Nelly was (1/4)n
      → When Mary was (1/3)n + (1/3)d, Nelly was half the age that Mary was when Nelly was the age that Mary was when Nelly was (1/4)n + d
      → When Mary was (1/3)n + (1/3)d, Nelly was half the age that Mary was when Nelly was (1/4)n + 2d
      → When Mary was (1/3)n + (1/3)d, Nelly was half (1/4)n + 3d
      → When Mary was (1/3)n + (1/3)d, Nelly was (1/8)n + (3/2)d
      → (1/3)n + (1/3)d = (1/8)n + (3/2)d + d
      → (5/24)n = (13/6)d
      → 5n = 52d

      So d must be a multiple of 5, and n must be a multiple of 52.

      For (n + d) < 65 we must have n = 52, d = 5.

      Solution: Nelly is 52. Mary is 57.

      Liked by 1 person

  • Unknown's avatar

    Jim Randell 5:47 pm on 2 August 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2967: Odds and evens 

    From The Sunday Times, 4th August 2019 [link] [link]

    I have done a “long multiplication”, which is reproduced above. [If the multiplication was ABC × DE, then the third line shows the result of ABC × E and the fourth line shows the result of ABC × D]. However instead of writing the actual digits involved, I have written “O” where there is an odd digit and “E” where there is an even digit.

    What is the result of the multiplication?

    [teaser2967]

     
    • Jim Randell's avatar

      Jim Randell 5:59 pm on 2 August 2019 Permalink | Reply

      (See also: Enigma 1242, Enigma 1721, Enigma 109, Enigma 1503).

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

      The following run file executes in 140ms.

      Run: [ @repl.it ]

      #! python -m enigma -rr
      
      #     A B C
      #  *    D E
      #   -------
      #   J K L M (= ABC * E)
      #   N P Q   (= ABC * D)
      #   -------
      #   F G H I
      #   =======
      
      SubstitutedExpression
      
      --distinct=""
      --invalid="1|3|5|7|9,BCDEHIJLMNQ" # even digits
      --invalid="2|4|6|8,AFGKP" # odd digits
      --invalid="0,ADFGJKNP" # odd digits + leading even digits
      
      "ABC * DE = FGHI"
      
      "ABC * E = JKLM"
      "ABC * D = NPQ"
      

      Solution: The result of the multiplication sum is 9744.

      The full sum is:

      348 × 28 = 2784 + 696×10 = 9744

      Like

    • GeoffR's avatar

      GeoffR 8:45 am on 5 August 2019 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 0..9:A; var 0..9:B; var 1..9:C; 
      var 0..9:D; var 0..9:E; 
      var 0..9:F; var 0..9:G; var 0..9:H; var 0..9:I; 
      var 0..9:J; var 0..9:K; var 0..9:L; var 0..9:M; 
      var 0..9:N; var 0..9:P; var 0..9:Q;
      
      constraint A > 0 /\ D > 0 /\ J > 0 /\ N > 0 
      /\ K > 0/\ F > 0 /\ G > 0 /\ P > 0;
      
      % Allocate parity to the digits
      constraint A mod 2 == 1 /\ B mod 2 == 0 /\ C mod 2 == 0;
      constraint D mod 2 == 0 /\ E mod 2 == 0;
      constraint F mod 2 == 1 /\ G mod 2 == 1 /\ H mod 2 == 0
       /\ I mod 2 == 0;
      
      constraint J mod 2 == 0 /\ K mod 2 == 1 /\ L mod 2 == 0
       /\ M mod 2 == 0;
      constraint N mod 2 == 0 /\ P mod 2 == 1 /\ Q mod 2 == 0;
      
      % Components of multiplication sum
      var 100..999: ABC = 100*A + 10*B + C;
      var 10..99: DE = 10*D + E;
      var 1000..9999: JKLM = 1000*J + 100*K +10*L + M;
      var 1000..9999: NPQ = 1000*N + 100*P + 10*Q;
      var 1000..9999: FGHI = 1000*F + 100*G + 10*H + I;
      
      constraint ABC * DE == FGHI;
      constraint ABC * D * 10 == NPQ;
      constraint ABC * E == JKLM;
      
      solve satisfy;
      
      output ["Multiplication sum is " ++ show(ABC) ++ " * " 
      ++ show(DE) ++ " = " ++ show(JKLM) ++ " + " ++ show(NPQ)
       ++ " = " ++ show(FGHI) ];
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:16 pm on 14 June 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2960: Bags of sweets! 

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

    I recently bought a number of equally priced bags of sweets for a bargain price, spending more than 50p in total. If they had been priced at 9p less per bag, I could have had 2 bags more for the same sum of money. In addition, if each had cost 12p less than I paid, then I could also have had an exact number of bags for the same sum of money.

    How much did I spend in total on the sweets?

    [teaser2960]

     
    • Jim Randell's avatar

      Jim Randell 11:47 pm on 14 June 2019 Permalink | Reply

      If we buy n bags of sweets at x pence per bag, then the total outlay n.x can be expressed as:

      n.x = (n + 2)(x − 9)
      n.x = k(x − 12)
      n.x > 50

      (for some whole number k, where n > 1).

      From which we see:

      x = (18 + 9n) / 2

      This Python program finds the first value of n that satisfies the conditions and gives an integer value for k.

      Run: [ @repl.it ]

      from enigma import (Rational, irange, inf, div, printf)
      
      Q = Rational()
      
      # consider n the number of bags of sweets bought
      for n in irange(1, inf):
        x = Q(18 + 9 * n, 2)
        t = n * x
        if not (t > 50): continue
      
        # find the number of bags if the prices were 12p less
        k = div(t, x - 12)
        if k is None: continue
      
        printf("{t}p = {n} bags @ {x}p [= {n2} bags @ {x9}p = {k} bags @ {x12}p]", n2=n+2, x9=x-9, x12=x-12)
        break
      

      Solution: The total spend was 216p.


      With a bit more analysis we can show this is the only solution.

      We can write an expression for k as:

      k = 9n(n + 2) / (9n − 6) = n + 8/3 + 16 / (9n − 6)

      And this only gives a whole number when 16 / (9n − 6) has a fractional part of 1/3.

      This is only possible for n = 1, 2, 6.

      n = 1 gives x = 13.5p, n.x = 13.5p, which is not more than 50p (and we want n > 1 anyway).

      n = 2 gives x = 18p, n.x = 36p, which is not more than 50p.

      n = 6 gives x = 36p, n.x = 216p, so this is the solution.

      Here is a program that uses this analysis to consider all possible solutions, by looking at the divisors of 48:

      from enigma import (Rational, divisors, div, printf)
      
      Q = Rational()
      
      # consider divisors of 48
      for d in divisors(48):
        # we only want divisors of the form (3z + 1)
        if not (d % 3 == 1): continue
        # compute n
        n = div(48 // d + 6, 9)
        if n is None: continue
        # compute x and t
        x = Q(18 + 9 * n, 2)
        t = n * x
        if not (t > 50): continue
        # compute k
        k = div(9 * n * (n + 2), 9 * n - 6)
        # output solution
        printf("[d={d}] n={n} x={x} t={t} k={k}")
      

      Removing the check at line 15 will give all three solutions for the equations.

      Like

    • GeoffR's avatar

      GeoffR 6:49 pm on 16 June 2019 Permalink | Reply

      We can put Jim’s initial three equations into a MinZinc constraint for an easy solution

      % A Solution in MiniZinc
      include "globals.mzn";
       
      var 1..50: n;   % no of bags bought originally
      var 1..50: k;   % no bags bought at 12p less
      var 1..100: x;  % original cost per bag (pence)
      
      constraint n * x > 50 /\ n * x == (n + 2) * (x - 9) 
      /\ n * x = k * (x - 12);
      
      solve satisfy;
      output ["Total spent on sweets = " ++ show(n * x) ++ " pence" ];
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:46 am on 17 June 2019 Permalink | Reply

        @GeoffR: This approach works OK for cases where the price per bag is a whole number (which is the case in the actual solution).

        I didn’t assume that and found there were 3 candidate solutions that satisfied the 2 equations, one of which has the bags priced with a fractional amount. Two of the candidates are eliminated by the inequality (including the one where the bags are priced a fractional amount), leaving a single solution.

        Like

    • GeoffR's avatar

      GeoffR 3:12 pm on 17 June 2019 Permalink | Reply

      @Jim: I can see you are making a mathematical point about prices for fractional amounts, but is this applicable for this teaser ?

      We don’t have fractional pennies these days in our monetary system, so maybe we should assume that prices per bag are in whole numbers of pennies ?

      Like

      • Jim Randell's avatar

        Jim Randell 4:56 pm on 18 June 2019 Permalink | Reply

        @GeoffR: It was just a comment to try and extract a bit more interest from a relatively straightforward puzzle.

        I try not to make additional assumptions about the puzzle if I can help it. From the formula for x we see that the price per pack is a whole number of half-pennies, so it seemed reasonable to allow this. And we do get an extra potential solution if we do. Although this solution is then removed by the “more than 50p” requirement, so it doesn’t really matter if we consider it or not.

        Like

  • Unknown's avatar

    Jim Randell 9:01 am on 29 May 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2909: Pensionable service 

    From The Sunday Times, 24th June 2018 [link] [link]

    A few years ago my father was lucky enough to have been able to retire and take his pension at sixty. I explained to my young son – not yet a teenager – that this was not now usual, and that neither I nor he would be so lucky in the future.

    When I am as old as my father is now, I shall be six times my son’s present age. By then, my son will be nine years older than I am now. (These statements all refer to whole years and not fractions of years).

    How old am I now?

    [teaser2909]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 29 May 2019 Permalink | Reply

      Very straightforward.

      Run: [ @repl.it ]

      # suppose the father is currently x, the setter y and the son z
      
      from enigma import (irange, div, printf)
      
      # the son is not yet a teenager
      for z in irange(1, 12):
      
        # the father's current age is 6x the sons age
        x = 6 * z
      
        # but this is more than 60
        if not (x > 60): continue
      
        # the son will be 9 years older than the setters current age in (x - y) years
        # z + (x - y) = y + 9
        y = div(x + z - 9, 2)
        if y is None: continue
      
        printf("father = {x}, setter = {y}, son = {z}")
      

      Solution: You are 34.

      Manually (these are the same steps the program carries out):

      If the father is currently x, the setter y and the son z.

      Then, the son is not yet a teenager:

      z = 1 … 12

      The father is six times the son’s present age, x = 6z, and this is more than 60. The only two possibilities are:

      z = 11, x = 66
      z = 12, x = 72

      The son will be 9 years older than the setters current age in (x − y) years, so:

      z + (x − y) = y + 9
      y = (x + z − 9) / 2

      So we get:

      z = 11, x = 66, y = 34
      z = 12, x = 72, y = 75/2

      And only the first of these is viable.

      Liked by 1 person

    • Jim Randell's avatar

      Jim Randell 9:44 am on 30 May 2019 Permalink | Reply

      Here is a simple MiniZinc model for the puzzle:

      %#! minizinc -a
      
      var int: x; % father's age
      var int: y; % setter's age
      var int: z; % son's age
      
      % all ages are non-negative
      constraint x > y /\ y > z /\ z > 0;
      
      % father is aged over 60
      constraint x > 60;
      
      % son is not yet a teenager
      constraint z < 13;
      
      % father's age is 6x son's age
      constraint x = 6 * z;
      
      % the son will be 9 years older than the setters age in (x - y) years
      constraint z + (x - y) = y + 9;
      
      solve satisfy;
      

      Like

  • Unknown's avatar

    Jim Randell 10:47 am on 29 April 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2918: Prime multiplication 

    From The Sunday Times, 26th August 2018 [link] [link]

    Almost everyone knows that the single digit prime numbers are 2, 3, 5, and 7, the number 1 having been excluded quite a long time ago.

    Here is a multiplication involving prime digits:

    What is the answer?

    [teaser2918]

     
    • Jim Randell's avatar

      Jim Randell 10:47 am on 29 April 2019 Permalink | Reply

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

      The following run file executes in 113ms.

      Run: [ @replit ]

      #! python -m enigma -rr
      
      #        A B C
      #    *     D E
      #    ---------
      #      F G H J
      #    K L M N
      #    ---------
      #    P Q R S T
      #    =========
      
      SubstitutedExpression
      
      # use only prime digits
      --digits="2,3,5,7"
      --distinct=""
      
      "ABC * DE = PQRST"
      "ABC * E = FGHJ"
      "ABC * D = KLMN"
      
      # required answer
      --answer="PQRST"
      

      Solution: The result of the multiplication is 25575.

      If the digit 1 were allowed, there would be a further 9 solutions (including one that does not have the digit 1 in the result).

      Like

    • GeoffR's avatar

      GeoffR 1:58 pm on 29 April 2019 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: S = {2,3,5,7};    % single digit primes
      
      var S: a;  var S: b;  var S: c;  var S: d;
      var S: e;  var S: f;  var S: g;  var S: h;
      var S: i;  var S: j;  var S: k;  var S: m;
      var S: n;  var S: p;  var S: q;  var S: r;
      var S: s;  var S: t;  
      
      %      a b c
      %   x    d e
      %    -------
      %    f g h i
      %  j k m n 0
      %  ---------
      %  p q r s t
      %  ---------
      
      var 100..999: abc = 100*a + 10*b + c;
      var 10..99: de = 10*d + e;
      var 10000..99999: pqrst = 10000*p + 1000*q + 100*r + 10*s + t;
      var 1000..9999: fghi = 1000*f + 100*g + 10*h + i;
      var 10000..99999: jkmn0 = 10000*j + 1000*k + 100*m + 10*n;
      
      constraint abc * de == pqrst;
      constraint abc * d * 10 == jkmn0;
      constraint abc * e == fghi;
      
      solve satisfy;
      
      % a = 7; b = 7; c = 5; d = 3; e = 3;
      % f = 2; g = 3; h = 2; i = 5; 
      % j = 2; k = 3; m = 2; n = 5; 
      % p = 2; q = 5; r = 5; s = 7; t = 5;
      % time elapsed: 0.02 s
      %----------
      %==========
      % Sum is:
      %     775
      %   x  33
      %   ----
      %    2325
      %   23250
      %   -----
      %   25575
      %   -----
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:39 am on 24 February 2019 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2944: Happy families 

    From The Sunday Times, 24th February 2019 [link] [link]

    Teaser 2944

    Oliver lives with his parents, Mike and Nellie, at number 5. In each of numbers 1 to 4 lives a family, like his, with a mother, a father and one child. He tries listing the families in alphabetical order and produces the table above.

    However, apart from his own family, there is just one father, one mother and one child in the correct position. Neither Helen nor Beth lives at number 3 and neither Dave nor Ingrid lives at number 1. George’s house number is one less than Larry’s and Beth’s house number is one less than Carol’s. Apart from Oliver’s family, who are correctly positioned?

    [teaser2944]

     
    • Jim Randell's avatar

      Jim Randell 8:05 am on 24 February 2019 Permalink | Reply

      The wording of the puzzle is a little strange. I’m assuming the columns of the correct table are placed in order of house number (not “alphabetical” order), and the names of the fathers, mothers and children have been placed in the correct rows, just not necessarily in the correct columns. It seems like a reasonable assumption, and leads to a unique solution.

      I think I would have described the filling out of the original table like this:

      Oliver knows the names of the fathers, mothers and children, but is not sure who lives where, so he filled out each row in alphabetical order (from left to right), to give the table above. This correctly places his own family at house number 5. However …

      We can easily consider all the possibilities and eliminate those that do not satisfy the requirements.

      This Python program runs in 71ms.

      Run: [ @repl.it ]

      from itertools import permutations
      from enigma import printf
      
      # generate permutations that are only correct in 1 position
      def generate(vs, k=1):
        for s in permutations(vs):
          if sum(x == v for (x, v) in zip(s, vs)) == k:
            yield s
      
      # permute the Mums
      for mums in generate('BEHK'):
        (m1, m2, m3, m4) = mums
      
        # neither H or B live at number 3
        if m3 == 'H' or m3 == 'B': continue
      
        # permute the kids
        for kids in generate('CFIL'):
          (k1, k2, k3, k4) = kids
      
          # I does not live at number 1
          if k1 == 'I': continue
      
          # B's house number is 1 less than C's
          if not (mums.index('B') + 1 == kids.index('C')): continue
      
          # permute the dads
          for dads in generate('ADGJ'):
            (d1, d2, d3, d4) = dads
      
            # D does not live at 1
            if d1 == 'D': continue
      
            # G's house number is 1 less than L's
            if not (dads.index('G') + 1 == kids.index('L')): continue
      
            printf("1 = {d1}+{m1}+{k1}; 2 = {d2}+{m2}+{k2}; 3 = {d3}+{m3}+{k3}; 4 = {d4}+{m4}+{k4}")
      

      Solution: George, Kate and Larry were also correctly placed.

      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