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

  • Unknown's avatar

    Jim Randell 11:02 am on 6 January 2026 Permalink | Reply
    Tags: by: Angela Newing   

    Brain teaser 981: Bailing out 

    From The Sunday Times, 10th May 1981 [link]

    The pirate ship “Nancy” had one leaky hold. As long as the water was not allowed to rise above a certain level there was no danger of the ship sinking; so the practice was to allow the water to rise to a mark on the side of the hold which was just below the danger level, and then to send in a team of about 15 or 20 pirates to bale out the hold until it was empty. The water came in continuously at a constant: rate, so the process had, to be repeated regularly.

    There were two kinds of baling equipment: practical pans and capacious cans (which emptied the water at different rates), and each pirate who was assigned to baling duty picked up whichever of these was nearest to hand.

    One baling party consisting of 11 pirates with practical pans and 6 pirates with capacious cans emptied the hold in 56 minutes. When the water had risen again, the next party of 5 pirates with practical pans and 12 with capacious cans emptied it in 35 minutes. A third party of 15 with practical pans plus 4 with capacious cans took exactly an hour to do the job.

    How long would it take a party of 6 with practical pans and 10 with capacious cans?

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

    [teaser981]

     
    • Jim Randell's avatar

      Jim Randell 11:02 am on 6 January 2026 Permalink | Reply

      If we treat the volume of water at the point that baling starts as 1 unit, then we can write the following equation for a team with P pans and C cups as:

      T(P.p + C.c + f) = 1

      P.p + C.c + f = 1/T

      where:

      T is the total time taken to empty the hold (min);
      p is the rate at which a pan removes water (units/min);
      c is the rate at which a cup removes water (units/min);
      f is the rate at which the hold fills (units/min) (this will be negative as water is flowing into the hold).

      For the three teams given we get the following equations:

      11p + 6c + f = 1/56
      5p + 12c + f = 1/35
      15p + 4c + f = 1/60

      This gives us three equations in three variables, which can be solved to give the rates p, c and f.

      We can then calculate the time taken for the 4th team:

      T(6p + 10c + f) = 1

      T = 1/(6p + 10c + f)

      The following Python program runs in 74ms. (Internal runtime is 186µs).

      from enigma import (Matrix, Rational, printf)
      
      Q = Rational()
      
      # construct the equations
      eqs = [
        #  p   c   f   1/time
        ((11,  6,  1), Q(1, 56)), # team 1 (11p + 6c) takes 56 minutes
        (( 5, 12,  1), Q(1, 35)), # team 2 (5p + 12c) takes 35 minutes
        ((15,  4,  1), Q(1, 60)), # team 3 (15p + 4c) takes 60 minutes
      ]
      
      (p, c, f) = Matrix.linear(eqs, field=Q)
      printf("[p={p} c={c} f={f}]")
      
      # calculate time for team 4 (6p + 10c)
      t = Q(1, 6*p + 10*c + f)
      printf("team 4 takes {t} minutes")
      

      Solution: It would take the 4th team 42 minutes to empty the hold.

      We get:

      p = 1/840
      c = 1/336
      f = −11/840

      1/T = 6/840 + 10/336 − 11/840
      1/T = 1/42
      T = 42

      So the water is coming in at 11/840 units/min, and a pirate with a pail can empty out 1/840 units/min. So 11 pirates with pails would be able to remove the water at the rate it was coming in. A pirate with a cup can empty 1/336 units/min (so a cup can bail more than a pail), and 2.5 pirates with cups could remove water at the rate it is coming in.

      Like

  • Unknown's avatar

    Jim Randell 10:32 am on 23 September 2025 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2506: [Domestic chores] 

    From The Sunday Times, 3rd October 2010 [link] [link]

    Three brothers share a house in a university city. Each is capable of undertaking two of the four domestic chores required. For instance, only one knows how to vacuum and only one can dust. They have recruited two friends who can do three chores each, so now any chore can be done by three people. Leo does not wash up; in fact, only one person can both wash up and do the laundry. There is one job both Leo and Neil can do, while Keith and Neil can do two jobs together. John can vacuum; Mike cannot. Neil is one of the brothers.

    Who are the other two brothers, and who does the dusting?

    This puzzle was originally published with no title.

    [teaser2506]

     
    • Jim Randell's avatar

      Jim Randell 10:34 am on 23 September 2025 Permalink | Reply

      We can think of filling out a 5×5 binary matrix, indicating whether a person is one of the brothers, and which of the 4 tasks he can do.

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

      It runs in 133ms. (Internal runtime is 439µs).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #     bro  vcm  dst  wsh  lnd
      #  J:  J    A    F    Q    V
      #  K:  K    B    G    R    W
      #  L:  L    C    H    S    X
      #  M:  M    D    I    T    Y
      #  N:  N    E    P    U    Z
      
      --base=2
      --distinct=""
      
      # there are 3 brothers (and 2 friends)
      "J + K + L + M + N == 3"
      
      # each brother can do exactly 2 tasks (and each friend exactly 3 tasks) [rows]
      --code="person = lambda b, *ts: sum(ts) == (2 if b else 3)"
      "person(J, A, F, Q, V)"
      "person(K, B, G, R, W)"
      "person(L, C, H, S, X)"
      "person(M, D, I, T, Y)"
      "person(N, E, P, U, Z)"
      
      # each task can be done by exactly 3 people [cols]
      --code="task = lambda *ps: sum(ps) == 3"
      "task(A, B, C, D, E)"
      "task(F, G, H, I, P)"
      "task(Q, R, S, T, U)"
      "task(V, W, X, Y, Z)"
      
      # exactly 1 brother knows how to vacuum
      "dot([J, K, L, M, N], [A, B, C, D, E]) == 1"
      
      # exactly 1 brother knows how to dust
      "dot([J, K, L, M, N], [F, G, H, I, P]) == 1"
      
      # L does not wash-up
      "S == 0"
      
      # exactly 1 person can wash-up and do the laundry
      "[Q + V, R + W, S + X, T + Y, U + Z].count(2) == 1"
      
      # there is 1 job both L and N can do
      "[C + E, H + P, S + U, X + Z].count(2) == 1"
      
      # there are 2 jobs K and N can do
      "[B + E, G + P, R + U, W + Z].count(2) == 2"
      
      # J can vacuum
      "A == 1"
      
      # M cannot vacuum
      "D == 0"
      
      # N is one of the brothers
      "N == 1"
      
      --template="(J A F Q V) (K B G R W) (L C H S X) (M D I T Y) (N E P U Z)"
      --solution=""
      --denest=-1
      --verbose="1-W"
      

      The following program can be used to give a more friendly output to the answer:

      from enigma import (SubstitutedExpression, seq2str, printf)
      
      # load the puzzle
      p = SubstitutedExpression.from_file(["{dir}/teaser2506.run"])
      
      # names
      names = ["John", "Keith", "Leo", "Mike", "Neil"]
      tasks = ["brother", "vacuum", "dust", "wash-up", "laundry"]
      results = ["JAFQV", "KBGRW", "LCHSX", "MDITY", "NEPUZ"]
      
      # solve the puzzle and format the solutions
      for s in p.solve(verbose=0):
        # output solution
        for (n, rs) in zip(names, results):
          # collect tasks
          ts = list(t for (t, v) in zip(tasks, rs) if s[v])
          printf("{n}: {ts}", ts=seq2str(ts, sep=", ", enc=""))
        printf()
      

      Solution: The other two brothers are: John and Mike. Keith, Leo and Neil can do dusting.

      The complete solution is:

      Brothers:
      John: vacuuming, laundry
      Mike: washing-up, laundry
      Neil: dusting, washing-up

      Friends:
      Keith: vacuuming, dusting, washing-up
      Leo: vacuuming, dusting, laundry

      Like

      • Frits's avatar

        Frits 1:27 pm on 23 September 2025 Permalink | Reply

        The task function could also have been used instead of the person function.

        Like

  • Unknown's avatar

    Jim Randell 4:44 pm on 4 September 2025 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2332: Schoolwork 

    From The Sunday Times, 3rd June 2007 [link]

    Five girls were each given £4 to spend on rubbers, pencils and pens (not necessarily all three). Rubbers cost 20p each, pencils 30p, pens 40p. They each bought 12 items, spending the entire £4.

    Sue bought the same number of pencils as Tina did rubbers. Tina had as many pens as Ursula had pencils. Viola had as many rubbers as Wendy had pens. And just one girl had the same number of pens as Tina had pencils.

    How many of each item did Ursula buy?

    [teaser2332]

     
    • Jim Randell's avatar

      Jim Randell 4:45 pm on 4 September 2025 Permalink | Reply

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

      It runs in 129ms. (Internal runtime of the generated program is 1.5ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #    rb  pl  pn
      # S:  A   B   C
      # T:  D   E   F
      # U:  G   H   I
      # V:  J   K   L
      # W:  M   N   P
      #
      --distinct=""
      --base=13  # allow quantities 0-12
      
      # each girl spent 400p and bought 12 items
      --code="girl = lambda rb, pl, pn: (rb + pl + pn == 12) and (2 * rb + 3 * pl + 4 * pn == 40)"
      "girl(A, B, C)"
      "girl(D, E, F)"
      "girl(G, H, I)"
      "girl(J, K, L)"
      "girl(M, N, P)"
      
      # equivalences
      "B = D"  # S.pl = T.rb
      "F = H"  # T.pn = U.pl
      "J = P"  # V.rb = W.pn
      
      # "just one girl bought the same number of pens as T did pencils"
      "[C, F, I, L, P].count(E) == 1"
      
      --template="(A B C) (D E F) (G H I) (J K L) (M N P)"
      --solution=""
      
      --answer="(G, H, I)"
      

      Solution: Ursula bought: 1 rubber; 6 pencils; 5 pens.

      The full solution is:

      S: 3 rubbers; 2 pencils; 7 pens
      T: 2 rubbers; 4 pencils; 6 pens
      U: 1 rubber; 6 pencils; 5 pens
      V: 4 rubbers; 0 pencils; 8 pens
      W: 0 rubbers; 8 pencils; 4 pens

      Like

      • Jim Randell's avatar

        Jim Randell 11:24 am on 5 September 2025 Permalink | Reply

        Here is a solution using the [[ choose() ]] function from the enigma.py library.

        It runs in 70ms (with an internal runtime of 387µs).

        from enigma import (Record, decompose, choose, icount_exactly as count, nl, printf)
        
        # look for combinations of 12 items with a cost of 400p
        vs = list()
        for (rb, pl, pn) in decompose(12, 3, increasing=0, sep=0, min_v=0):
          if 2 * rb + 3 * pl + 4 * pn == 40:
            vs.append(Record(rb=rb, pl=pl, pn=pn))
        
        # selection functions (in order) for each girl
        fns = [
          None,  # S
          (lambda S, T: S.pl == T.rb),  # T
          (lambda S, T, U: T.pn == U.pl),  # U
          None,  # V
          (lambda S, T, U, V, W: V.rb == W.pn),  # W
        ]
        
        # find candidate solutions
        for (S, T, U, V, W) in choose(vs, fns):
          # "just one girl bought the same number of pens as T did pencils"
          if not count((S, T, U, V, W), (lambda x, t=T.pl: x.pn == t), 1): continue
          # output solution
          printf("S: {S}{s}T: {T}{s}U: {U}{s}V: {V}{s}W: {W}", s=nl)
          printf()
        

        Like

    • Frits's avatar

      Frits 6:36 pm on 4 September 2025 Permalink | Reply

      from itertools import product
      
      N, T = 12, 400
      pa, pb, pc = 20, 30, 40
      
      # each girl spent 400p and bought 12 items and
      # bought <a> rubbers, <b> pencils and <c> pens
       
      # quantity options for the girls    
      sols = [(a, b, c) for a in range(min(N, T // pa) + 1)
              for b in range(min(N - a, (T - a * pa) // pb) + 1)
              if T - pa * a - pb * b == pc * (c := N - a - b)]   
      
      # determine quantities for all girls
      for S, T, U, V, W in product(sols, repeat=5):
        # Sue bought the same number of pencils as Tina did rubbers
        if S[1] != T[0]: continue
        # Tina had as many pens as Ursula had pencils
        if T[2] != U[1]: continue
        # Viola had as many rubbers as Wendy had pens
        if V[0] != W[2]: continue
        # just one girl bought the same number of pens as T did pencils
        if [x[2] for x in (S, T, U, V, W)].count(T[1]) != 1: continue
        print("answer:", U)
      

      Like

    • Ruud's avatar

      Ruud 7:43 am on 5 September 2025 Permalink | Reply

      import types
      
      combinations = [
          types.SimpleNamespace(rubbers=rubbers, pencils=pencils, pens=pens)
          for rubbers in range(20)
          for pencils in range(14)
          for pens in range(11)
          if pens * 40 + pencils * 30 + rubbers * 20 == 400 and pens + pencils + rubbers == 12
      ]
      
      for s in combinations:
          for t in combinations:
              if s.pencils == t.rubbers:
                  for u in combinations:
                      if t.pens == u.pencils:
                          for v in combinations:
                              for w in combinations:
                                  if v.rubbers == w.pens:
                                      if sum(girl.pens == t.pencils for girl in (s, t, u, v, w)) == 1:
                                          for name in "Sue Tina Ursula Viola Wendy".split():
                                              print(f"{name:7} {locals()[name[0].lower()]}")
      

      Like

  • Unknown's avatar

    Jim Randell 8:28 am on 26 August 2025 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2476: [Parking spaces] 

    From The Sunday Times, 7th March 2010 [link]

    Ann, Beth, Carol, Dave and Eric rent flats on each of the five floors of a block. Each has a parking space in which are their five makes of car, in five colours.

    Ann does not live in the basement, whose tenant owns the MG. Ann’s car is not the silver Audi or the blue car, which belongs to the second-floor tenant. The Ford (not black) is the first-floor tenant’s. The ground-floor tenant’s car is white and Eric’s is not black. One of the girls owns the Vauxhall, and Dave (who lives one floor below Carol) owns the Toyota.

    Whose cars are red, white and blue respectively?

    This puzzle was originally published with no title.

    [teaser2476]

     
    • Jim Randell's avatar

      Jim Randell 8:29 am on 26 August 2025 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to solve the puzzle.

      We assign numeric values from 0 to 4 to the floors (this makes it easier to code the condition that D is one floor below C), and then assign each of the floor values to the remaining groups of attributes: name, colour, make.

      The program runs in 71ms. (Internal runtime is 8.4ms).

      from enigma import (SubstitutedExpression, sprintf, printf)
      
      # we assign floor values (0 .. 4 = basement .. 3rd) to each of the groups:
      #   name (A, B, C, D, E)
      #   make
      #   colour
      
      # floors (basement, ground, 1st, 2nd, 3rd)
      floor = (FB, FG, F1, F2, F3) = (0, 1, 2, 3, 4)
      
      # symbols and labels for the groups
      name = (A, B, C, D, E) = "ABCDE"
      make = (MM, MA, MF, MV, MT) = "KLMNP"  # MG, Audi, Ford, Vauxhall, Toyota
      colour = (CS, CB, CK, CW, CR) = "QRSTU"  # silver, blue, black, white, red
      
      # construct the expressions
      exprs = list(sprintf(x) for x in [
        # "A does not live in the basement"
        "{FB} != {A}",
        # "basement owns the MG"
        "{FB} == {MM}",
        # "A's car is not the silver Audi" => "the Audi is silver"
        "{MA} == {CS}", "{MA} != {A}", "{CS} != {A}",
        # "A's car is not blue"
        "{CB} != {A}",
        # "the blue car belongs to the second floor"
        "{CB} == {F2}",
        # "the Ford is not black" (!)
        "{MF} != {CK}",
        # "the Ford belongs to the first floor"
        "{MF} == {F1}",
        # "the ground floor car is white"
        "{FG} == {CW}",
        # "E's car is not black"
        "{CK} != {E}",
        # "the Vauxhall belongs to A, B, or C"
        "{MV} in {{ {A}, {B}, {C} }}",
        # "D lives one floor below C"
        "{D} + 1 == {C}",
        # "D owns the Toyota"
        "{MT} == {D}",
      ])
      
      # construct the solver
      p = SubstitutedExpression(exprs, base=5, distinct=[name, make, colour])
      
      # labels for output
      floors = ["basement", "ground floor", "1st floor", "2nd floor", "3rd floor"]
      names = ["Anne", "Beth", "Carol", "Dave", "Eric"]
      colours = ["silver", "blue", "black", "white", "red"]
      makes = ["MG", "Audi", "Ford", "Vauxhall", "Toyota"]
      output = lambda f, n, c, m: printf("{f}: {n}, {c} {m}")
      
      # solve the puzzle
      for s in p.solve(verbose=0):
        # collect attributes by floor
        ss = list([f] for f in floors)
        for (xs, ts) in [(name, names), (colour, colours), (make, makes)]:
          for (x, t) in zip(xs, ts):
            ss[s[x]].append(t)
        # output solution
        for vs in ss: output(*vs)
        printf()
      

      Solution: Eric’s car is red; Anne’s car is white; Dave’s car is blue.

      The complete description is:

      basement: Beth, black, MG
      ground floor: Anne, white, Vauxhall
      1st floor: Eric, red, Ford
      2nd floor: Dave, blue, Toyota
      3rd floor: Carol, silver, Audi

      Like

    • Frits's avatar

      Frits 7:43 pm on 27 August 2025 Permalink | Reply

      Reusing code from Tantalizer 66.

      deepcopy = lambda s: [x.copy() for x in s]
      n_elements = lambda s: sum(len(y) for x in s for y in x)
      
      # update values in positions <ps> for column <col>
      def update(ps, vals):
        col = colno[vals[0]] # column for the values in <vals>
        for i in range(5):
          if i in ps:
            lst[i][col] = vals
          else:
            # remove values from other positions
            remove(i, [x for x in lst[i][col] if x in vals]) 
      
      # remove values <vals> from lst[pos][col]      
      def remove(pos, vals):
        if not vals: return
        col = colno[vals[0]] # column for the values in <vals>
        ln = len(lst[pos][col])
        lst[pos][col] = [x for x in lst[pos][col] if x not in vals]  
        if not lst[pos][col]:
          error = True   # no value remained
        if ln > 1 and len(lst[pos][col]) == 1:
          update([pos], lst[pos][col])
      
      # propagate uniqueness of value <v>
      def unique(col, v):
        # check for uniqueness
        if len(s := [i for i in range(5) if v in lst[i][col]]) == 1:
          update(s, [v])     
      
      # values <v1> and <v2> belong together
      def linked(v1, v2):
        c1 = colno[v1] 
        c2 = colno[v2] 
        # if v1 and v2 don't both exist for a position remove all of them
        for i in range(5):
          if not(v1 in lst[i][c1] and v2 in lst[i][c2]):
            remove(i, [v1])
            remove(i, [v2])
      
        unique(c1, v1)
        unique(c2, v2)  
      
      # values <v1> and <v2> don't belong together
      def not_linked(v1, v2):
        c1 = colno[v1] 
        c2 = colno[v2] 
        # remove other value if a position has a set value for <v1> or <v2> 
        for i in range(5):
          if lst[i][c1] == [v1]:
            remove(i, [v2])
          if lst[i][c2] == [v2]:
            remove(i, [v1])
      
      # try all options for person <p> and list <s>
      def solve(k, p, s, ss=[]):
        global lst
        if (n := n_elements(lst)) == 15:
          yield lst
        elif k == 0:
          # we have to try options for another person
          p = ss[1]
          s = deepcopy(lst[p])
          yield from solve(3, p, s, ss[1:])
        else:
          if len(s[k - 1]) == 1:  # one value
            yield from solve(k - 1, p, s, ss)
          else:
            save = deepcopy(lst)
            # try all values
            for v in s[k - 1]:
              update([p], [v])
              # check/update relations until <lst> is no longer updated
              n = check()   
              if not error:
                yield from solve(k - 1, p, s, ss)
              # back track all updates
              lst = deepcopy(save)          
      
      # check/update relations until <lst> is no longer updated
      def check():
        global error
        error = False
        n, prev = 75, 76
        
        while n < prev and not error:
          linked("basement", "MG")        # "basement owns the MG"
          linked("Audi", "silver")        # "A's car is not the silver Audi"
          linked("blue", "second-floor")  # "the blue car belongs to the second floor"
          not_linked("Ford", "black")     # "the Ford is not black"
          linked("Ford", "first-floor")   # "the Ford belongs to the first floor"
          linked("ground-floor", "white") # "the ground floor car is white"
          # "D lives one floor below C"
          for fl in lst[D][0]:
            if floor[floor.index(fl) + 1] not in lst[C][0]:
              remove(D, [fl])
          for fl in lst[C][0]:
            if floor[floor.index(fl) - 1] not in lst[D][0]:
              remove(C, [fl])    
      
          # calculate number of elements in flattened list
          prev, n = n, n_elements(lst)
          if n < 15:
            error = True
        return n
      
      floor  = "basement ground-floor first-floor second-floor third-floor".split()
      make   = "MG Audi Ford Vauxhall Toyota".split()
      colour = "silver blue black white red".split()
      names  = "Anne Beth Carol Dave Eric".split()
      A, B, C, D, E = range(5)
      
      # dictionary column number per item
      colno = {x: 0 if x in floor else 1 if x in colour else 2 
                 for x in floor + make + colour}
      
      # make an initial list per person with all possibilities
      lst = [[floor, colour, make] for _ in range(5)]
      error = False
      
      # initial constraints
      remove(A, ["basement"])         # "A does not live in the basement"
      remove(A, ["Audi"])             # "A's car is not the silver Audi"
      remove(A, ["silver", "blue"])   # "A's car is not blue"
      remove(E, ["black"])            # "E's car is not black"
      # "the Vauxhall belongs to A, B, or C"
      remove(D, ["Vauxhall"])
      remove(E, ["Vauxhall"])
      # "D lives one floor below C"
      remove(D, ["third-floor"])
      remove(C, ["basement"])
      update([D], ["Toyota"])         # "D owns the Toyota"
      
      # check/update relations until <lst> is no longer updated
      n = check()    
      if error: 
        print("no solution")
      elif n == 15:
        for name, vals in zip(names, lst):
          print(f"{name}:", ", ".join(x[0] for x in vals))
      elif n > 15:
        # start with person for which the most is known
        ns = sorted([(sum(len(y) for y in x), i) for i, x in enumerate(lst)])
        ns = [i for x, i in ns if x > 3]
        # try all options 
        for s in solve(3, ns[0], deepcopy(lst[ns[0]]), ns):
          for name, vals in zip(names, lst):
            print(f"{name}:", ", ".join(x[0] for x in vals))
          print("answer:", [[name for name, vals in zip(names, lst) 
                if col in vals[1]][0] for col in ["red", "white", "blue"]])
      

      Like

  • Unknown's avatar

    Jim Randell 9:39 am on 27 May 2025 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2520: [Phone number] 

    From The Sunday Times, 9th January 2011 [link] [link]

    I have a brand-new 11-digit phone number. The first and last digits are zero and the middle section uses each of the digits 1 to 9 once. If you called the number by pressing the buttons on the standard keypad of a modern phone, you would find no two consecutive digits of the number in the same row or column. Looking at the fifth and sixth digits, I see one is double the other. The second and third digits differ by two, the seventh is lower than the sixth and the 10th is odd, and one more than the eighth.

    What is my number?

    This puzzle was originally published with no title.

    [teaser2520]

     
    • Jim Randell's avatar

      Jim Randell 9:40 am on 27 May 2025 Permalink | Reply

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

      The following run file executes in 77ms. (Internal runtime of the generated program is 170µs).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # the number is: 0ABCDEFGHI0
      --digits="1-9"  # for A-I
      
      # a keypad is laid out as:
      #
      #   1 2 3
      #   4 5 6
      #   7 8 9
      #     0
      #
      # map digits to (<row>, <col>):
      #              0  1  2  3  4  5  6  7  8  9
      --code="row = [4, 1, 1, 1, 2, 2, 2, 3, 3, 3]"
      --code="col = [2, 1, 2, 3, 1, 2, 3, 1, 2, 3]"
      
      # no digits share row or col with an adjacent digit
      --code="check = lambda x, y: col[x] != col[y] and row[x] != row[y]"
      "check(0, A)"
      "check(A, B)"
      "check(B, C)"
      "check(C, D)"
      "check(D, E)"
      "check(E, F)"
      "check(F, G)"
      "check(G, H)"
      "check(H, I)"
      "check(I, 0)"
      
      # one of 5th or 6th digits is double the other
      "(D == 2 * E) or (E == 2 * D)"
      
      # 2nd and 3rd digits differ by 2
      "abs(A - B) = 2"
      
      # 7th digit is lower than 6th
      "F < E"
      
      # 10th digit is odd, and one more than 8th
      "I % 2 = 1"
      "G + 1 = I"
      
      --template="0 A B C D E F G H I 0"
      --solution=""
      

      Solution: The number is: 03594816270.

      Like

    • Ruud's avatar

      Ruud 3:02 pm on 27 May 2025 Permalink | Reply

      Very brute force, but still vey fast:

      row = {0: 0, 1: 1, 2: 1, 3: 1, 4: 4, 5: 4, 6: 4, 7: 7, 8: 7, 9: 7}
      column = {0: 2, 1: 1, 2: 2, 3: 3, 4: 1, 5: 2, 6: 3, 7: 1, 8: 2, 9: 3}
      
      for n in itertools.permutations(range(1, 10)):
          if (
              ((number:=(None,0)+ n + (0,))[5] == 2 * number[6] or number[6] == 2 * number[5])
              and abs(number[2] - number[3]) == 2
              and number[7] < number[6]
              and number[10] % 2 == 1
              and number[10] == number[8] + 1
              and all(row[number[i]] != row[number[i + 1]] and column[number[i]] != column[number[i + 1]] for i in range(1, 11))
          ):
              print("".join(str(number[i]) for i in range(1, 12)))
      

      Like

    • Frits's avatar

      Frits 6:29 pm on 27 May 2025 Permalink | Reply

      #! python3 -m enigma -rr
       
      SubstitutedExpression
      
      # the number is: 0ABCDEFGHI0
      --digits="1-9"  # for A-I
       
      # a keypad is laid out as:
      #
      #   1 2 3      
      #   4 5 6      
      #   7 8 9      
      #     0       
      #
      # map digits to (<row>, <col>):
      #              0  1  2  3  4  5  6  7  8  9
      --code="row = [4, 1, 1, 1, 2, 2, 2, 3, 3, 3]"
      --code="col = [2, 1, 2, 3, 1, 2, 3, 1, 2, 3]"
      
      --invalid="1|3|5|7|9,G"     # G is even
      --invalid="1|3|5|6|7|9,DE"  # {D, E} is either {2, 4} or {4, 8}
      --invalid="8|9,F"           # F < E and E is 2, 4 or 8
      --invalid="4,ABCFGH"        # either D = 4 or E = 4
      --invalid="2|4|6|8,AB"      # D, E and G are even, so A and B can't be even
      --invalid="1|9,AB"          # A and B not both on same row
      --invalid="5,CDEFGH"        # {3, 5, 7} remains for A and B
      --invalid="2|5|8,A"         # not adjacent to "0" button
      
      --assign="B,5"              # either A = 5 or B = 5 but also A != 5
      --invalid="2|4|6|8,AC"      # not on same row/col as B(5)
       
      # no digits share row or col with an adjacent digit
      --code="check = lambda x, y: col[x] != col[y] and row[x] != row[y]"
      "check(A, B)"
      "check(B, C)"
      "check(C, D)"
      "check(D, E)"
      "check(E, F)"
      "check(F, G)"
      "check(G, H)"
      "check(H, I)"
      
      # parity of F and H is unknown
      "45 - (A + B + C + D + E + F + G + I) = H"
       
      # one of 5th or 6th digits is double the other
      "(D == 2 * E) or (E == 2 * D)"
       
      # 2nd and 3rd digits differ by 2
      "abs(A - B) = 2"
       
      # 7th digit is lower than 6th
      "F < E"
       
      # 10th digit is odd, and one more than 8th
      "G + 1 = I"
       
      --template="0 A B C D E F G H I 0"
      --distinct="ABCDEFGI"
      --solution=""
      

      or

      N = 3
      r = lambda i: (i - 1) // N
      c = lambda i: (i - 1) % N if i else 1
      
      forbidden = {i: set(j for j in range(10) if j != i and
                      (r(i) == r(j) or c(i) == c(j))) for i in range(10)}
      
      def solve(k=9, ns=set(range(1, 10)), ss=[0]):
        if k == 0:
          # check 10th number
          if 0 not in forbidden[ss[-1]] and ss[-1] == ss[-3] + 1:
            yield ss + [0]
        else:
          match(10 - k):
           case(3): 
             if abs(ss[-1] - ss[-2]) != 2: return
           case(6): 
             if not (ss[-1] == 2 * ss[-2] or ss[-2] == 2 * ss[-1]): return
           case(7):   
             if ss[-1] > ss[-2]: return
           case(8):    
             if ss[-1] % 2: return
      
          for n in ns:
            if n not in forbidden[ss[-1]]:
              yield from solve(k - 1, ns - {n}, ss + [n])
      
      for s in solve():
        print("answer:", ''.join(str(x) for x in s))
      

      Like

  • Unknown's avatar

    Jim Randell 9:15 am on 4 February 2025 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2571: Dog show 

    From The Sunday Times, 1st January 2012 [link] [link]

    Five dogs were entered by owners Alan, Brian, Colin, David and Edward, whose surnames are Allen, Bryan, Collins, Davis and Edwards. No owner had the same first and second initials, no two had the same pair of initials and none shared an initial with their breed.

    Colin was in position 1, David Allen’s dog was in position 4, Brian’s corgi was not next to the collie; the chow and doberman were at opposite ends. The other breed was a dachshund.

    In the voting, dogs eliminated in order were Mr Bryan’s, the corgi, David’s and the doberman.

    Who won, and what breed was their dog?

    [teaser2571]

     
    • Jim Randell's avatar

      Jim Randell 9:17 am on 4 February 2025 Permalink | Reply

      Initially, I found the wording of this puzzle a bit confusing.

      I think the situation is that the five competitors are lined up (I assigned them numbers 1 – 5 in the line), they are then inspected (in order) by the judges, who then deliberate, eliminating one competitor at a time until there is only the winner remaining. Although we are given the order of the elimination, this has nothing to do with the order of the line-up, but serves to tell us that the descriptions of the four eliminations identify four different competitors.

      I used the [[ SubstitutedExpression() ]] solver from the enigma.py library to check most of the required constraints. (I found it was easier to check the “no competitors shared the same pair of initials” condition after the slots in the line-up had been filled out).

      The following Python program runs in 73ms. (Internal runtime is 4.9ms).

      from enigma import (SubstitutedExpression, irange, seq_all_different, ordered, printf)
      
      # we allocate the competitor numbers: 1 - 5 to:
      #
      #  owner:   A B C D E  [A = Alan; B = Brian; C = Colin; D = David; E = Edward]
      #  surname: F G H I J  [F = Allen; G = Bryan; H = Collins; I = Davis; J = Edwards]
      #  breed:   K L M N P  [K = corgi; L = collie; M = chow; N = doberman; P = dachshund]
      
      # the distinct groups:
      distinct = [
        # each group is a permutation of the digits 1-5
        'ABCDE', 'FGHIJ', 'KLMNP',
        # no owner has the same first/last initial
        # no breed shares an initial with either of its owners names
        'AF', 'BG', 'CHKLM', 'DINP', 'EJ',
        # elimination order is: Bryan (G), corgi (K), David (D), doberman (N)
        # so these must all be in different positions
        'GKDN',
      ]
      
      # assignments we are given:
      #   Colin (C) is competitor 1
      #   David Allen (D F) is competitor 4
      s2d = dict(C=1, D=4, F=4)
      
      # invalid assignments we are given:
      #   the chow (M) and the doberman (N) were at opposite ends (i.e. 1 and 5)
      d2i = dict.fromkeys([2, 3, 4], "MN")
      
      # additional constraints
      exprs = [
        # Brian (B)'s corgi (K) was not next to the collie (L)
        "B = K",
        "abs(K - L) > 1",
      ]
      
      # construct the puzzle
      p = SubstitutedExpression(
        exprs,
        base=6, digits=irange(1, 5),
        distinct=distinct, s2d=s2d, d2i=d2i,
        answer="((A, B, C, D, E), (F, G, H, I, J), (K, L, M, N, P))",
      )
      
      # labels for owners / surnames / breeds
      owners = "Alan Brian Colin David Edward".split()
      surnames = "Allen Bryan Collins Davis Edwards".split()
      dogs = "corgi collie chow doberman dachshund".split()
      
      # solve the puzzle and fill out the slots in the line-up
      for ss in p.answers(verbose=0):
        slots = dict((k, [None] * 3) for k in irange(1, 5))
        for (i, (s, labels)) in enumerate(zip(ss, [owners, surnames, dogs])):
          for (k, v) in zip(s, labels):
            slots[k][i] = v
        # check: no two owners share the same pair of initials
        if not seq_all_different(ordered(fn[0], sn[0]) for (fn, sn, br) in slots.values()): continue
      
        # output the competitors, and identify the winner
        for k in sorted(slots.keys()):
          (fn, sn, br) = slots[k]
          eliminated = (sn == 'Bryan' or br == 'corgi' or fn == 'David' or br == 'doberman')
          printf("({k}) {fn} {sn}, {br}{w}", w=('' if eliminated else ' [WINNER]'))
        printf()
      

      Solution: The winner was the dachshund, owned by Alan Collins.

      The line-up of the competitors was:

      (1) Colin Edwards, doberman
      (2) Brian Davis, corgi
      (3) Alan Collins, dachshund [WINNER]
      (4) David Allen, collie
      (5) Edward Bryan, chow

      Like

  • Unknown's avatar

    Jim Randell 10:41 am on 28 January 2025 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2582: Anyone for tennis? 

    From The Sunday Times, 18th March 2012 [link] [link]

    The girls of St Trinian’s choose two games from the four on offer. In Felicity’s gang of four, no game was chosen by both Daphne and Erica; Chloe and Miss Brown chose lacrosse; Miss Smith chose netball; and only Miss Jones excluded hockey. In Harriet’s gang of four, Clara, Debbie and Ellen all chose netball but only one of the four chose hockey. To avoid having two girls with the same first name initial in any one game, one of the girls in the second group (but not Debbie) moved from one game to another. This meant that there was an odd number of these [eight] girls playing each game.

    Which of the eight girls played tennis?

    [teaser2582]

     
    • Jim Randell's avatar

      Jim Randell 10:42 am on 28 January 2025 Permalink | Reply

      We want each of the sports to end up with an odd number of participants after the swap.

      So before the swap there must be two sports that have an even number of participants (and the other two sports have an odd number of participants). And the swap must occur from one of the sports with an even number of participants to the other sport with an even number of participants. Leaving all sports with an odd number.

      Furthermore, the girl who swaps (who is in the second group, and shares and initial with a member of the first group, and is not Debbie – so must be Clara or Ellen), must have chosen one of the even sports (that was also chosen by her counterpart with the same initial), and not chosen the other even sport (that was also not chosen by her counterpart with the same initial, otherwise the swap would not remedy the situation). And this must be the only situation where a sport has been chosen by two girls with the same initial.

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to determine possible choices for each of the groups separately, and then combines possibilities to consider possible choices for the 8 girls, and then checks the remaining conditions for the combined choices.

      It runs in 82ms. (Internal runtime is 11.5ms).

      from enigma import (
        SubstitutedExpression, cproduct, chain, filter2, multiset,
        singleton, diff, join, printf
      )
      
      # possible choices are:
      #
      #   k   L N H T
      #   0 = - - x x (HT)
      #   5 = x x - - (LN)
      #   1 = - x - x (NT)
      #   4 = x - x - (LH)
      #   2 = - x x - (NH)
      #   3 = x - - x (LT)
      #
      # such that for a choice k the unchosen options are (5 - k)
      
      # map choice numbers to sports (0 - 5)
      choice = ['HT', 'NT', 'NH', 'LT', 'LH', 'LN']
      
      # sets for the four sports
      macros = {
        'lacrosse': "{3, 4, 5}",
        'netball': "{1, 2, 5}",
        'hockey': "{0, 2, 4}",
        'tennis': "{0, 1, 3}",
      }
      
      # group 1:
      # for sport choices allocate: C, D, E, F to k-values (0-5)
      # for surnames allocate: B, S, J to values 0-3 (= C - F) [distinct]
      exprs1 = [
        # "no game was chosen by both D and E"
        "D + E == 5",
        # "C chose lacrosse"
        "C in @lacrosse",
        # "Miss Brown (who is not C) also chose lacrosse"
        "B != 0",
        "[C, D, E, F][B] in @lacrosse",
        # "Miss Smith chose netball"
        "[C, D, E, F][S] in @netball",
        # only Miss Jones excluded hockey
        "all((x in @hockey) == (i != J) for (i, x) in enumerate([C, D, E, F]))",
      ]
      
      names1 = "Chloe Daphne Erica Felicity".split()
      surnames1 = "Brown Smith Jones".split()
      
      def solve1():
        # find the first group
        p1 = SubstitutedExpression(
          exprs1, base=6, macro=macros,
          distinct="BSJ", d2i={ 4: "BSJ", 5: "BSJ" },
        )
        for s in p1.solve(verbose=0):
          # map names to choices
          d1 = dict((x, choice[s[x[0]]]) for x in names1)
          # map names to surnames
          sn = dict((names1[s[x[0]]], x) for x in surnames1)
          yield (d1, sn)
      
      
      # group 2:
      # for sport choices allocate: C, D, E, H to k-values (0-5)
      exprs2 = [
        # "C, D, E all chose netball"
        "{C, D, E}.issubset(@netball)",
        # "only one of C, D, E, H chose hockey"
        "sum(x in @hockey for x in [C, D, E, H]) == 1",
      ]
      
      names2 = "Clara Debbie Ellen Harriet".split()
      
      def solve2():
        # find the second group
        p2 = SubstitutedExpression(exprs2, base=6, macro=macros, distinct="")
        for s in p2.solve(verbose=0):
          # map names to choices
          d2 = dict((x, choice[s[x[0]]]) for x in names2)
          yield d2
      
      
      # output a group
      def output(g, sn=None):
        for k in sorted(g.keys()):
          s = (sn.get(k) if sn else None)
          name = (k + " " + s if s else k)
          printf("{name} -> {v}", v=join(g[k], sep=' + '))
        printf()
      
      # choose solutions for each group
      for ((g1, sn), g2) in cproduct([solve1(), solve2()]):
        # make the combined solution
        g = dict(chain(g1.items(), g2.items()))
        # map each sport to a list of girls
        s = dict((k, list()) for k in 'LNHT')
        for (k, vs) in g.items():
          for v in vs:
            s[v].append(k)
      
        # find sports with even and odd numbers of participants
        (even, odd) = filter2((lambda k: len(s[k]) % 2 == 0), s.keys())
        # there must be 2 even sports
        if len(even) != 2: continue
      
        # find sports that have more than one participant with a given initial
        ms = list()
        for (k, vs) in s.items():
          ds = list(x for (x, n) in multiset.from_seq(v[0] for v in vs).items() if n > 1)
          if ds: ms.append((k, ds))
        # there must be just one sport with just one duplicated initial
        if not (len(ms) == 1 and len(ms[0][1]) == 1): continue
        (x, i) = (ms[0][0], ms[0][1][0])
        # the duplicate sport must have an even number of participants
        # and we must swap to the other sport with an even number of participants
        y = singleton(diff(even, {x}))
        if y is None: continue
        # the duplicate initial must be C or E
        swap = other = None
        if i == 'C':
          # Clara needs to swap from x to y
          (swap, other) = ('Clara', 'Chloe')
        elif i == 'E':
          # Ellen needs to swap from x to y
          (swap, other) = ('Ellen', 'Erica')
        # check the swap fixes the issue
        if swap is None or not (x in g[swap] and y not in g[swap] and y not in g[other]): continue
      
        # final list for tennis
        ts = list(s['T'])
        if x == 'T': ts.remove(swap)
        if y == 'T': ts.append(swap)
      
        # output solution:
        # first output the choices for each group
        output(g1, sn)
        output(g2)
        # output the swap, and the final list for tennis
        printf("{swap} swaps {x} -> {y}")
        printf("-> tennis = {ts}", ts=join(ts, sep=", ", sort=1))
        printf()
      

      Solution: The girls playing tennis are: Clara, Debbie, Erica.

      The initial choices made are:

      Chloe = lacrosse + hockey
      Daphne Brown = lacrosse + hockey
      Erica Jones = netball + tennis
      Felicity Smith = netball + hockey

      Clara = netball + tennis
      Debbie = netball + tennis
      Ellen = lacrosse + netball
      Harriet = netball + hockey

      But both Erica and Ellen have chosen netball, so there would be 2 girls with the same initial in the netball group.

      To fix this Ellen swaps from netball to hockey, giving the following assignments:

      lacrosse = Chloe, Daphne, Ellen (CDE, 3)
      netball = Clara, Debbie, Erica, Felicity, Harriet (CDEFH, 5)
      hockey = Chloe, Daphne, Ellen, Felicity, Harriet (CDEFH, 5)
      tennis = Clara, Debbie, Erica (CDE, 3)

      Like

  • Unknown's avatar

    Jim Randell 4:55 pm on 27 November 2024 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2621: Come dancing 

    From The Sunday Times, 16th December 2012 [link] [link]

    Angie, Bianca, Cindy and Dot were given dance partners and performed in front of four judges, Juno, Kraig, Len and Marcia. Each judge placed the performances in order and gave 4 marks to the best, then 3, 2 and 1 point to the others. The dancers’ marks were then added up and they finished in alphabetical order with no ties. Angie’s winning margin over Bianca was larger than Bianca’s over Cindy, and Bianca’s winning margin over Cindy was larger than Cindy’s over Dot.

    Juno’s ordering of the four was different from the final order, and Kraig’s 4 points and Len’s 3 points went to dancers who were not in the top two.

    How many points did Cindy get from Juno, Kraig, Len and Marcia respectively?

    [teaser2621]

     
    • Jim Randell's avatar

      Jim Randell 4:56 pm on 27 November 2024 Permalink | Reply

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

      The following run file executes in 90ms. (Internal runtime of the generated program is 10.5ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #     A B C D
      #  J: a b c d
      #  K: e f g h
      #  L: i j k m
      #  M: n p q r
      
      --digits="1-4"
      --distinct="abcd,efgh,ijkm,npqr"
      
      # totals for each
      --macro="@A = ({a} + {e} + {i} + {n})"
      --macro="@B = ({b} + {f} + {j} + {p})"
      --macro="@C = ({c} + {g} + {k} + {q})"
      --macro="@D = ({d} + {h} + {m} + {r})"
      
      # A's margin over B is larger than B's margin over C
      "@A - @B > @B - @C" "@B - @C > 0"
      # B's margin over C is larger than C's margin over D
      "@B - @C > @C - @D" "@C - @D > 0"
      
      # J's order was different from the final order, so not (4, 3, 2, 1)
      "({a}, {b}, {c}, {d}) != (4, 3, 2, 1)"
      
      # K's 4 did not go to A or B
      --invalid="4,ef"
      
      # L's 3 did not go to A or B
      --invalid="3,ij"
      
      --template="A=({a} {e} {i} {n}) B=({b} {f} {j} {p}) C=({c} {g} {k} {q}) D=({d} {h} {m} {r})"
      --solution=""
      --verbose="1-H"
      

      Solution: Cindy’s points were: Juno = 1, Kraig = 4, Len = 1, Marcia = 2.

      The complete scores are (J + K + L + M):

      A: 4 + 3 + 4 + 4 = 15
      B: 3 + 2 + 2 + 3 = 10
      C: 1 + 4 + 1 + 2 = 8
      D: 2 + 1 + 3 + 1 = 7

      Like

    • Ruud's avatar

      Ruud 8:25 am on 28 November 2024 Permalink | Reply

      import itertools
      
      
      def order(lst):
          return [lst.index(el) for el in sorted(lst, reverse=True)]
      
      
      for juno, kraig, len, marcia in itertools.product(itertools.permutations(range(1, 5)), repeat=4):
          totals = [sum(x) for x in zip(juno, kraig, len, marcia)]
          if sorted(totals, reverse=True) == totals:
              if totals[0] - totals[1] > totals[1] - totals[2] > totals[2] - totals[3] > 0:
                  if order(juno) != order(totals):
                      if kraig.index(4) in (2, 3) and len.index(3) in (2, 3):
                          print(juno[2], kraig[2], len[2], marcia[2])
      

      Like

  • Unknown's avatar

    Jim Randell 6:51 pm on 4 September 2024 Permalink | Reply
    Tags: by: Angela Newing   

    Brainteaser 1047: Youth opportunities 

    From The Sunday Times, 22nd August 1982 [link]

    Five of the shops in our local High Street sell cakes, electrical goods, greengrocery, hardware and shoes. Each decided recently take on two young assistants, one of each sex, from the Youth Opportunities Scheme.

    These ten lucky youngsters include Hazel and Stephen, who live on either side of my house, and Eric from across the road. He gave me the following information:

    The initials of the assistants’ forenames are all different from the initial of the shop in which they work. Moreover no boy works with a girl whose initial is the same as his own. In addition, Emma does not work with Henry, nor does she work in the shoe shop.

    Henry doesn’t work the in the cake shop, Gordon is not in the hardware shop, Gwen is not in the shoe shop, and  neither Charles nor Sheila work in the greengrocer’s.

    If Carol works in the hardware shop, who sells the electrical goods?

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

    [teaser1047]

     
    • Jim Randell's avatar

      Jim Randell 6:51 pm on 4 September 2024 Permalink | Reply

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

      Most of the conditions can be dealt with using the [[ --distinct ]] and [[ --invalid ]] parameters.

      The following run file executes in 70ms. (Internal runtime of the generated code is just 27µs).

      Run: [ @jdoodle ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # shops: 0 1 2 3 4  (= Cakes, Electricals, Greengrocers, Hardware, Shoes)
      # boys:  Q R S T U  (= Charles, Eric, Gordon, Henry, Steven; in some order)
      # girls: V W X Y Z  (= Carol, Emma, Gwen, Hazel, Shiela; in some order)
      
      --base=5
      # boys and girls have different initials
      # but no-one works with someone with the same initial
      --distinct="QRSTU,VWXYZ,QV,RW,SX,TY,UZ"
      
      # no-one shares an initial with the shop
      # Emma does not work in the shoe shop
      # Gwen does not work in the shoe shop
      # Henry does not work in the cake shop
      # Gordon does not work in the hardware shop
      # neither Charles nor Shiela work in the greengrocers
      --invalid="0,QVS"   # C = 0
      --invalid="1,RWZ"   # E = 1
      --invalid="2,SXZT"  # G = 2
      --invalid="3,TYQ"   # H = 3
      --invalid="4,UZX"   # S = 4
      
      # Carol works in the hardware shop
      --assign="Y,0"
      
      # Henry and Emma do not work together
      "(Q != 3) or (V != 1)" "(S != 3) or (X != 1)"
      
      --answer="((Q, R, S, T, U), (V, W, X, Y, Z))"
      --template="(Q R S T U) (V W X Y Z)"
      --solution=""
      

      We can write additional Python code to format the output nicely:

      from enigma import (SubstitutedExpression, printf)
      
      # label shops and people
      shops = "Cakes Electricals Greengrocers Hardware Shoes".split()
      boys  = "Charles Eric Gordon Henry Steven".split()
      girls = "Carol Emma Gwen Hazel Shiela".split()
      
      # load the puzzle
      p = SubstitutedExpression.from_file("teaser1047.run")
      
      # solve the puzzle, and format solutions
      for (bs, gs) in p.answers(verbose=0):
        for (s, (b, g)) in enumerate(zip(bs, gs)):
          printf("{s} = {b} + {g}", s=shops[s], b=boys[b], g=girls[g])
        printf()
      

      Solution: Henry and Gwen work in the electrical goods shop.

      The full solution is:

      Cakes = Gordon + Shiela
      Electricals = Henry + Gwen
      Greengrocers = Steven + Emma
      Hardware = Eric + Carol
      Shoes = Charles + Hazel

      Like

      • Frits's avatar

        Frits 10:35 am on 5 September 2024 Permalink | Reply

        This is much more intuitive to me:

        #! python3 -m enigma -r
         
        SubstitutedExpression
         
        # shops: 0 1 2 3 4  (= Cakes, Electricals, Greengrocers, Hardware, Shoes)
        # boys:  Q R S T U  (= Charles, Eric, Gordon, Henry, Steven)
        # girls: V W X Y Z  (= Carol, Emma, Gwen, Hazel, Sheila)
         
        --base=5
        # boys and girls have different initials
        # but no-one works with someone with the same initial
        --distinct="QRSTU,VWXYZ,QV,RW,SX,TY,UZ"
         
        # no-one shares an initial with the shop
        # Emma does not work in the shoe shop
        # Gwen does not work in the shoe shop
        # Henry does not work in the cake shop
        # Gordon does not work in the hardware shop
        # neither Charles nor Sheila work in the greengrocers
        --invalid="0,QVT"   # C = 0
        --invalid="1,RW"    # E = 1
        --invalid="2,SXQZ"  # G = 2
        --invalid="3,TYS"   # H = 3
        --invalid="4,UZWX"  # S = 4
         
        # Carol works in the hardware shop
        --assign="V,3"
         
        # Henry and Emma do not work together
        "T != W"
         
        --answer="((Q, R, S, T, U), (V, W, X, Y, Z))"
        --template="(Q R S T U) (V W X Y Z)"
        --solution=""
        

        and

        from enigma import (SubstitutedExpression, printf)
         
        # label shops and people
        shops = "Cakes Electricals Greengrocers Hardware Shoes".split()
        boys  = "Charles Eric Gordon Henry Steven".split()
        girls = "Carol Emma Gwen Hazel Sheila".split()
         
        # load the puzzle
        p = SubstitutedExpression.from_file("teaser/teaser1047.run")
         
        # solve the puzzle, and format solutions
        for (bs, gs) in p.answers(verbose=0):
          for i in range(5):
            b, g = boys[bs.index(i)], girls[gs.index(i)]
            print(shops[i], "=", b, "+", g)
        

        Like

  • Unknown's avatar

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

    Teaser 3220: Graffiti 

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

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

    The answers were:

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

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

    What was the correct order for the upside down ones?

    [teaser3220]

     
    • Jim Randell's avatar

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

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

      Run: [ @replit ]

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

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

      So the number of correct identifications for each pupil were:

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

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

      Like

      • Jim Randell's avatar

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

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

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

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

        Run: [ @replit ]

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

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

        Like

    • Frits's avatar

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

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

      Like

      • Frits's avatar

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

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

        Like

        • Ruud's avatar

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

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

          Like

    • Ruud's avatar

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

      Here’s my version:

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

      Like

    • Rob's avatar

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

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

      Like

  • Unknown's avatar

    Jim Randell 8:04 am on 16 May 2024 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2615: Gunpowder, treason and plot 

    From The Sunday Times, 4th November 2012 [link] [link]

    Last year I overheard this conversation one day during the first seven days of November:

    Alec: It’s the 5th today, let’s have a bonfire.
    Bill: No, the 5th is tomorrow.
    Chris: You’re both wrong — the 5th is the day after tomorrow.
    Dave: All three of you are wrong.
    Eric: Yesterday was certainly not the 1st.
    Frank: We’ve missed bonfire night.

    If you knew how many of their statements were true, then you could work out the date in November on which this conversation took place.

    What was that date?

    In Britain, Bonfire night is 5th November.

    [teaser2615]

     
    • Jim Randell's avatar

      Jim Randell 8:05 am on 16 May 2024 Permalink | Reply

      A straightforward puzzle to solve, either manually or programatically.

      This Python program evaluates the statements on each of the 7 possible days, and looks for situations where the number of true statements uniquely identifies a single day.

      Run: [ @replit ]

      from enigma import (irange, group, seq2str, printf)
      
      # calculate statement values for date <d>
      def statements(d):
        # A: "It is the 5th today ..."
        A = (d == 5)
        # B: "The 5th is tomorrow ..."
        B = (d + 1 == 5)
        # C: "The 5th is the day after tomorrow ..."
        C = (d + 2 == 5)
        # D: "A, B, C are all wrong ..."
        D = not (A or B or C)
        # E: "Yesterday was not the 1st ..."
        E = (d - 1 != 1)
        # F: We have missed bonfire night (the 5th) ..."
        F = (d > 5)
        return [A, B, C, D, E, F]
      
      # group dates by the number of true statements
      g = group(irange(1, 7), by=(lambda d: statements(d).count(True)))
      
      # look for groups with a single member
      for (k, vs) in g.items():
        if len(vs) == 1:
          printf("{k} true -> {vs} November", vs=seq2str(vs, sort=1, enc=""))
      

      Solution: The conversation took place on 2nd November.

      There is only 1 true statement, and it is made by Dave.

      Like

    • Frits's avatar

      Frits 10:31 am on 16 May 2024 Permalink | Reply

      # the number of correct statements 
      fn = lambda d: sum([d == 5, d == 4, d == 3, d < 3 or d > 5, d != 2, d > 5])
       
      seen, sols = set(), dict()
      # now find a count that is unique
      for d in range(1, 8):
        if (c := fn(d)) in seen:
          try:
            del sols[c]
          except KeyError:
            pass
        else:
          sols[c] = str(d)  
          seen.add(c)  
          
      print(f"answer: November {' or '.join(sols.values())}")
      

      Like

    • Lise Andreasen's avatar

      Lise Andreasen 7:06 pm on 16 May 2024 Permalink | Reply

      T = the date today.
      A: T = 5.
      B: T = 4.
      C: T = 3.
      D: T != 3, 4, 5.
      E: T != 2.
      F: T = 6 v T = 7.
      Of A, B, C and D, 1 is true, 3 are false.
      There can therefore be 1, 2 or 3 true statements.
      With 3 true statements, both E and F are true. T could be 6 or 7.
      With 2 true statements, either E or F is true. If E is true, T = 2. If F is true, T could be 1, 3, 4 or 5.
      With 1 true statement, both E and F are false. T = 2. This is the only option leading to 1 answer. Therefore there was 1 true statement and it’s 2nd November.

      Like

    • GeoffR's avatar

      GeoffR 1:54 pm on 17 May 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..7: T; % Date conversation took place
      
      % A - F statements
      constraint sum([T==5, T==4, T==3, T!=3 \/ T!=4 \/ T!=5,
      T!=2, T==6 \/ T==7]) == 1;
      
      solve satisfy;
      
      output [" T = " ++ show(T)];
      
      % T = 2;
      %---------
      %=========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:18 am on 30 November 2023 Permalink | Reply
    Tags: by: Angela Newing   

    Brainteaser 1449: Magic! 

    From The Sunday Times, 17th June 1990 [link]

    This is a magic square containing all the numbers from 1 to 16 once each. As is always the case with magic squares, all rows, columns, and full-length diagonals add up to the same sum (but all our shorter diagonals add to less than that sum).

    Fill in the missing numbers.

    [teaser1449]

     
    • Jim Randell's avatar

      Jim Randell 9:19 am on 30 November 2023 Permalink | Reply

      If we consider the four rows that make up the square, each row sums to the magic constant k, and between all 4 rows each number is included exactly once. So:

      4k = sum(1 .. 16)
      ⇒ k = 34

      We can now use the [[ SubstitutedExpression ]] solver from the enigma.py library to fill out the remaining squares.

      The following run file executes in 96ms. (Internal runtime of the generated program is 853µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C D
      #  E F G H
      #  I J K L
      #  M N P Q
      
      # digits 1-16 are used:
      --base="17"
      --digits="1-16"
      
      # rows
      "A + B + C + D == 34"
      "E + F + G + H == 34"
      "I + J + K + L == 34"
      "M + N + P + Q == 34"
      
      # columns
      "A + E + I + M == 34"
      "B + F + J + N == 34"
      "C + G + K + P == 34"
      "D + H + L + Q == 34"
      
      # diagonals
      "A + F + K + Q == 34"
      "D + G + J + M == 34"
      
      # short diagonals
      "B + G + L < 34"
      "C + F + I < 34"
      "E + J + P < 34"
      "H + K + N < 34"
      
      # given values
      "A == 9"
      "J == 13"
      "N == 12"
      "Q == 14"
      
      --template=""
      --verbose="AT"
      

      Solution: Here is the completed square:

      Like

    • NigelR's avatar

      NigelR 6:22 pm on 30 November 2023 Permalink | Reply

      Hi Jim
      I agree your solution, but I’m not sure how you reached it with your short diagonal condition of
      “E + J + P < 34" (which isn't satisfied by the solution). Shouldn't it be E+J+O?

      Like

      • NigelR's avatar

        NigelR 6:25 pm on 30 November 2023 Permalink | Reply

        Apologies: Just realised that your notation didn’t include ‘O’!!

        Like

      • Jim Randell's avatar

        Jim Randell 7:55 pm on 30 November 2023 Permalink | Reply

        @NigelR: I often skip O as a variable, as it is easily confused with 0 (zero).

        Like

    • GeoffR's avatar

      GeoffR 7:02 pm on 1 December 2023 Permalink | Reply

      #  A B C D
      #  E F G H
      #  I J K L
      #  M N P Q
      
      from itertools import product, permutations
      from enigma import all_different
      
      # Given grid values
      A, J, N, Q = 9, 13, 12, 14
      
      # Find remaining 12 digits
      digits = set(range(1,17)).difference({9, 13, 12, 14 }) 
      
      # 1st row of grid
      for B, C, D in product(digits, repeat=3):
        if not all_different(A, B, C, D):continue
        if A + B + C + D != 34:continue
        R1 = list(digits.difference({B, C, D}))
        
        # 2nd row of grid
        for E, F, G, H in product(R1, repeat=4):
          if not all_different(E, F, G, H):continue
          if E + F + G + H != 34:continue
          R2 = set(R1).difference({E, F, G, H})
        
          # 3rd row of grid
          for I, K, L in product(R2, repeat=3):
            if not all_different(I, J, K, L):continue
            if I + J + K + L != 34:continue
            if B + G + L >= 34:continue
            if C + F + I >= 34:continue
            
            # only M and P are missing from the 4th row of grid
            R3 = list(set(R2).difference({I, K, L}))
            for M, P in permutations(R3, 2):
              # columns
              if A + E + I + M != 34:continue
              if B + F + J + N != 34:continue
              if C + G + K + P != 34:continue
              if D + H + L + Q != 34:continue
              # diagonals
              if E + J + P >= 34:continue
              if H + K + N >= 34:continue
              if A + F + K + Q != 34:continue
              if D + J + G + M != 34:continue
              
              print('A, B, C, D = ', A,B,C,D)
              print('E, F, G, H = ', E,F,G,H)
              print('I, J, K, L = ', I,J,K,L)
              print('M, N, P, Q = ', M,N,P,Q)
              exit() # only 1 solution needed
      
      # A, B, C, D =  9 6 15 4
      # E, F, G, H =  16 3 10 5
      # I, J, K, L =  2 13 8 11
      # M, N, P, Q =  7 12 1 14
      
      
      
      

      Like

    • Hugo's avatar

      Hugo 10:01 am on 2 December 2023 Permalink | Reply

      I found a second solution. Did I do something wrong?

      9 2 15 8
      6 7 10 11
      16 13 4 1
      3 12 5 14

      Like

      • Jim Randell's avatar

        Jim Randell 11:03 am on 2 December 2023 Permalink | Reply

        Yes, the shorter diagonals have to sum less than 34.

        You have 16 + 7 + 15 = 38 in your diagram.

        Like

        • Hugo's avatar

          Hugo 11:16 am on 2 December 2023 Permalink | Reply

          Aha! I misread the condition as “not equal to that sum”.

          Like

  • Unknown's avatar

    Jim Randell 9:21 am on 19 October 2023 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2643: Pack points 

    From The Sunday Times, 19th May 2013 [link] [link]

    Five cubs were awarded points for effort. Enid’s son and Master Smith had 17 points between them, Masters Jones and Robinson had a total of 16, Ivan and Master Robinson together had 14, and the two youngest of the cubs had a total of 13. John and Master Brown had a total of 12 points, Brenda’s son and Mike had 11 points between them, Ken and his best friend had a total of 10, Ann’s son and Debbie’s son together had 9, Christine’s son and Nigel had a total of 8, and Master Perkins and Debbie’s son together had 6.

    In alphabetical order of their mothers’ Christian names, what were the names of the five cubs (Christian name and surname)?

    [teaser2643]

     
    • Jim Randell's avatar

      Jim Randell 9:21 am on 19 October 2023 Permalink | Reply

      If we use variables A, B, C, D, E to correspond to the points allocated (according to mothers name), then we can allocate the first names and surnames of the cubs to the same set of variables to give us a set of simultaneous linear equations, which we can attempt to solve to give the points values.

      I used the [[ Matrix.linear() ]] solver from the enigma.py library to find assignments that give non-negative points values. (Although there are no further solutions if negative values are permitted).

      I also ensure that the pairings mentioned in the text all consist of distinct cubs. (Although, again, there are no further solutions if a pairing can refer to the same cub twice).

      The following Python program runs in 375ms.

      Run: [ @replit ]

      from enigma import (Matrix, subsets, catch, printf)
      
      # suppose the points are: A, B, C, D, E (according to mothers name)
      # then we can map the first and surnames to these values
      
      # variables
      vs = "ABCDE"
      
      # a function to construct equations
      def eq(vs, k, fn=Matrix.equation(vs)):
        r = fn(vs, k)
        assert {0, 1}.issuperset(r[0])  # reject coefficients that aren't 0 or 1
        return r
      
      # validate point values
      def valid(x):
        assert not x < 0
        return x
      
      # for firstnames I, J, K, M, N
      for (I, J, K, M, N) in subsets(vs, size=len, select='P'):
      
        # for surnames P, R, S, T (= Brown), U = (Jones)
        for (P, R, S, T, U) in subsets(vs, size=len, select='P'):
      
          # try to solve the equations
          r = None
          try:
            # construct equations
            eqs = [
              eq(['E', S], 17),  # Enid + Smith = 17
              eq([U, R], 16),  # Jones + Robinson = 16
              eq([I, R], 14),  # Ivan + Robinson = 14
              eq([J, T], 12),  # John + Brown = 12
              eq(['B', M], 11),  # Brenda + Mike = 11
              eq(['A', 'D'], 9),  # Ann + Debbie = 9
              eq(['C', N], 8),  # Christine + Nigel = 8
              eq([P, 'D'], 6),  # Perkins + Debbie = 6
            ]
      
            # solve the equations
            r = Matrix.linear(eqs, valid=valid)
          except (ValueError, AssertionError):
            pass
          if r is None: continue
      
          # check there is a pair = 13 (two youngest), and a pair = 10 (Ken + friend)
          v = dict(zip(vs, r))
          if not any(v[x] + v[K] == 10 for x in vs if x != K): continue
          if not any(v[x] + v[y] == 13 for (x, y) in subsets(vs, size=2)): continue
      
          # output solution
          mn = { 'A': 'Anne', 'B': 'Brenda', 'C': 'Christine', 'D': 'Debbie', 'E': 'Enid' }
          fn = { I: 'Ivan', J: 'John', K: 'Ken', M: 'Mike', N: 'Nigel' }
          sn = { P: 'Perkins', R: 'Robinson', S: 'Smith', T: 'Brown', U: 'Jones' }
          for k in vs:
            printf("{fn} {sn} = {v} points [{mn}]", mn=mn[k], fn=fn[k], sn=sn[k], v=v[k])
          printf()
      

      Solution: The names of the cubs are:

      Mike Smith = 7 points [Anne]
      Ivan Perkins = 4 points [Brenda]
      Ken Jones = 6 points [Christine]
      Nigel Brown = 2 points [Debbie]
      John Robinson = 10 points [Enid]

      So the youngest two are Mike and Ken (= 13 points in total), and Ivan must be Ken’s best friend (= 10 points in total).

      Like

  • Unknown's avatar

    Jim Randell 9:18 am on 28 February 2023 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2715: Colour coded 

    From The Sunday Times, 5th October 2014 [link] [link]

    Five lads cycle together daily. Each has a different number of helmets, less than a dozen, and none of them has two of the same colour. Each wears in daily rotation all the cycle helmets he possesses. On 1st September, Alan wore mauve, Bill and Charles wore red, Dave orange and Eric green. On 11th September, two were red, one green, one mauve and one white. On 19th September, Dave’s was orange, Eric’s green and the others red. Eric wore orange on the 22nd and white on the 23rd. On 1st October all five wore the same as on 1st September.

    In alphabetical order of the riders, what were the helmet colours on the 11th September?

    [teaser2715]

     
    • Jim Randell's avatar

      Jim Randell 9:18 am on 28 February 2023 Permalink | Reply

      I am assuming that each cyclist cycles(!) through their helmets in strict rotation. So each time through they wear the same colours in the same order.

      None of them have duplicate colours, so if we see one of them wearing the same colour on different days, they must have completed a whole number of cycles between those days. And they are all wearing the same colours on 1st September and 1st October, so the length of each of their cycles must be a divisor of 30, less than 12 (i.e. 1, 2, 3, 5, 6, 10).

      For any given cyclist, if we are given a set of days and colours then we can look at days when the same colour is worn. The gap between the days must be a multiple (possibly 1) of the cycle length. And so by considering all possible gaps we can can determine possible cycle lengths. Furthermore, different colours must be worn on different days in the cycle. So

      This Python program calculates possible cycle lengths from the colours given on the specified dates.

      It runs in 55ms. (Internal runtime is 2.2ms).

      Run: [ @replit ]

      from enigma import (
        subsets, group, unpack, union, tuples, mgcd, divisors,
        seq_all_different, delete, update, rev, map2str, printf
      )
      
      # find cycle lengths for values give in ms
      def solve(ms, ns=dict()):
        # are we done?
        if not ms:
          yield ns
        else:
          # choose a key to process (the one with the most entries)
          x = max(ms.keys(), key=(lambda k: len(ms[k].keys())))
          # group days by colour
          g = group(ms[x].items(), by=unpack(lambda k, v: v), f=unpack(lambda k, v: k))
          # collect multiples of cycle length
          s = union((abs(b - a) for (a, b) in tuples(vs, 2)) for vs in g.values())
          # consider possible cycle lengths (must be < 12)
          for n in divisors(mgcd(*s)):
            if n > 11: break
            # is this one already used?
            if n in ns: continue
            # check different colours give different cycle days
            if not seq_all_different(vs[0] % n for vs in g.values()): continue
            # solve for the remaining items
            yield from solve(delete(ms, [x]), update(ns, [(n, x)]))
      
      # consider possible orderings for 11th
      for (a, b, c, d, e) in subsets('RRGMW', size=len, select='mP'):
        # known positions (day 1 = 1st September)
        ms = dict(
          A={ 1: 'M', 11: a, 19: 'R', 31: 'M' },
          B={ 1: 'R', 11: b, 19: 'R', 31: 'R' },
          C={ 1: 'R', 11: c, 19: 'R', 31: 'R' },
          D={ 1: 'O', 11: d, 19: 'O', 31: 'O' },
          E={ 1: 'G', 11: e, 19: 'G', 22: 'O', 23: 'W', 31: 'G' },
        )
        # find possible cycle lengths
        for ns in solve(ms):
          # output solution
          printf("day 11: (A B C D E) = ({a} {b} {c} {d} {e}) {ns}", ns=map2str(rev(ns), sep=" ", enc="[]"))
      

      Solution: On 11th September, the colours were: Alan = mauve; Bill = red; Charles = red; Dave = green; Eric = white.

      Alan has 5 or 10 helmets. Bill has 1 or 2. Charles has 2 or 1. Dave has 3. Eric has 6.

      We can’t work out all the colours, but here is what we do know:

      A: (M ? ? ? ?) or (M ? ? ? ? ? ? ? ? ?)
      B: (R) or (R ?)
      C: (R ?) or (R)
      D: (O ? ?)
      E: (G ? ? O W ?)

      However these cycle lengths are chosen, they all give the same sequence of values for the 11th September.

      Like

  • Unknown's avatar

    Jim Randell 8:50 am on 22 December 2022 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2693: Let the dog see the rabbit 

    From The Sunday Times, 4th May 2014 [link] [link]

    Each of Messrs Burrows, Cook, Field, Skinner and Warren has a pet dog, and each of the men’s wives has a pet rabbit. Each dog bit one of the rabbits, with no two dogs biting the same rabbit. Mrs Warren’s rabbit was bitten by Mr Skinner’s dog. Mr Warren’s dog bit the rabbit owned by the wife of the owner of the dog that bit Mrs Field’s rabbit. Mrs Burrows’ rabbit was bitten by the dog owned by the husband of the lady whose rabbit was bitten by the dog belonging to the husband of the lady whose rabbit was bitten by the dog belonging to Mr Cook.

    Whose dog bit Mrs Skinner’s rabbit, and whose rabbit did Mr Field’s dog bite?

    There are now 800 Teaser puzzles available on the site.

    [teaser2693]

     
    • Jim Randell's avatar

      Jim Randell 8:53 am on 22 December 2022 Permalink | Reply

      Each surname has a dog and a rabbit, so we can just attach the surnames directly to the animals.

      This Python program runs in 50ms. (Internal runtime is 180µs).

      Run: [ @replit ]

      from enigma import (subsets, printf)
      
      # label the surnames
      names = (B, C, F, S, W) = (0, 1, 2, 3, 4)
      
      # choose an ordering of the names for the rabbits
      # r[X] == Y  ->  "dog X bit rabbit Y"
      for r in subsets(names, size=len, select="P"):
      
        # "rabbit W was bitten by dog S"
        if not (r[S] == W): continue
      
        # "dog W bit rabbit of family of dog that bit rabbit F"
        if not (r[r[W]] == F): continue
      
        # "rabbit B was bitten by the dog of family whose rabbit was bitten by the
        # dog of family whose rabbit was bitten by dog C"
        if not (r[r[r[C]]] == B): continue
      
        # output solution (dog -> rabbit)
        name = "BCFSW"
        for (x, y) in zip(names, r):
          printf("dog {x} bit rabbit {y}", x=name[x], y=name[y])
        printf()
      

      Solution: Cook’s dog bit Skinner’s rabbit. Field’s dog bit Cook’s rabbit.

      The full solution is:

      dog B bit rabbit F
      dog C bit rabbit S
      dog F bit rabbit C
      dog S bit rabbit W
      dog W bit rabbit B

      Like

  • Unknown's avatar

    Jim Randell 8:28 am on 29 November 2022 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2685: Not the Gold Cup 

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

    Five horses took part in a race, but they all failed to finish, one falling at each of the first five fences. Dave (riding Egg Nog) lasted longer than Bill whose horse fell at the second fence; Big Gun fell at the third, and the jockey wearing mauve lasted the longest. Long Gone lasted longer than the horse ridden by the jockey in yellow, Chris’s horse fell one fence later than the horse ridden by the jockey in green, but Fred and his friend (the jockey in blue) did not fall at adjacent fences. Nig Nag was ridden by Wally and Dragon’s jockey wore red.

    Who was the jockey in yellow, and which horse did he ride?

    [teaser2685]

     
    • Jim Randell's avatar

      Jim Randell 8:28 am on 29 November 2022 Permalink | Reply

      I used the [[ SubstitutedExpression ]] solver from the enigma.py library to assign the fence values (1-5) to the 3 groups of jockeys, horses and colours.

      The following run file executes in 66ms. (Internal runtime of the generated program is 94µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # assign fence numbers (1-5) to the following groups:
      #
      #  jockeys = A (Wally), B (Bob), C (Chris), D (Dave), E (Fred)
      #  horses  = F (Egg Nog), G (Big Gun), H (Long Gone), I (Nig Nag), J (Dragon)
      #  colours = K (mauve), L (yellow), M (green), N (blue), P (red)
      
      SubstitutedExpression
      
      --base="6"
      --digits="1-5"
      --distinct="ABCDE,FGHIJ,KLMNP"
      --template="(A B C D E) (F G H I J) (K L M N P)"
      --solution=""
      
      # "Dave (D) riding Egg Nog (F) lasted longer than Bill (B) who fell at the 2nd fence"
      "D = F"
      "D > B"
      "2 = B"
      
      # "Big Gun (G) fell at the 3rd"
      "3 = G"
      
      # "mauve (K) lasted the longest"
      "5 = K"
      
      # "Long Gone (H) lasted longer than yellow (L)
      "H > L"
      
      # "Chris's (C's) horse fell one fence later than the horse ridden by green (M)"
      "M + 1 = C"
      
      # "Fred (E) and his friend blue (N) did not fall at adjacent fences"
      "abs(E - N) > 1"
      
      # "Nig Nag (I) was ridden by Wally (A)"
      "I = A"
      
      # "Dragon's (J's) jockey wore red"
      "J = P"
      

      We can wrap this in Python code to format the solution in a more friendly form:

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, group, printf)
      
      # load the run file
      p = SubstitutedExpression.from_file("teaser2685.run")
      
      # map symbols to jockey, horse, colour names
      name = dict(
        A="Wally", B="Bob", C="Chris", D="Dave", E="Fred",
        F="Egg Nog", G="Big Gun", H="Long Gone", I="Nig Nag", J="Dragon",
        K="mauve", L="yellow", M="green", N="blue", P="red",
      )
      
      # solve the puzzle
      for s in p.solve(verbose=0):
        # group symbols by value
        d = group(s.keys(), by=s.get)
        # output solution for each fence
        for k in sorted(d.keys()):
          # extract jockey, horse, colour
          (j, h, c) = (name[s] for s in sorted(d[k]))
          printf("{k}: {j} ({c}) on {h}")
        printf()
      

      Solution: Fred was in yellow, riding Big Gun.

      The full solution is:

      1st fence: Wally (blue) on Nig Nag
      2nd fence: Bob (red) on Dragon
      3rd fence: Fred (yellow) on Big Gun
      4th fence: Dave (green) on Egg Nog
      5th fence: Chris (mauve) on Long Gone

      Like

  • Unknown's avatar

    Jim Randell 7:37 am on 20 September 2022 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2744: The school run 

    From The Sunday Times, 26th April 2015 [link] [link]

    Each of the three houses of Merryhouse School entered four students in the cross-country race. Points were awarded with 12 for the winner, 11 for second, and so on down to 1 for the tail-ender (from Berry House). When the points were added up, all houses had equal points. Three of the runners from Cherry House were in consecutive positions, as were just the two middle-performers from Derry House.

    Which house did the winner come from, and what were the individual scores of its runners?

    [teaser2744]

     
    • Jim Randell's avatar

      Jim Randell 7:38 am on 20 September 2022 Permalink | Reply

      There are tri(12) = 78 points awarded, so each of the three houses gets 26 points.

      We can treat this puzzle as an alphametic problem in base 13, distributing values 1-12 to each of the runners, and use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve it.

      When the puzzle says that 3 from team C and 2 from team D are consecutive, I assumed that this implies that exactly the specified number are consecutive for those teams. (For a looser interpretation we can change the != at line 31 to an or).

      The following run file executes in 67ms. (The internal run time of the generated program is 265µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # suppose points are:
      #
      #       1  2  3  4
      #  B =  E  F  G  H
      #  C =  I  J  K  L
      #  D =  M  N  P  Q
      
      SubstitutedExpression
      
      # allocate points 1-12
      --base=13
      --digits=1-12
      
      # teams are in order
      "E > F > G > H"  # team B
      "I > J > K > L"  # team C
      "M > N > P > Q"  # team D
      
      # team B has the tail-ender
      "H = 1"
      
      # each team had the same number of points (= 26)
      "E + F + G + H == 26"
      "I + J + K + L == 26"
      "M + N + P + Q == 26"
      
      # team C has 3 consecutive points
      "J == K + 1"
      "(I == J + 1) != (K == L + 1)"
      
      # team D has middle 2 consecutive points
      "N == P + 1"
      "M > N + 1"
      "P > Q + 1"
      
      --template=""
      

      Solution: The winner was from Derry House, which was awarded 12 + 6 + 5 + 3 points.

      The points awarded are:

      B: 11 + 10 + 4 + 1 = 26
      C: 9 + 8 + 7 + 2 = 26
      D: 12 + 6 + 5 + 3 = 26

      Like

  • Unknown's avatar

    Jim Randell 4:03 pm on 22 July 2022 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 3122: Bank robbery 

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

    Five witnesses were interviewed following a robbery at the bank in the High Street. Each was asked to give a description of the robber and his actions.

    The details given were: height; hair colour; eye colour; weapon carried; escape method.

    Witness 1: short; fair; brown; cricket bat; motorbike.

    Witness 2: tall; fair; grey; gun; car.

    Witness 3: tall; dark; brown; crowbar; motorbike.

    Witness 4: short; ginger; blue; knife; car.

    Witness 5: tall; dark; blue; stick; pushbike.

    When the police caught up with the perpetrator, they found that each of the five witnesses had been correct in exactly two of these characteristics.

    What was the robber carrying, and how did he get away?

    [teaser3122]

     
    • Jim Randell's avatar

      Jim Randell 4:15 pm on 22 July 2022 Permalink | Reply

      It is straightforward to try all possible combinations. (Assuming the robber has a unique single value for each characteristic).

      I include an “other” value for each characteristic to account for possibilities where none of the witnesses have correctly described it.

      This Python program runs in 58ms. (Internal runtime is 1.9ms).

      Run: [ @replit ]

      from enigma import (cproduct, join, printf)
      
      # descriptions
      descs = [
        dict(S='short', T='tall', X='other'),
        dict(F='fair', D='dark', G='ginger', X='other'),
        dict(R='brown', G='grey', L='blue', X='other'),
        dict(B='cricket bat', G='gun', C='crowbar', K='knife', S='stick', X='other'),
        dict(M='motorbike', C='car', P='pushbike', X='other'),
      ]
      
      # statements
      statements = [ 'SFRBM', 'TFGGC', 'TDRCM', 'SGLKC', 'TDLSP' ]
      
      # how many characteristics are correct?
      correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
      
      # consider characteristics of the perp
      for cs in cproduct(d.keys() for d in descs):
        # each witness gave 2 correct statements
        if all(correct(xs, cs) == 2 for xs in statements):
          # output solution
          printf("{cs}", cs=join((d[k] for (d, k) in zip(descs, cs)), sep=", "))
      

      Solution: The robber was carrying a knife. He made his escape by motorbike.

      In fact we can determine a complete description:

      height = tall
      hair = fair
      eyes = blue
      weapon = knife
      vehicle = motorbike

      Like

      • Jim Randell's avatar

        Jim Randell 8:48 am on 26 July 2022 Permalink | Reply

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

        The following run file executes in 72ms.

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        # statements:
        #
        #  A = height is short; B = height is tall
        #  C = hair is fair; D = hair is dark; E = hair is ginger
        #  F = eyes are brown; G = eyes are grey; H = eyes are blue
        #  I = weapon is cricket bat; J = weapon is gun; K = weapon is crowbar; L = weapon is knife; M = weapon is stick
        #  N = vehicle is motorbike; P = vehicle is car; Q = vehicle is pushbike
        
        SubstitutedExpression
        
        # binary statements
        --base=2
        --distinct=""
        
        # at most one of the statements in each category is true
        "not (A + B > 1)"
        "not (C + D + E > 1)"
        "not (F + G + H > 1)"
        "not (I + J + K + L + M > 1)"
        "not (N + P + Q > 1)"
        
        # witnesses each make exactly 2 true statements
        "A + C + F + I + N == 2"
        "B + C + G + J + P == 2"
        "B + D + F + K + N == 2"
        "A + E + H + L + P == 2"
        "B + D + H + M + Q == 2"
        
        --template=""
        

        This construction also leads to a simple manual solution:

        In the matrix of statements (lines 25 – 29), each row sums to 2. So the total sum of all matrix elements is 10.

        Looking at the columns of the matrix we get the following potential column totals:

        height: 0 (X), 2 (A), 3 (B)
        hair: 0 (X), 1 (E), 2 (C, D)
        eyes: 0 (X), 1 (G), 2 (F, H)
        weapon: 0 (X), 1 (I, J, K, L, M)
        vehicle: 0 (X), 1 (Q), 2 (N, P)

        A grand total of 10 can only be achieved by taking the maximum value for each column.

        So we can eliminate all the X’s and A, E, G, Q (all of which must = 0). Hence B = 1.

        One of C, D = 1 (and the other = 0). If D = 1, then witnesses 3 and 5 have achieved their 2 correct statements so: F, H, K, M, N = 0, but one of F, H = 1. So D = 0 and C = 1.

        We can then complete the assignment of values, and determine the true statements are: B, C, H, L, N.

        Like

    • GeoffR's avatar

      GeoffR 5:51 pm on 23 July 2022 Permalink | Reply

      
      # List of possible witness statements
      statements = []
      
      # Witness statements
      W1 = ['short', 'fair', 'brown', 'cricket bat', 'motorbike']
      W2 = ['tall', 'fair', 'grey', 'gun', 'car' ]
      W3 = ['tall', 'dark', 'brown', 'crowbar', 'motorbike' ]
      W4 = ['short', 'ginger', 'blue', 'knife', 'car' ] 
      W5 = ['tall', 'dark', 'blue', 'stick', 'pushbike' ]
      
      # Form lists of all possible witness statements
      for a in ('short', 'tall'): 
        for b in ('fair', 'dark', 'ginger'):
          for c in ('brown', 'grey', 'blue'):
            for d in ('cricket bat', 'gun', 'crowbar', 'knife', 'stick'):
              for e in ('motorbike', 'car', 'pushbike'):
                statements.append([a, b, c, d, e])
      
      for st in statements:
          a, b, c, d, e = st
          # Two statements from five are true for each witness
          # test Witness No.1 statements
          if sum([a == 'short', b == 'fair', c == 'brown', \
                  d == 'cricket bat', e == 'motorbike']) == 2:
            
            #test Witness No.2 statements
            if sum([a == 'tall', b == 'fair', c == 'grey', \
                    d == 'gun', e == 'car']) == 2:
              
              # test Witness No.3 statements
              if sum([ a == 'tall', b == 'dark', c == 'brown', \
                       d == 'crowbar', e == 'motorbike']) == 2:
              
                # test Witness No.4 statements
                if sum([a == 'short', b == 'ginger', c == 'blue', \
                        d == 'knife', e == 'car']) == 2:
                  
                  # test Witness No.5 statements
                  if sum([ a == 'tall', b == 'dark', c == 'blue', \
                           d == 'stick', e == 'pushbike']) == 2:
                    print(f"The robber was {a}.")
                    print(f"He had {b} coloured hair and {c} colour eyes.")
                    print(f"He carried a {d} as a weapon, escaping on a {e}.")
                    
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 9:41 pm on 23 July 2022 Permalink | Reply

      Indexing the witness statement lists is neater:

      
      # List of possible witness statements
      statements = []
      
      # Witness statements
      W1 = ['short', 'fair', 'brown', 'cricket bat', 'motorbike']
      W2 = ['tall', 'fair', 'grey', 'gun', 'car' ]
      W3 = ['tall', 'dark', 'brown', 'crowbar', 'motorbike' ]
      W4 = ['short', 'ginger', 'blue', 'knife', 'car' ] 
      W5 = ['tall', 'dark', 'blue', 'stick', 'pushbike' ]
      
      # Form lists of all possible witness statements
      for a in ('short', 'tall'): 
        for b in ('fair', 'dark', 'ginger'):
          for c in ('brown', 'grey', 'blue'):
            for d in ('cricket bat', 'gun', 'crowbar', 'knife', 'stick'):
              for e in ('motorbike', 'car', 'pushbike'):
                statements.append([a, b, c, d, e])
      
      for st in statements:
          a, b, c, d, e = st
          # Two statements from five are true for each witness
          # test Witness No.1 statements
          if sum([a == W1[0], b == W1[1], c == W1[2], \
                  d == W1[3], e == W1[4]]) == 2:
            
            # test Witness No.2 statements
            if sum([a == W2[0], b == W2[1], c == W2[2], \
                    d == W2[3], e == W2[4]]) == 2:
              
              # test Witness No.3 statements
              if sum([ a == W3[0], b == W3[1], c == W3[2], \
                       d == W3[3], e == W3[4]]) == 2:
              
                # test Witness No.4 statements
                if sum([a == W4[0], b == W4[1], c == W4[2], \
                        d == W4[3], e == W4[4]]) == 2:
                  
                  # test Witness No.5 statements
                  if sum([ a == W5[0], b == W5[1], c == W5[2], \
                           d == W5[3], e == W5[4]]) == 2:
                    print(f"The robber was {a}.")
                    print(f"He had {b} coloured hair and {c} colour eyes.")
                    print(f"He carried a {d} as a weapon, escaping on a {e}.")
                    
      
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 12:07 pm on 1 August 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: TF = {1,0};
      
      var TF:short; var TF:tall;
      var TF:h_fair; var TF:h_dark; var TF:h_ginger;
      var TF:e_brown; var TF:e_grey; var TF:e_blue;
      var TF:c_bat; var TF:gun; var TF:c_bar; var TF:knife; var TF:stick;
      var TF:mbike; var TF:car; var TF:pbike;
      
      % Values are 0 or 1 for main variables 
      % ..i.e. for height, hair colour, eye colour, weapon, vehicle
      constraint sum([short, tall]) < 2;
      constraint sum([h_fair, h_dark, h_ginger]) < 2;
      constraint sum([e_brown, e_grey, e_blue]) < 2;
      constraint sum([c_bat, gun, c_bar, knife, stick]) < 2;
      constraint sum([mbike, car, pbike]) < 2;
      
      % 5 witness statements - 2 are true for each witness
      constraint sum([short, h_fair, e_brown,c_bat, mbike]) == 2;
      constraint sum([tall, h_fair, e_grey, gun, car]) == 2;
      constraint sum([tall, h_dark, e_brown, c_bar, mbike]) == 2;
      constraint sum([short, h_ginger, e_blue, knife, car]) == 2;
      constraint sum([tall, h_dark, e_blue, stick, pbike]) ==  2;
      
      solve satisfy;
      
      output [" [short, tall ] = " ++ show([ short, tall ]) ++ "\n"
      ++ " [h_fair, h_dark, h_ginger] = " ++ show([ h_fair, h_dark, h_ginger])  
      ++ "\n" ++ " [e_brown, e_grey, e_blue] = " ++ show([e_brown, e_grey, e_blue] )
      ++ "\n" ++ " [c_bat, gun, c_bar, knife, stick] = " 
      ++ show([c_bat, gun, c_bar, knife, stick]) 
      ++ "\n" ++ " [mbike, car, pbike] = "  ++ show([mbike, car, pbike]) ];
      
      %  [short, tall ] = [0, 1]
      %  [h_fair, h_dark, h_ginger] = [1, 0, 0]
      %  [e_brown, e_grey, e_blue] = [0, 0, 1]
      %  [c_bat, gun, c_bar, knife, stick] = [0, 0, 0, 1, 0]
      %  [mbike, car, pbike] = [1, 0, 0]
      % ----------
      % ==========
      % i.e. Robber was tall, had fair hair, blue eyes, with a knife, escaping on a motorbike.
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:11 am on 26 May 2022 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2858: Beach game 

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

    Ken, Leanne, Mike, Nancy, Olive and Paul were playing on the beach. They had drawn a large circle in the sand and written their names clockwise, in that order, equally spaced around the edge of the circle. They also had a circular piece of card around which they had written the numbers 1 to 6 clockwise in order, also equally spaced. Then they spun the card in the middle of the sand circle and each child was awarded the number of points equal to the number closest to their name. They kept repeating this process and after each spin they kept a total of their scores so far. Mike was ahead after the first spin and after each of the first five spins there was a different clear leader. Then the tide came in and washed the game away.

    Which child was never in the lead, and what was that child’s total after the five spins?

    [teaser2858]

     
    • Jim Randell's avatar

      Jim Randell 9:12 am on 26 May 2022 Permalink | Reply

      This Python program runs in 58ms.

      Run: [ @replit ]

      from enigma import (irange, rotate, Record, map2str, printf)
      
      # possible scores
      scores = [1, 2, 3, 4, 5, 6]
      
      # play <k> rounds of the game
      # ss = scores
      # ls = leaders
      def play(k, ss, ls):
        if k == 0:
          yield (ss, ls)
        else:
          # choose a rotation
          for i in irange(0, 5):
            # calculate the new totals
            s = list(a + b for (a, b) in zip(ss[-1], rotate(scores, i)))
            # do we have a new clear leader?
            t = list(Record(pos=i, score=x) for (i, x) in enumerate(s))
            t.sort(key=(lambda x: x.score), reverse=1)
            if not (t[0].score > t[1].score and t[0].pos not in ls): continue
            # play the next game
            yield from play(k - 1, ss + [s], ls + [t[0].pos])
      
      # M (pos 2) won the first game, so gets 6 points
      s = rotate(scores, 3)
      assert s[2] == 6
      
      # play 4 more rounds of the game
      for (ss, ls) in play(4, [s], [2]):
        # who was never in the lead?
        for n in irange(0, 5):
          if n in ls: continue
          # output solution
          name = "KLMNOP"
          for (i, s) in enumerate(ss):
            printf("{r}: {s}; leader={l}", r=i + 1, s=map2str(zip(name, s)), l=name[ls[i]])
          printf("never={name}, score={score}", name=name[n], score=ss[-1][n])
          printf()
      

      Solution: Nancy was never in the lead. After five spins she had 15 points.

      Like

  • Unknown's avatar

    Jim Randell 8:57 am on 3 March 2022 Permalink | Reply
    Tags: by: Angela Newing   

    Teaser 2874: An age-old progression 

    From The Sunday Times, 22nd October 2017 [link] [link]

    It is my birthday today — the same day as two of my younger relatives, Betty and Charles. I commented that the digits involved in our three ages are all different. Betty noted that the square of her age is equal to my age multiplied by Charles’s age. Then Charles added that on one of our birthdays in the next ten years the sum of our ages will be one hundred.

    What are our three ages today?

    [teaser2874]

     
    • Jim Randell's avatar

      Jim Randell 8:58 am on 3 March 2022 Permalink | Reply

      We have:

      A > B, C
      B² = A⋅C (i.e. B is the geometric mean of A and C)
      A, B, C digits are all different
      A + B + C + 3n = 100, for some n = 1 .. 10

      We can get an upper bound on B by setting A = B = C:

      A + B + C ≤ 97
      3B ≤ 97
      B ≤ 32

      This Python program finds possible values for A, B, C. It runs in 47ms.

      Run: [ @replit ]

      from enigma import (irange, divisors_pairs, is_duplicate, div, printf)
      
      # consider values of B
      for B in irange(1, 32):
      
        # B^2 = A.C; C < A
        for (C, A) in divisors_pairs(B, 2):
          if not (C < B < A): continue
      
          # calculate n
          n = div(100 - (A + B + C), 3)
          if n is None or n < 1 or n > 10: continue
      
          # check for repeated digits
          if is_duplicate(A, B, C): continue
      
          # output solution
          printf("A={A} B={B} C={C}; n={n}")
      

      The program finds 2 possible solutions:

      B=8; C=1 A=64; n=9
      B=21; C=7 A=63; n=3

      The second of these seems the most likely as Charles joins in the conversation. (Although it would have been nice if the first case had been ruled out explicitly by the puzzle text).

      Solution: Angela is 63. Betty is 21. Charles is 7.

      We have 21² = 441 = 7 × 63 as required.

      And in 3 years time: (63 + 3) + (21 + 3) + (7 + 3) = 100.

      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