Updates from July, 2025 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:25 am on 8 July 2025 Permalink | Reply
    Tags: by: Bert Brooke   

    Brainteaser 1031: Definitive darts 

    From The Sunday Times, 2nd May 1982 [link]

    We of the Private Bar of the Wapping Boar are an exclusive elite in darting fraternity; albeit we play the conventional darts of commoner folk and employ their customary board and rules. Our two opposing players throw three darts each, in turn. We require that the last (winning) dart of any game must score some double or the bull (50) — and must thereby raise the player’s score precisely to 501.

    Last evening, two of our virtuosi played an epic exhibition; a series of games were all won by the first-to-throw and each game, required the least possible number of darts. The precision was faultless and no dart thrown by the eventual winner of any game ever exceeded the score of any preceding dart in that game. Later, our President eulogised and vouched that in the suite but recently enjoyed, every possible scoring sequence of winning games (within our definition) had been played just once.

    “What about my record?” interceded that parvenu from the “public” (one we import to manage the chalks and call the 3 dart total following each throw). The low fellow claims a maximum possible of top score shouts “One hundred and eighty!!” in a demonstration match such as we’d just had; he insists his feat be recognised. The oaf must have his due if we are to use him anew, but there’s the rub — who scores for a scorer?

    Readers, please say the maximum of correct “180” shouts to be credited in our circumstance.

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

    [teaser1031]

     
    • Jim Randell's avatar

      Jim Randell 8:25 am on 8 July 2025 Permalink | Reply

      It is not possible to score 501 in fewer than 9 darts, as 60 is the maximum possible score with a single dart, and 501 / 60 > 8.

      But it is possible to score 501 in 9 darts, as 501 = 3 × 167, and 167 = 60 (treble 20) + 57 (treble 19) + 50 (bull). So we can finish the 9 dart run on a bull.

      We need to find sequences of 9 darts, with scores in non-increasing order, that score a total of 501, and finish on a double.

      And this corresponds to the winners throws. The winner went first, so the loser in each match only threw 6 darts, and as the scorer shouted “180” the maximum possible number of times, the loser must have thrown 6×60 for 2 shouts of “180”.

      This Python 3 program runs in 67ms. (Internal runtime is 1.3ms).

      Run: [ @codepad ]

      from enigma import (irange, union, seq_items, chunk, printf)
      
      # possible scores with 3 darts
      single = union([irange(1, 20), [25]])
      double = set(2 * x for x in single)
      treble = set(3 * x for x in single if x != 25)
      darts = sorted(union([single, double, treble]), reverse=1)
      
      # score <t> in <k> darts (finishing with a double)
      # using non-increasing subsets of darts[i..]
      def score(t, k, i=0, ss=[]):
        x = darts[i]
        # finish on a double
        if k == 1:
          if t in double and not (x < t):
            yield ss + [t]
        elif not (x * k < t):
          # score the next dart
          for (j, x) in seq_items(darts, i):
            if x < t:
              yield from score(t - x, k - 1, j, ss + [x])
      
      # score 501 in 9 darts, count the number of matches, and "180" shouts
      n = s = 0
      for ss in score(501, 9):
        n += 1
        gs = list(chunk(ss, 3))
        x = sum(sum(g) == 180 for g in gs)
        s += x
        printf("[{n}: {gs}; {x} shouts]")
      
      # total number of shouts
      t = s + 2 * n
      printf("total shouts = {t} [from {n} matches]")
      

      Solution: There were 62 shouts of “180”.

      There were 18 matches. The winners scores as follows:

      1: [(60, 60, 60), (60, 60, 60), (60, 57, 24)]; 2 shouts
      2: [(60, 60, 60), (60, 60, 60), (60, 51, 30)]; 2 shouts
      3: [(60, 60, 60), (60, 60, 60), (60, 45, 36)]; 2 shouts
      4: [(60, 60, 60), (60, 60, 60), (57, 54, 30)]; 2 shouts
      5: [(60, 60, 60), (60, 60, 60), (57, 50, 34)]; 2 shouts
      6: [(60, 60, 60), (60, 60, 60), (57, 48, 36)]; 2 shouts
      7: [(60, 60, 60), (60, 60, 60), (54, 51, 36)]; 2 shouts
      8: [(60, 60, 60), (60, 60, 60), (51, 50, 40)]; 2 shouts
      9: [(60, 60, 60), (60, 60, 57), (57, 57, 30)]; 1 shout
      10: [(60, 60, 60), (60, 60, 57), (57, 51, 36)]; 1 shout
      11: [(60, 60, 60), (60, 60, 57), (54, 54, 36)]; 1 shout
      12: [(60, 60, 60), (60, 60, 57), (54, 50, 40)]; 1 shout
      13: [(60, 60, 60), (60, 60, 51), (50, 50, 50)]; 1 shout
      14: [(60, 60, 60), (60, 57, 57), (57, 54, 36)]; 1 shout
      15: [(60, 60, 60), (60, 57, 57), (57, 50, 40)]; 1 shout
      16: [(60, 60, 60), (60, 57, 54), (50, 50, 50)]; 1 shout
      17: [(60, 60, 60), (57, 57, 57), (57, 57, 36)]; 1 shout
      18: [(60, 60, 60), (57, 57, 57), (50, 50, 50)]; 1 shout

      This accounts for 26 of the shouts.

      The loser in each match accounts for another 2 shouts per match, giving 26 + 18×2 = 62 shouts in total.

      Like

      • Frits's avatar

        Frits 12:12 pm on 10 July 2025 Permalink | Reply

        @Jim, as “darts” is non-increasing the check in line 20 can be done outside the loop (if needed at all as x always seems to be less than t).

        Like

        • Jim Randell's avatar

          Jim Randell 12:36 pm on 11 July 2025 Permalink | Reply

          @Frits: Yes, we could skip any elements that are not less than the remaining total, and then just process all the remaining elements without testing (as they are in decreasing order). But I didn’t think it was worth the additional complication.

          But we do need the test, for example, if we wanted to find a 2 dart finish from 37 we would call [[ score(37, 2) ]].

          Like

    • Frits's avatar

      Frits 6:08 pm on 8 July 2025 Permalink | Reply

      N = 9 # number of throws needed
      
      # return a valid loop structure string, indent after every "for" statement
      def indent(s):
        res, ind = "", 0
        for ln in s:
          res += " " * ind + ln + "\n"
          if len(ln) > 2 and ln[:3] == "for":
            ind += 2
        return res  
      
      syms = "ABCDEFGHIJKLMNOPQR"
      throws = ["X"] + list(syms[:N])
      varstring = "[" + ', '.join(throws[1:]) + "]"
      # possible scores with 3 darts
      single = set(range(1, 21)) | {25}
      double = {2 * x for x in single}
      treble = {3 * x for x in single if x != 25}
      
      # determine mininum for the first 8 throws
      mn = 501 - (7 * max(treble) + max(double))
      # darts to be used for the first 8 throws
      darts = sorted([x for x in single | double | treble if x >= mn], reverse=True)
      
      # generate an execution string with 8 loops
      exprs = [f"X = {max(treble)}; cnt = 0"]           # X is a dummy variable
      for i, t in enumerate(throws[1:], 1):
        if i < N:
          exprs.append(f"for {t} in [x for x in {darts} if x <= {throws[i - 1]}]:") 
          if i < N - 1:
            exprs.append(f"if sum([{', '.join(throws[1:i + 1])}]) + "
                         f"{N - i - 1} * {throws[i]} + {max(double)} < 501: break")
        else:
          # most inner loop
          exprs.append(f"{throws[N]} = 501 - sum([{', '.join(throws[1:-1])}])")   
          exprs.append(f"if {throws[N]} > {throws[N - 1]}: break")    
          exprs.append(f"if not ({throws[N]} in {double}): continue")   
          # the loser in each game accounts for another 2 shouts per game
          exprs.append(f"cnt += sum({varstring}[3 * i:3 * (i + 1)] == "
                       f"[max(treble)] * 3 for i in range(3)) + 2")
          #exprs.append(f"print({varstring})")
             
      exprs = indent(exprs)
      exprs += f"print('answer:', cnt)"
      #print(exprs)
      exec(exprs)
      

      The program generates (and executes) the following code:

      X = 60; cnt = 0
      for A in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= X]:
        if sum([A]) + 7 * A + 50 < 501: break
        for B in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= A]:
          if sum([A, B]) + 6 * B + 50 < 501: break
          for C in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= B]:
            if sum([A, B, C]) + 5 * C + 50 < 501: break
            for D in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= C]:
              if sum([A, B, C, D]) + 4 * D + 50 < 501: break
              for E in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= D]:
                if sum([A, B, C, D, E]) + 3 * E + 50 < 501: break
                for F in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= E]:
                  if sum([A, B, C, D, E, F]) + 2 * F + 50 < 501: break
                  for G in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= F]:
                    if sum([A, B, C, D, E, F, G]) + 1 * G + 50 < 501: break
                    for H in [x for x in [60, 57, 54, 51, 50, 48, 45, 42, 40, 39, 38, 36, 34, 33, 32] if x <= G]:
                      I = 501 - sum([A, B, C, D, E, F, G, H])
                      if I > H: break
                      if not (I in {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 50}): continue
                      cnt += sum([A, B, C, D, E, F, G, H, I][3 * i:3 * (i + 1)] == [max(treble)] * 3 for i in range(3)) + 2
      print('answer:', cnt)
      

      Like

  • Unknown's avatar

    Jim Randell 8:42 am on 1 July 2025 Permalink | Reply
    Tags:   

    Brain teaser 1029: Staff factors 

    From The Sunday Times, 18th April 1982 [link]

    The Manager of a large company greeted his twelve new computer staff. “You have been given consecutive, five-digit, Staff Reference Numbers (SRN). I remember numbers by finding their prime factors, using mental arithmetic — pre-computer vintage. Why not try it? If you would each tell me what factors you have, without specifying them (and ignoring unity), I should be able to work out your SR numbers”.

    John said: “My number is prime.”

    Ted said ” I have two prime factors. Your number follows mine doesn’t it, Les?”

    Ian said: “I also have two, one of which squared. Alan’s just before me on the list.”

    Sam said: “One of mine is to the power four. The last two digits of my SRN give me the other prime factor.”

    Pete said: “I’ve got one factor to the power four as well. The other one is my year of birth.”

    Brian said: “My number has one prime factor cubed and two others, both squared.”

    Chris said: “I’m the only one with four factors, one of which is squared. Fred’s number is one less than mine.”

    Dave started to say: “Kevin’s SRN is the one after mine, which …” when the Manager interrupted. “I can now list all twelve!”

    List the twelve people, by initials, in increasing order of SRNs. What is Sam’s SRN?

    This was the final puzzle to go by the title “Brain teaser“. The next puzzle was “Brainteaser 1030“.

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

    [teaser1029]

     
    • Jim Randell's avatar

      Jim Randell 8:43 am on 1 July 2025 Permalink | Reply

      Instead of trying all possible 5-digit SRNs we can reduce the number of candidates considered by only considering numbers in the neighbourhood of P, as there are only a few numbers that are the 4th power of a prime multiplied by a viable prime birth year. (I considered primes between 1912 and 1982 as viable birth years).

      This Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library, to solve the puzzle given a candidate start number.

      It runs in 308ms (although I haven’t looked at optimising the evaluation order of the expressions).

      from enigma import (SubstitutedExpression, irange, primes, cache, sprintf as f, join, printf)
      
      # prime factors of n -> ((p1, e1), (p2, e2), ...)
      factors = cache(lambda n: tuple(primes.prime_factor(n)))
      
      # I has exactly 2 prime factors; one is a power of 2
      def checkI(n):
        ((p1, e1), (p2, e2)) = factors(n)
        return 2 in {e1, e2}
      
      # S has exactly 2 prime factors; one is a power of 4, and the other is the last 2 digits of S
      def checkS(n):
        ((p1, e1), (p2, e2)) = factors(n)
        return (4, n % 100) in {(e1, p2), (e2, p1)}
      
      # P has exactly 2 prime factors; one is a power of 4, and the other is his year of birth
      def checkP(n):
        ((p1, e1), (p2, e2)) = factors(n)
        return (e1 == 4 and 1911 < p2 < 1983) or (e2 == 4 and 1911 < p1 < 1983)
      
      # B has exactly 3 prime factors; a power of 3 and two powers of 2
      def checkB(n):
        ((p1, e1), (p2, e2), (p3, e3)) = factors(n)
        return sorted([e1, e2, e3]) == [2, 2, 3]
      
      # C has exactly 4 prime factors; one is a power of 2
      def checkC(n):
        ((p1, e1), (p2, e2), (p3, e3), (p3, e4)) = factors(n)
        return 2 in {e1, e2, e3, e4}
      
      def solve(start, s2d=dict()):
        # expressions to evaluate
        exprs = [
          # J is prime
          "is_prime(J + start)",
          # T has exactly 2 prime factors
          "len(factors(T + start)) == 2",
          # all the others (except C) have < 4 prime factors
          "all(len(factors(x + start)) < 4 for x in [A, D, F, K, L])",
          # adjacent values
          "T + 1 = L",
          "I - 1 = A",
          "C - 1 = F",
          "D + 1 = K",
          # checks on the remaining values
          "checkI({I} + start)",
          "checkS({S} + start)",
          "checkP({P} + start)",
          "checkB({B} + start)",
          "checkC({C} + start)", 
        ]
      
        # environment
        env = dict(
          start=start,
          factors=factors, is_prime=primes.is_prime,
          checkI=checkI, checkS=checkS, checkP=checkP, checkB=checkB, checkC=checkC,
        )
      
        # construct the puzzle
        p = SubstitutedExpression(exprs, base=12, s2d=s2d, env=env)
      
        # solve the puzzle for the offsets
        for s in p.solve(verbose=''):
          # fill out the actual numbers from the offsets
          s = dict((k, v + start) for (k, v) in s.items())
          # output the numbers in order
          fmt = (lambda fs: join((f("{p}" if e == 1 else "{p}^{e}") for (p, e) in fs), sep=", "))
          for k in sorted(s.keys(), key=s.get):
            n = s[k]
            printf("{k} = {n} [{fs}]", fs=fmt(factors(n)))
          printf()
      
      # consider possible prime birth years for P
      for y in primes.between(1912, 1982):
        # consider other prime factor, raised to the 4th power
        for p in primes:
          P = p**4 * y
          if P > 99999: break
          # consider possible start numbers
          for start in irange(max(10000, P - 11), min(99988, P)):
            solve(start, dict(P=P - start))
      

      Solution: The order is: D K A I B S T L P F C J. Sam’s SRN is 31213.

      The SRNs [along with their prime factors] are given below:

      D = 31208 [2^3, 47, 83]
      K = 31209 [3, 101, 103]
      A = 31210 [2, 5, 3121]
      I = 31211 [23^2, 59]
      B = 31212 [2^2, 3^3, 17^2]
      S = 31213 [7^4, 13]
      T = 31214 [2, 15607]
      L = 31215 [3, 5, 2081]
      P = 31216 [2^4, 1951]
      F = 31217 [19, 31, 53]
      C = 31218 [2, 3, 11^2, 43]
      J = 31219 [31219]

      So we see P was born in 1951, which is 31 years before the puzzle was set.


      With a bit of work, we can further reduce the number of cases considered significantly.

      There are only 7 candidate values for P, and there are only 11 candidate values for S. And there is only a single pair where P and S are close enough together to give a solution. This also gives a way to tackle puzzle manually.

      Like

      • Frits's avatar

        Frits 1:44 pm on 1 July 2025 Permalink | Reply

        @Jim, is there a typo in “which is 42 years before the puzzle was set”?

        Like

      • Frits's avatar

        Frits 7:19 pm on 1 July 2025 Permalink | Reply

        from enigma import is_prime, prime_factor as pfactor, ceil, subsets
        
        # possible SRN's for people in list <s>
        SRNs = lambda s: {i for i in range(s[0] - 11, s[0] + 12) 
                          if i not in s and all(abs(x - i) < 12 for x in s)}
                          
        # mandatory SRN's for people in list <s>
        m_SRNs = lambda s: {i for i in range(min(s) + 1, max(s)) if i not in s}                  
        
        mn = 1912 # minimum birth date
        
        # set of prime numbers up to 99
        P = {3, 5, 7}
        P |= {2} | {x for x in range(11, 100, 2) if all(x % p for p in P)}
        sP = sorted(P)
        
        # check last item in sequence <s>
        def check(s, ln, vs, fs4={}):
          if fs4:
            # mandatory missing numbers plus last item  may not have 4 factors
            if not (m_SRNs(s) | {s[-1]}).isdisjoint(fs4): return False
          
          # check length and exponents
          if len(pfs := list(pfactor(s[-1]))) != ln or {y for x, y in pfs} != vs:
            return False
          return True  
        
        # start with P ([p1^4, 19xx])
        for p1 in sP:
          if p1**4 * mn > 99999: break
          ns = [n for y in range(mn, 1965) if 9999 < (n := p1**4 * y) <= 99999]
          mn, mx = ns[0] - 11 , ns[-1] + 11
          # check S ([s1^4, s2 with s2 last 2 digits])
          # S = xxxxab and S = ab.s1^4,  100 * xxxx = ab.(s1^4 - 1) so s1^4 = ...1
          for s1 in sP[1:]:
            if (pow4 := s1**4) * 11 > 99999: break
            if pow4 % 10 != 1 or mx / pow4 > 99: continue
            # calculate possible numbers for S
            for s in [s for ab in range(ceil(mn / pow4), mx // pow4 + 1) 
                      if ab in P and (s := ab * pow4) % 100 == ab]:
              # only C may have exactly 4 factors
              fs4 = {x for x in SRNs([s]) if len(list(pfactor(x))) == 4}
              for j in [j for j in SRNs([s]) if is_prime(j)]:
                for c in SRNs([s, j]):
                  if not check([s, j, c], 4, {1, 2}): continue
                  for b in SRNs([s, j, c]):
                    if not check([s, j, c, b], 3, {2, 3}, fs4): continue
                    for i in SRNs([s, j, c, b]):
                      if not check([s, j, c, b, i], 2, {1, 2}, fs4): continue
                      for t in SRNs([s, j, c, b, i]):
                        if not check([s, j, c, b, i, t], 2, {1}, fs4): continue
                        for p in SRNs([s, j, c, b, i, t]):
                          if not check([s, j, c, b, i, t, p], 2, {1, 4}, fs4): continue
                          # before I is A, after T is L, before C is F
                          a, l, f = i - 1, t + 1, c - 1
                          n10 = sorted([s, j, c, b, i, t, p, a, l, f])
                          if len(set(n10)) != 10: continue
                          # K after D, find places to insert (D, K)
                          for d, k in subsets(SRNs(n10), 2):
                            if k != d + 1: continue
                            if not {d, k}.isdisjoint(fs4): continue
                            # 12 consecutive numbers
                            if ((n12 := sorted(n10 + [d, k])) == 
                                 list(range(n12[0], n12[0] + 12))):
                              # final check   
                              if not (set(n12) - {c}).isdisjoint(fs4): continue
                              map = dict(list(zip([s, j, c, b, i, t, p, a, l, f, d, k],
                                            "SJCBITPALFDK")))
                              for k, v in sorted(map.items()):
                                print(f"{v}: {k} = "
                                      f"{' * '.join(['^'.join(str(y) for y in x)
                                                     for x in pfactor(k)])}")
        

        Like

  • Unknown's avatar

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

    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 6:28 pm on 14 July 2024 Permalink | Reply
    Tags: by: E. R. J. Benson   

    Brainteaser 1081: Trifling troubles 

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

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

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

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

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

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

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

    [teaser1081]

     
    • Jim Randell's avatar

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

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

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

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

      Run: [ @replit ]

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

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

      Which gives 3 piles:

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

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

      Like

      • Ruud's avatar

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

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

        Like

        • Jim Randell's avatar

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

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

          Like

      • Ruud's avatar

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

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

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

        Like

        • Frits's avatar

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

          @Ruud,

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

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

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

          Like

          • Frits's avatar

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

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

            Like

    • Frits's avatar

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

      Runs within a second for up to 21 saucepans.

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

      Like

      • Ruud's avatar

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

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

        Like

      • Frits's avatar

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

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

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

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

        Like

  • Unknown's avatar

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

    Brain-Teaser 959: A question of age 

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

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

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

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

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

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

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

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

    [teaser959]

     
    • Jim Randell's avatar

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

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

      It runs in 156ms.

      Run: [ @replit ]

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

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

      Corresponding to:

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

      And the given facts are:

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

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

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

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

      Like

    • Lise Andreasen's avatar

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

      h - i denotes h is the father of i.

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

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

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

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

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

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

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

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

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

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

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

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

      As (4):
      k = 12

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

      So it has to be:

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

      Like

    • Frits's avatar

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

      Python program to describe a manual solution.

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

      Like

  • Unknown's avatar

    Jim Randell 10:16 am on 14 May 2024 Permalink | Reply
    Tags:   

    Brain-Teaser 950: Magic moments 

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

    Joe decided to utilise the garden see-saw to demonstrate the principles of balance, and of moments to, his family. Alf, Bert, Charlie, Doreen, Elsie and Fiona weighed 100, 80, 70, 60, 50 and 20 kilos respectively. The seesaw had three seats each side, positioned 1, 2 and 3 metres from the centre.

    They discovered many ways of balancing using some or all of the family.

    In one particular combination, with at most one person on each seat, one of the family not on the seesaw observed that the sum of the moments of all of the people on the seesaw was a perfect square.

    “That’s right”, said Joe, “but, as you can see, it doesn’t balance — and what’s more, there’s nowhere you can sit to make it balance”.

    “Wrong”, was the reply. “I could sit on someone’s lap”.

    “True,” said Joe, “but only if you sit at the end”.

    Which two would then be sitting together, and who else, if anyone, would be on the same side of the see-saw?

    [To find the moment due to each person, multiply their distance from the centre by their weight. The sum of the moments on one side should equal the sum of those on the other if balance is to be achieved].

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

    [teaser950]

     
    • Jim Randell's avatar

      Jim Randell 10:16 am on 14 May 2024 Permalink | Reply

      Participants can be uniquely identified by their weights.

      This Python program finds possible seating arrangements on the see-saw.

      It runs in 72ms. (Internal runtime is 13ms).

      Run: [ @replit ]

      from enigma import (subsets, is_square, div, printf)
      
      # weights and positions
      weights = [100, 80, 70, 60, 50, 20]
      poss = [+1, +2, +3, -1, -2, -3]
      
      # there is at least one member not seated
      for ws in subsets(weights, min_size=2, max_size=len(weights) - 1, select='C'):
        # allocate positions (including +3)
        for ps in subsets(poss, size=len(ws), select='P'):
          if +3 not in ps: continue
          # find the total sum of the moments
          ms = sum(w * abs(p) for (w, p) in zip(ws, ps))
          if not is_square(ms): continue
          # weight required at +3 to balance the see-saw
          x = -sum(w * p for (w, p) in zip(ws, ps))
          w = div(x, +3)
          # is it the weight of a missing person?
          if not (w in weights and w not in ws): continue
          # output solution
          printf("ws={ws} ps={ps} ms={ms} x={x} w={w}")
      

      Solution: Doreen would join Fiona at the end of the see-saw. Elsie is also seated on the same side.

      The seating arrangement is as follows:

      Without D the moments are (kg.m):

      L = 70×3 + 80×1 = 290
      R = 20×3 + 50×1 = 110

      They don’t balance, but they do sum to 400 = 20^2.

      With D the moments on the right become:

      R = (20 + 60)×3 + 50×1 = 290

      and the see-saw balances.

      Like

    • Frits's avatar

      Frits 2:26 pm on 14 May 2024 Permalink | Reply

        
      from enigma import express
      from itertools import product
       
      # weights and positions
      weights = [100, 80, 70, 60, 50, 20]
      names = ["Alf", "Bert", "Charlie", "Doreen", "Elsie", "Fiona"]
      
      # possible squares divisible by 10 
      for sq in [100, 400, 900]:
        # decompose square into weights
        for e in express(sq, weights, min_q=0, max_q=3):
          # someone is sitting at the end and one place is empty
          if all(x for x in e) or 3 not in e: continue
          # at most one person on each seat
          if any(e.count(i) > 2 for i in range(1, 4)): continue
         
          # one of the family not on the seesaw to sit at the end on someone's lap
          for i, seat in enumerate(e):
            if seat: continue # not an empty seat
            e1 = e[:i] + [3] + e[i + 1:] 
            
            # multiply elements by 1 or -1 times the weight and 
            # see if they sum to zero
            for prod in product([-1, 1], repeat=len([x for x in e1 if x])):
              if prod[0] > 0: continue    # avoid symmetric duplicates
              # weave in zeroes
              p = list(prod)
              p = [p.pop(0) if x else 0 for x in e1]
              
              # is the seesaw in balance?
              if sum([x * y * weights[i] 
                     for i, (x, y) in enumerate(zip(p, e1))]) != 0: continue
              # new persion must be sitting on someone's lap at the end
              someone = [j for j in range(6) if j != i and e1[j] == 3 and 
                                                p[j] == p[i]]
              if not someone: continue
              print(f"{names[i]} would join {names[someone[0]]}",
                    f"at the end ofthe see-saw.")
              oths = [j for j in range(6) if p[j] == p[i] and 
                                             j not in {i, someone[0]}]
              if not oths: continue
              ns = [names[o] for o in oths]
              oths = ns[0] + " is" if len(oths) == 1 else ' and '.join(ns) + " are"
              print(f"{oths} also seated on the same side.")   
      

      Like

  • Unknown's avatar

    Jim Randell 6:04 pm on 31 March 2024 Permalink | Reply
    Tags: by: Sir John Cowley   

    Brainteaser 1092: If a man and a half… 

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

    I wanted some small alterations done, to my house, so I asked Tom Smith, the local builder, to come and see me.

    I told Tom exactly what the job was and he said he could do it alone, but if he brought his brother Dick (also a trained builder), and his son Harry (an apprentice), the three of them could do the job in half the time that he; Tom, would take if he worked alone.

    Dick could also do the job by himself, but it would take him a day (eight hours) longer than the three of them working together. Young Harry working alone would take six days more than the three of them.

    I wanted the job done quickly, but as there was no room for all three of them to work together, and it was not necessary to employ two trained builders, I decided to employ either Tom and Harry working together, or Dick and Harry working together.

    Exactly how much longer would the second pair take to finish the job than the first pair?

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

    [teaser1092]

     
    • Jim Randell's avatar

      Jim Randell 6:05 pm on 31 March 2024 Permalink | Reply

      Suppose Tom, Dick, Harry can do T, D, H amounts of work per day.

      If we suppose the job consists of 1 unit of work, which all 3 can complete in d days:

      1 = (T + D + H) d

      Tom would take twice as long to do it alone:

      1 = T 2d

      T = 1/(2d)

      Dick would take 1 day longer than all three:

      1 = D (d + 1)

      D = 1/(d + 1)

      And Harry alone would take 6 days longer than all three:

      1 = H (d + 6)

      H = 1/(d + 6)

      And so:

      1/d = T + D + H
      = 1/(2d) + 1/(d + 1) + 1/(d + 6)
      = [(d + 1)(d + 6) + 2d(d + 6) + 2d(d + 1)] / 2d(d + 1)(d + 6)

      2(d + 1)(d + 6) = (d + 1)(d + 6) + (2d² + 12d) + (2d² + 2d)
      d² + 7d + 6 = 4d² + 14d
      3d² + 7d − 6 = 0
      (3d − 2)(d + 3) = 0
      d = 2/3 (= 5h 20m)

      and:

      T = 3/4
      D = 3/5
      H = 3/20

      So the time taken for Tom and Harry (= t1) is:

      t1 = 1 / (T + H)
      T + H = 3/4 + 3/20 = 9/10
      t1 = 10/9

      And the time taken for Dick and Harry (= t2) is:

      t2 = 1 / (D + H)
      D + H = 3/5 + 3/20 = 3/4
      t2 = 4/3

      And the difference:

      t2 − t1 = 4/3 − 10/9 = 2/9 (days)

      Solution: It would take exactly 1 hour, 46 minutes, 40 seconds longer.

      Liked by 1 person

  • Unknown's avatar

    Jim Randell 8:28 am on 17 March 2024 Permalink | Reply
    Tags:   

    Brain-Teaser 948: Journey north-east 

    From The Sunday Times, 21st September 1980 [link]

    For the purpose of my problem I have to choose two towns 26 miles apart. I might have chosen Oxford and Newbury, but it would be more appropriate for me as a Scotsman to go much farther north and choose Kingussie and Grantown-on-Spey, where the roads are somewhat less busy.

    Alf, Bert and Charles start off at the same time from Kingussie to make their way north-eastwards to Grantown-on-Spey, 26 miles distant.

    Alf walks at a constant speed of four miles per hour. Bert and Charles drive together in a car. After a certain time, Bert leaves the car, and walks forward at the same rate as Alf, while Charles drives back to meet Alf.

    Alf gets Into the car with Charles, and they continue to drive to Grantown-on-Spey, arriving there just as Bert does.

    On each stretch Charles averages 40 miles per hour.

    What is the time (in minutes) taken for them all to travel from Kingussie to Grantown-on-Spey?

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

    [teaser948]

     
    • Jim Randell's avatar

      Jim Randell 8:29 am on 17 March 2024 Permalink | Reply

      See: Teaser 3140, where we determined:

      If there are k pedestrians to be transported a distance d, and each walks a distance x at velocity w and is transported a distance (d − x) at velocity v, and the total time taken is t, then we have:

      n = 2k − 1
      x = dw(n − 1) / (v + wn)
      t = d(w + vn) / v(v + wn)

      We can plug the numbers for this puzzle in and calculate the result:

      Run: [ @replit ]

      from enigma import (fdiv, printf)
      
      # initial conditions
      k = 2   # number of walkers
      d = 26  # distance
      w = 4   # walking speed
      v = 40  # driving speed
      
      # calculate walking distance per person
      n = 2 * k - 1
      x = fdiv(d * w * (n - 1), v + w * n)
      
      # calculate time taken
      t = fdiv(x, w) + fdiv(d - x, v)
      
      # output solution
      printf("t = {t:g} hours (= {m:g} min) [x={x:g} miles]", m=t * 60)
      

      Solution: The total time taken is 93 minutes.

      Alf walks the first 4 miles (in 60 minutes), and is driven the remaining 22 miles (in 33 minutes).

      Bert is driven 22 miles first, and walks the last 4 miles.

      Charles drives 22 miles to drop off Bert, returns 18 miles to collect Alf, and then 22 miles to the destination, a total of 62 miles (in 93 minutes).

      So each arrives at the destination after 93 minutes.

      Like

    • Lise Andreasen's avatar

      Lise Andreasen 4:22 pm on 4 April 2024 Permalink | Reply

      Alf walks x miles. For symmetry reasons, Bert also walks x miles. In the middle we have y miles. 26 = x + y + x.
      They all spend the same amount of time.
      x/4 + (x+y)/40 = (2x+3y)/40.
      Solve.

      Like

  • Unknown's avatar

    Jim Randell 1:34 pm on 10 March 2024 Permalink | Reply
    Tags: by: Rachel Blunt   

    Brain teaser 1023: A mixed maths class 

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

    My mathematics class consists of six boys and six girls. In their annual examination each was awarded integral an mark out of 100.

    Disappointingly no boy received a distinction (over 80)  but all the boys managed over 40. The lowest mark in the class was 36.

    Upon listing the boys’ marks I noticed that all their marks were different prime numbers and that their average was an even number. Further three of the boys’ marks formed an arithmetic progression, and the other three another arithmetic progression.

    Turning, my, attention to the girls I found that their marks were all different. There was little overall difference in the performance of the sexes, the total of the girls’ marks being just one more than the total of the boys’. Three of the girls’ marks formed one geometric progression, and the other three formed another geometric progression with the same ratio as the first one.

    Finally when listing the results in numerical order I was pleased to see that Annie (who did so badly last year) had come seventh in the class.

    What were the top six marks (in descending order)?

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

    [teaser1023]

     
    • Jim Randell's avatar

      Jim Randell 1:35 pm on 10 March 2024 Permalink | Reply

      This Python program looks at possible scores for the boys and the girls and then looks for a common key (for the boys the key is the sum of the scores, for the girls it is the sum minus 1), and then checks for possibilities that place a girl (Annie) in 7th position.

      It runs in 60ms. (Internal runtime is 3.0ms).

      Run: [ @replit ]

      from enigma import (
        irange, primes, subsets, partitions, seq_all_same, tuples,
        fraction, group, item, intersect, cproduct, printf
      )
      
      # does sequence <seq> form an arithmetic progression
      is_arithmetic = lambda seq: seq_all_same(y - x for (x, y) in tuples(seq, 2))
      
      # generate possible marks for the boys
      def gen_boys():
        # the marks are 6 different primes between 41 and 80
        for bs in subsets(primes.between(41, 80), size=6):
          # and their average is an even number
          t = sum(bs)
          if t % 12 != 0: continue
          # and they form two 3-length arithmetic progressions
          for (b1, b2) in partitions(bs, 3):
            if not (is_arithmetic(b1) and is_arithmetic(b2)): continue
            printf("[boys = {b1} + {b2} -> {t}]")
            # return (<total>, (<series1>, <series2>))
            yield (t, (b1, b2))
      
      # generate possible marks for the girls
      def gen_girls():
        # choose geometric progression that start 36
        a = 36
        for b in irange(37, 100):
          (c, r) = divmod(b * b, a)
          if c > 100: break
          if r != 0: continue
          # now look for another geometric progression with the same ratio = b/a
          for x in irange(37, 100):
            (y, ry) = divmod(x * b, a)
            (z, rz) = divmod(y * b, a)
            if z > 100: break
            if ry != 0 or rz != 0: continue
            t = sum([a, b, c, x, y, z]) - 1
            printf("[girls = ({a}, {b}, {c}) + ({x}, {y}, {z}); r={r} -> {t}]", r=fraction(b, a))
            # return (<total> - 1, (<series1>, <series2>))
            yield (t, ((a, b, c), (x, y, z)))
      
      # group boys by total
      boys = group(gen_boys(), by=item(0), f=item(1))
      
      # group girls by total - 1
      girls = group(gen_girls(), by=item(0), f=item(1))
      
      # look for common keys
      for k in intersect([boys.keys(), girls.keys()]):
        for ((b1, b2), (g1, g2)) in cproduct([boys[k], girls[k]]):
          # marks in order
          ms = sorted(b1 + b2 + g1 + g2, reverse=1)
          # 7th position (= index 6) must be a girl
          if not (ms[6] in g1 + g2): continue
          # output solution
          printf("boys = {b1} + {b2}, girls = {g1} + {g2} -> {ms}")
      

      Solution: The top six marks were: 90, 81, 79, 73, 67, 60.

      The only scenario is:

      boys = (41, 47, 53) + (67, 73, 79) → sum = 360
      both sequences have a common difference of 6

      girls = (36, 54, 81) + (40, 60, 90) → sum = 361
      both sequences have a common ratio of 3/2

      Which gives the following sequence of scores:

      1st = 90 (girl)
      2nd = 81 (girl)
      3rd = 79 (boy)
      4th = 73 (boy)
      5th = 67 (boy)
      6th = 60 (girl)
      7th = 54 (girl)
      8th = 53 (boy)
      9th = 47 (boy)
      10th = 41 (boy)
      11th = 40 (girl)
      12th = 36 (girl)

      This the only scenario where the 7th best score is a girl.

      Although there are two other scenarios for the boys that have the right sum, but each places a boy in 7th place overall:

      boys = (43, 61, 79) + (47, 59, 71) → sum = 360 [7th = 59]
      boys = (47, 53, 59) + (61, 67, 73) → sum = 360 [7th = 59]

      Separately there are 5 possible scenarios for the boys and 5 for the girls, but only those sequences with sums of 360/361 give matching keys.

      Like

    • GeoffR's avatar

      GeoffR 10:27 am on 12 March 2024 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % variables for boys scores
      var 41..79:B1; var 41..79:B2; var 41..79:B3; 
      var 41..79:B4; var 41..79:B5; var 41..79:B6; 
      
      % variables for girls scores
      var 36..100:G1; var 36..100:G2; var 36..100:G3;
      var 36..100:G4; var 36..100:G5; var 36..100:G6;
      
      % Geometric ratio is a/b, arithmetic difference = d
      var 1..9:a; var 1..9:b; var 1..9:d;  
      constraint a > b;
      
      % 2-digit primes for this teaser
      set of int: primes = {41, 43, 47, 53, 59, 61, 67, 71, 73, 79};
      
      constraint sum([B1 in primes, B2  in primes, B3  in primes,
      B4 in primes, B5 in primes, B6 in primes]) == 6;
       
      % Boys scores are in arithmetic progression (will be different)
      constraint B2 - B1 == d /\ B3 - B2 == d;
      constraint B4 > B1;
      constraint B5 - B4 == d /\ B6 - B5 == d;
       
      % Girls scores in geometric progression (will be different)
      constraint G1 == 36 /\ G4 > G1;
      constraint G2*b == G1*a /\ G3*b == G2*a
      /\ G5*b == G4*a /\ G6*b == G5*a;
      
      % Average of boys scores is an even number
      constraint (B1 + B2 + B3 + B4 + B5 + B6) div 6 > 0
      /\ (B1 + B2 + B3 + B4 + B5 + B6) mod 2 == 0;
      
      % Total of girls scores in one more than total of boys scores
      constraint G1 + G2 + G3 + G4 + G5 + G6 == B1 + B2 + B3 + B4 + B5 + B6 + 1;
      
      % Sort all pupils marks - gives ascending order
      array[1..12] of var int: pupils = [B1,B2,B3,B4,B5,B6,G1,G2,G3,G4,G5,G6];
      array[1..12] of var int: SP = sort(pupils);
      
      solve satisfy;
      
      output ["Boys scores = " ++ show([B1,B2,B3,B4,B5,B6]) 
      ++ "\n" ++ "Girls scores = " ++ show([G1,G2,G3,G4,G5,G6])
      ++ "\n" ++ "Pupils scores (in ascending order) = " ++ show(SP)
      ++ "\n" ++ "The top seven marks, (in descending order), were: "
      ++ show([SP[12], SP[11], SP[10], SP[9], SP[8], SP[7], SP[6]]) ];
      
      % Boys scores = [47, 53, 59, 61, 67, 73]
      % Girls scores = [36, 54, 81, 40, 60, 90]
      % Pupils scores (in ascending order) = [36, 40, 47, 53, 54, 59, 60, 61, 67, 73, 81, 90]
      % The top seven marks, (in descending order), were: 
      % [90, 81, 73, 67, 61, 60, 59]
      % ----------
      % Boys scores = [41, 47, 53, 67, 73, 79]
      % Girls scores = [36, 54, 81, 40, 60, 90]
      % Pupils scores (in ascending order) = [36, 40, 41, 47, 53, 54, 60, 67, 73, 79, 81, 90]
      % The top seven marks, (in descending order), were: 
      % [90, 81, 79, 73, 67, 60, 54]  <<< answer
      % ----------
      % ==========
      % The second solution gives a score of 54 in 7th position - a girl's score
      % and the first solution gives a score of 59 in 7th position - a boy's score
      

      Like

  • Unknown's avatar

    Jim Randell 8:13 am on 27 February 2024 Permalink | Reply
    Tags:   

    Brain teaser 980: Is anybody there? 

    From The Sunday Times, 3rd May 1981 [link]

    People often ask me how I first got involved with setting teasers, and the answer is rather interesting. A friend suggested that I should like to visit a medium, who, with spiritual help, would give me some advice for the future. He said that he knew a jolly woman who would probably be able to suggest an exciting change in my career (I thought it more likely that I’d strike a happy medium).

    Anyway, I went along, and found that this medium had an unusual ouija board. It consisted of 24 points equally spaced around the perimeter of a circle. She invited me to write a letter of the alphabet against each of these points. Some letters were used more than once, and some (of course) were not used at all.

    Then, after much concentration, a counter moved from the centre of the circle straight to a letter. It paused, and then went straight to another letter, and so on. When it had completed a word, it went straight back to the centre of the circle, paused, and then started the next word in the same way.

    There were two fascinating things about the message that it spelt out for me. The first was that, each time the counter moved, it moved an exact whole number of inches. The second was that it spelt out the message:

    SET
    A
    SUNDAY
    TIMES
    BRAIN
    TEASER
    IN
    ???

    The last bit consisted of three letters which were the first three letters of a month.

    Which month?

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

    [teaser980]

     
    • Jim Randell's avatar

      Jim Randell 8:15 am on 27 February 2024 Permalink | Reply

      In the 1994 book the words are presented as:

      SET A SUNDAY TIMES BRAINTEASER IN ???

      However there is no solution with BRAINTEASER as a single word.


      See: Teaser 2647.

      We could discard A and IN from the list of words, as they appear as contiguous substrings of other words.

      This Python program constructs possible sets of orbits for the given words, and then attempts to add in an extra 3-letter word corresponding to a month of the year.

      It runs in 57ms. (Internal runtime is 1.3ms).

      Run: [ @replit ]

      from enigma import (update, rev, ordered, unpack, wrap, uniq, join, printf)
      
      # add a word to a collection of (not more than k) orbits
      def add_word(k, word, orbs):
        # collect letters by odd/even parity
        (w0, w1) = (set(word[0::2]), set(word[1::2]))
        if len(w0) > k or len(w1) > k: return
        # consider existing orbits
        for (i, t) in enumerate(orbs):
          for (s0, s1) in [t, rev(t)]:
            orb_ = (s0.union(w0), s1.union(w1))
            if all(len(x) < 4 for x in orb_):
              yield update(orbs, [(i, orb_)])
        if len(orbs) < k:
          # add the word to a new orbit
          yield orbs + [(w0, w1)]
      
      # make <words> using <k> orbits
      def make_words(k, words, orbs=list()):
        # are we done?
        if not words:
          # provide orbits in a canonical order
          yield orbs
        else:
          w = words[0]
          # attempt to add word <w>
          for orbs_ in add_word(k, w, orbs):
            yield from make_words(k, words[1:], orbs_)
      
      fmt_orb = unpack(lambda x, y: join([join(sorted(x)), join(sorted(y))], sep="+"))
      fmt = lambda orbs: join(map(fmt_orb, orbs), sep=", ")
      
      @wrap(uniq)
      def solve():
        # words to spell out
        words = "SET A SUNDAY TIMES BRAIN TEASER IN".split()
        # possible months
        months = "JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC".split()
      
        # make orbits for the given words
        for orbs in make_words(4, words):
          # attempt to also add a month
          for w in months:
            for orbs_ in add_word(4, w, orbs):
              # return orbits in a canonical form
              orbs_ = ordered(*(ordered(ordered(*p0), ordered(*p1)) for (p0, p1) in orbs_))
              yield (w, orbs_)
      
      # solve the puzzle
      for (w, orbs) in solve():
        printf("{w} <- {orbs}", orbs=fmt(orbs))
      

      Solution: The month is March.

      i.e. the final “word” is MAR.

      This can be achieved with the following set of orbits:

      ABN+IMR → A, BRAIN, IN, MAR
      AET+ERS → A, TEASER
      ANS+DUY → A, SUNDAY
      ?EI+MST → SET, TIMES

      One of the letters remains unassigned.

      Like

  • Unknown's avatar

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

    Brain teaser 1004: Ladder limit 

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

    The outside of the house was almost painted; there remained only the barge board on the gable end to do. This wall of the house has the garage running along it, and immediately adjacent to it. My garage serves as an outhouse as well, so it is well proportioned, being 12 feet high, and having a flat roof 12 feet wide.

    My ladder is 35 feet long. Making sure that the foot of the ladder was firmly established on the ground, I rested the upper end against the wall. The top of the ladder just touched the apex: I had been very lucky, for I realised that it would have been impossible for the ladder to reach the apex had that apex been any higher.

    Later, after removing the ladder, I noticed that there was a small unpainted area where the ladder had rested. To save getting the ladder out again, I decided to touch-up this spot by standing on the garage roof, and using a brush tied to a long cane. In order to see if this were possible, I had to calculate the height of the apex above the garage roof.

    What was that height?

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

    [teaser1004]

     
    • Jim Randell's avatar

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

      If the ladder makes an angle θ with the ground (0 < θ < 90°), and the amount of ladder above the garage is x (0 < x < 35), then:

      cos(θ) = 12 / x

      And the amount of ladder below the garage is (35 − x):

      sin(θ) = 12 / (35 − x)

      Using sin² + cos² = 1, we have:

      (12/x)^2 + (12/(35 − x))^2 = 1

      (12^2)((35 − x)^2 + x^2) = x^2(35 − x)^2

      So we need to find roots of the polynomial:

      x^4 − 70 x^3 + 937 x^2 + 10080 x − 176400 = 0

      This can be factorised as:

      (x − 15)(x − 20)(x^2 − 35x – 588) = 0

      And the only roots in the required range are the rational roots x = 15, x = 20.

      The height of the apex above the garage roof is given by:

      h = 12x / (35 − x)

      So:

      x = 15 → h = 9
      x = 20 → h = 16

      And we want the maximum value.

      Solution: The apex is 16 feet above the roof of the garage.


      And we can do a similar thing programatically:

      Run: [ @replit ]

      from enigma import (Polynomial, Accumulator, sq, fdiv, catch, printf)
      
      # (12/(35 - x))^2 + (12/x)^2 = 1
      p1 = sq(Polynomial([35, -1]))  # p1 = (35 - x)^2
      p2 = Polynomial([0, 0, 1])  # p2 = x^2
      
      # calculate the polynomial
      p = p1 * p2 - sq(12) * (p1 + p2)
      printf("[p = {p}]")
      
      # look for real roots of p
      r = Accumulator(fn=max)
      for x in p.find_roots(domain='F'):
        h = catch(fdiv, 12 * x, 35 - x)
        printf("[x={x} -> h={h}]")
        if 0 < x < 35:
          r.accumulate(h)
      
      # answer is the maximum result
      h = r.value
      printf("h = {h:.2f}")
      

      Like

      • Jim Randell's avatar

        Jim Randell 10:45 am on 30 January 2024 Permalink | Reply

        I wonder if this puzzle (set 42 years ago) is set by the same Paul Hughes who recently set Teaser 3191.

        According to The Sunday Times Book of Brainteasers (1994), the Paul Hughes who set Teaser 1004 is:

        Seaman, mountaineer, historian and pilot. This problem occurred while navigating through the Trobriand Islands, but was set differently for publication.

        Like

    • Hugo's avatar

      Hugo 2:08 pm on 30 January 2024 Permalink | Reply

      So the ladder formed the hypotenuse of two triangles with ratio 3:4:5, one below and one above the corner of the garage roof.
      It doesn’t sound a safe angle for a ladder: I hope he secured the foot before climbing it.

      Like

  • Unknown's avatar

    Jim Randell 11:17 am on 31 December 2023 Permalink | Reply
    Tags:   

    Brain teaser 988: Pythagoras was never like this 

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

    In accordance with tradition the retiring President of the All Square Club set members a special competition. Titled as above, it required new meanings for the signs “+” and  “=” as in:

    6² + 1² = 19²

    indicating that both sides could be taken to represent 361.

    The number 361 was then called a “Special Pythogorean Square” (SPS) and the numbers 36 and 1 its “contributing squares”.

    Other examples given were:

    7² + 27² = 223²
    15² + 25² = 475²

    giving 49729 and 225625 as Special Pythogorean Squares.

    Members were invited to submit series of Special Pythogorean Squares which they could claim to be unique in some way. Each number in their series had to be less than a million, and no two numbers in their series could have the same number of digits.

    After much deliberation the prize was awarded to the member who submitted a series of Special Pythogorean Squares which he correctly claimed was the longest possible list of such numbers all with the same second contributing square.

    What (in increasing order) were the numbers in the winning series?

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

    [teaser988]

     
    • Jim Randell's avatar

      Jim Randell 11:17 am on 31 December 2023 Permalink | Reply

      So we treat “+” and “=” as string operations, being concatenation and equality respectively.

      This Python program generates all SPSs below 1 million, and then constructs maximal length sequences which share the same suffix square.

      It runs in 61ms. (Internal runtime is 2ms).

      Run: [ @replit ]

      from enigma import (
        defaultdict, Accumulator, irange, inf, sq, cproduct, join, printf
      )
      
      # generate SPSs
      def generate(N):
        # record squares (as strings)
        d = dict()
        for i in irange(0, inf):
          n = sq(i)
          if not (n < N): break
          s = str(n)
          d[s] = i
      
          # split the square into prefix and suffix
          for j in irange(1, len(s) - 1):
            (pre, suf) = (s[:j], s[j:])
            (pi, si) = (d.get(pre), d.get(suf))
            if pi is None or si is None: continue
            printf("[{n} = {pre} + {suf} -> {i}^2 = {pi}^2 + {si}^2]")
            yield (s, pre, suf)
      
      # collect SPSs: <suffix> -> <length> -> [<SPS>...]
      d = defaultdict(lambda: defaultdict(list))
      for (s, pre, suf) in generate(1000000):
        d[suf][len(s)].append(s)
      
      # find maximal length sequences sharing the same suffix
      r = Accumulator(fn=max, collect=1)
      for (suff, v) in d.items():
        r.accumulate_data(len(v.keys()), suff)
      printf("maximal sequence length = {r.value}")
      
      # output maximal length sequences
      for k in r.data:
        printf("suffix = {k}")
        for ss in cproduct(d[k].values()):
          printf("-> {ss}", ss=join(ss, sep=", ", enc="()"))
      

      Solution: The winning series was: 49, 169, 3249, 64009, 237169.

      Which are constructed as follows:

      49 = 4 + 9 → 7^2 = 2^2 + 3^2
      169 = 16 + 9 → 13^2 = 4^2 + 3^2
      3249 = 324 + 9 → 57^2 = 18^2 + 3^2
      64009 = 6400 + 9 → 253^2 = 80^2 + 3^2
      237169 = 23716 + 9 → 487^2 = 154^2 + 3^2

      Each has a suffix square of 9.

      Like

      • Frits's avatar

        Frits 4:55 pm on 3 January 2024 Permalink | Reply

        Slower.

            
        from enigma import SubstitutedExpression
        
        # the alphametic puzzle
        p = SubstitutedExpression(
          [
            # ABC^2 concatenated with DEF^2 = square
            "ABC > 0",
            # RHS must be less than a million
            "(d2 := DEF**2) < 10 ** (6 - len(str(a2 := ABC**2)))",
            "is_square(int(str(a2) +  str(d2)))",
          ],
          answer="ABC, DEF",
          d2i="",
          distinct="",
          reorder=0,
          verbose=0,    # use 256 to see the generated code
        )
        
        # store answers in dictionary (key = suffix)
        d = dict()
        for a, b in p.answers():
          d[b] = d.get(b, []) + [a]
        
        # find maximal length sequences sharing the same suffix
        m = len(max(d.values(), key=lambda k: len(k)))
        
        # assume more than one answer is possible
        print("answer:", ' or '.join(str(x) for x in
             [sorted(int(str(v**2) + str(k**2)) for v in vs)
                     for k, vs in d.items() if len(vs) == m]))
        

        Like

        • Jim Randell's avatar

          Jim Randell 10:25 pm on 4 January 2024 Permalink | Reply

          @Frits: Would this work if there were multiple SPSs with the same length and suffix?(e.g. if 99856+9 were a valid SPS).

          Like

          • Frits's avatar

            Frits 6:39 pm on 5 January 2024 Permalink | Reply

            @Jim, you are right. This wouldn’t work as I forgot the condition “no two numbers in their series could have the same number of digits”.

            Like

  • Unknown's avatar

    Jim Randell 8:30 am on 19 December 2023 Permalink | Reply
    Tags:   

    Brainteaser 1087: Pennies from heaven 

    From The Sunday Times, 5th June 1983 [link]

    In our club we have three one-armed bandits. The Saturn Skyjacker accepts 10p, 2p and 1p coins, the Mars Marauder accepts 10p and 1p coins, and the Aries Axeman accepts 5p and 2p coins.

    I am the club treasurer, so each week I have the onerous task of emptying the machines and counting the coins. Last week, my efforts were rewarded with the discovery of an interesting series of coincidences. On counting the coins for the Saturn Skyjacker, I found that there were the same number of coins of two of the denominations, and that the number of coins of the third denomination differed from this number by only one. In addition, the total value of all the coins was an exact number of pounds less than one hundred.

    The coins from the Mars Marauder were similarly distributed: the numbers of 10p and 1p coins differed by only one, and the total value was again an exact number of pounds. In fact, this total value was the same as for the Saturn Skyjacker.

    Incredibly, the same was true for coins from the Aries Axeman: the numbers of 5p and 2p coins differed by one, and the total value was the same as for the Mars Marauder and the Saturn Skyjacker.

    What was the total number of coins I emptied that day?

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

    [teaser1087]

     
    • Jim Randell's avatar

      Jim Randell 8:31 am on 19 December 2023 Permalink | Reply

      This Python program runs in 60ms. (Internal runtime is 306µs).

      Run: [ @replit ]

      from enigma import (irange, div, cproduct, printf)
      
      # decompose t into k * x and (k +/- 1) * y
      def decompose(t, x, y):
        k = div(t - y, x + y)
        if k: yield (k, k + 1)
        k = div(t + y, x + y)
        if k: yield (k, k - 1)
      
      # total number of pounds is less than 100
      for t in irange(100, 9900, step=100):
        # can we make this total for (10, 1) and (5, 2)
        for (m, a) in cproduct([decompose(t, 10, 1), decompose(t, 5, 2)]):
          # try to make t from (10, 2, 1) by combining 2 of the values
          for (x, y) in [(12, 1), (11, 2), (3, 10)]:
            for s in decompose(t, x, y):
              # calculate total number of coins
              n = sum(m) + sum(a) + sum(s) + s[0]
              # output solution
              printf("{n} coins [t={t}: m={m} a={a} -> x={x} y={y} s={s}]")
      

      Solution: The total number of coins is: 3001.

      In the Saturn Skyjacker there were 331× 10p + 330× 2p + 330× 1p = 4300p.

      In the Mars Marauder there were 391× 10p + 390× 1p = 4300p.

      In the Aries Axman there were 614× 5p + 615× 2p = 4300p.

      So in total there were 3001 coins totalling £129.

      Like

    • Frits's avatar

      Frits 2:48 pm on 19 December 2023 Permalink | Reply

       
      # coins (pennies) for Saturn Skyjacker, the Mars Marauder and the Aries Axeman
      cs = [{1, 2, 10}, {1, 10}, {2, 5}]
      
      # total number of pounds is less than 100
      for t in range(100, 10000, 100):
        # remainder by sum of coin denominations should be equal to one of the 
        # coin denominations or equal to the sum of all coin denominations but one
        if all((t % sum(c)) in c | {sum(c) - x for x in c} for c in cs):
          print("answer:", sum(len(c) * (t // sum(c)) + 
                (1 if (t % sum(c)) in c else len(c) - 1) for c in cs))  
      

      Like

    • GeoffR's avatar

      GeoffR 8:36 pm on 27 December 2023 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Saturn Skyjacker coins are 1p, 2p and 10p
      var 1..9999:SS;   % maximum Saturn Skyjacker total(p)
      
      % Assumed UB for SS, MM and AA coin numbers
      var 1..1000:s10; var 1..1000:s2; var 1..1000:s1; 
      
      % Two coins hae the same number, differing fron the other denomination by 1
      constraint (s10 == s2 /\ abs(s10 - s1) == 1)
      \/ (s10 == s1 /\ abs(s10 - s2) == 1)
      \/ (s1 == s2 /\ abs(s1 - s10) == 1);
      
      constraint s1*1 + s2*2 + s10*10 == SS;
      constraint SS div 100 < 100 /\ SS mod 100 == 0;
      
      % Mars Marauder coins 1p and 10p
      var 1..9999:MM; % maximum Mars Marauder total(p)
      var 1..1000:m1; var 1..1000:m10;
      
      % Mars Marauder coin numbers differ by 1
      constraint abs(m1 - m10) == 1;
      constraint MM = m1 * 1  + m10 * 10;
      constraint MM div 100 < 100 /\ MM mod 100 == 0;
      
      % Aries Axem coins are 2p and 5p
      var 1..9999:AA; % maximum Aries Axem  total(p)
      var 1..1000:a2; var 1..1000:a5;
      
      % Aries Axem coin numbers differ by 1
      constraint abs(a2 - a5) == 1;
      constraint AA = a2 * 2  + a5 * 5;
      constraint AA div 100 < 100 /\ AA mod 100 == 0;
      
      % Total amount from three machines is the same
      constraint MM == SS /\ MM == AA;
      
      % Total number of coins
      var 1..7000:tot_coins == s1 + s2 + s10 + m1 + m10 + a2 + a5;
      
      solve satisfy;
      
      output[" Total number of coins = " ++ show(tot_coins) ++ "\n" ++
      " Saturn coins are s1, s2, s10 = " ++ show([s1, s2, s10] ) ++ "\n" ++
      " Mars coins m1 and m10 = " ++ show([m1, m10]) ++ "\n" ++
      " Aries coins are a2 and a5 = " ++ show([a2, a5]) ++ "\n" ++
      " Total value in each machine = £" ++ show(SS div 100) ];
      
      %  Total number of coins = 3001
      %  Saturn coins are s1, s2, s10 = [330, 330, 331]
      %  Mars coins m1 and m10 = [390, 391]
      %  Aries coins are a2 and a5 = [615, 614]
      %  Total value in each machine = £43
      % ----------
      % ==========
      % Finished in 506msec.
      
      

      Like

  • Unknown's avatar

    Jim Randell 8:13 am on 5 November 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 935: Ears, noses & throats 

    From The Sunday Times, 22nd June 1980 [link]

    Our local hospital is a busy place, with a large department, luckily, to keep records of what is wrong with whom. Those who come to the ENT specialist to complain about their ears, sometimes complain about their nose or their throat.

    Of his 60 patients, the number complaining of ears and nose only is three times as great as the number complaining of ears and throat only.

    Another group consists of those who say that their ears are the only body part of the three where they are in good health; and this group is three times as large as the group which declares that the throat alone is healthy.

    It’s all very confusing, I know; but I can tell you that there are 110 complaints in all — if you count a complaint about two ears as one complaint.

    Everyone complains about at least one thing.

    How many patients come with complaints about one part of their anatomy (ears or nose or throat) only?

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

    [teaser935]

     
    • Jim Randell's avatar

      Jim Randell 8:14 am on 5 November 2023 Permalink | Reply

      We can label the areas of the Venn diagram we are interested in as: E, N, T, EN, ET, NT, ENT (this accounts for all patients, as each has at least one complaint).

      We have:

      E + N + T + EN + ET + NT + ENT = 60
      EN = 3 ET
      NT = 3 EN
      E + N + T + 2(EN + ET + NT) + 3 ENT = 110

      And we want to determine the value of E + N + T.

      So:

      EN = 3 ET
      NT = 3 EN = 9 ET

      Hence:

      EN + ET + NT = 3 ET + ET + 9 ET = 13 ET

      Writing: X = E + N + T, we get:

      X + 13 ET + ENT = 60
      X + 26 ET + 3 ENT = 110

      X = ENT + 10

      And so:

      2 ENT = 50 − 13 ET

      There are 2 possibilities:

      ET = 0 ⇒ ENT = 25, E+N+T = 35
      ET = 2 ⇒ ENT = 12, E+N+T = 22

      If we suppose the phrase: “three times as great/large” in the puzzle text precludes zero values, then only a single solution remains.

      Solution: 22 of the patients have a single complaint.

      Like

  • Unknown's avatar

    Jim Randell 9:32 am on 17 September 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 919: Golden age of steam 

    From The Sunday Times, 2nd March 1980 [link]

    Ever since the dawn of history, mankind has looked back to a Golden Age when everybody and everything was so much better.

    As far as our railway system is concerned, the Golden Age was the prewar era of the great steam locomotives. In spite of their noise, steam, and smoke pollution, (or perhaps even because of these characteristics), the older generation look back with nostalgia to that golden era. Let us therefore pose a not-too-difficult problem concerning those wonderful days.

    Two steam locomotives, each travelling at 50 mph, are driven by Tom and Dick. They are approaching Harry’s signal box from opposite directions.

    Tom sounds his whistle. Immediately upon hearing it, Dick replies. Harry hears both whistles with an interval of seven seconds between them. The two engines later pass the signal box at the same instant.

    You may assume that the speed of sound was 1100 feet per second relative to the ground. One mile has 5280 feet.

    How far (in feet) was Tom from the box when he sounded his whistle?

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

    [teaser919]

     
    • Jim Randell's avatar

      Jim Randell 9:33 am on 17 September 2023 Permalink | Reply

      The trains are travelling at the same speed, and (presumably on different tracks) pass the signal box at the same time, so they must start the same distance from the signal box.

      Working in feet per second, a speed of 50 mph = 220/3 ft/s.

      Suppose at time t = 0 both T and D are a distance x feet from H, and T sounds his whistle.

      At a time t = x / 1100 H hears T’s whistle.

      And if D hears the whistle when he is a distance y from H, then he hears T’s whistle at time t = (x + y) / 1100.

      D instantaneously responds (still at distance y), and H hears D’s whistle at time t = (x + 2y) / 1100.

      H hears the whistles 7 seconds apart, so:

      (x + 2y) / 1100 = 7 + x / 1100
      y = (7/2)1100 = 3850

      So D sounds his whistle when he is 3850 ft from H at time t = (x + 3850) / 1100.

      And at this time T must also be 3850 ft from H.

      Hence:

      x = 3850 + (220/3)(x + 3850) / 1100
      ⇒ x = 4400

      Solution: Tom was 4400 ft from the signal box when he sounded his whistle.

      Like

    • Hugo's avatar

      Hugo 9:49 am on 17 September 2023 Permalink | Reply

      It must have been a cold day: the speed of sound is about 1100 ft/s at 5°C and 60% relative humidity
      (about 3 ft/s less in dry air at that temperature).

      Like

  • Unknown's avatar

    Jim Randell 8:05 am on 10 September 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 928: Confused contracts 

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

    The results of the hands in a bridge tournament were written like this: “Joe Smith, two ♥”. Before I could calculate the scores, my dog tore up the results of the last four players. Fortunately the writing remained legible. By manoeuvring the fragments, and knowing there had been one contract in each suit, and one at each of the one, two, three and four levels, I was able to construct various feasible combinations.

    I first tried putting ♦ opposite Ted, Pete and Reg in turn. Ted could have ♦ only when Mr Green had the two contract; Pete only when ♠ were four; and Reg only when Mr Black had the one. Similarly, if Reg or Mr Black had ♣, then Mr Green had four; If Mr White had ♥ or ♣ then Mr Black had the other; if Reg had ♥ then Pete had ♦; if Mr Brown had ♥ then Mr Black had four; if Mr Black had ♥ then Mr Green had two; and if Mr Black didn’t have ♥ then ♠ were in less than four.

    Mr Black could have one only when Pete had three ♠; Mr Green could have two only when ♥ were four; Mr Black had four only when Reg had ♦; Mr Green had four only when ♦ were in three; and ♣ could be in three or four only when Reg had ♥.

    Finally I noted that Ted and Vic had the same coloured suits whenever I tried putting ♠ with the three, but different coloured suits if Mr White did not have ♥.

    Luckily, I then remembered that ♦ had actually been bid at the four level.

    What was the suit of the three contract, and what was the full name of the player who bid it?

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

    [teaser928]

     
    • Jim Randell's avatar

      Jim Randell 8:06 am on 10 September 2023 Permalink | Reply

      I know nothing of Bridge, so at first the puzzle text made no sense to me, but there is a handy hint in the 1974 book:

      Ignoring the Bridge jargon, you have to match each of the numbers 1 to 4 with one of the suits, a forename, and a surname.

      I used the [[ SubstitutedExpression ]] solver to allocate the values among the three groups, so all we need to do is translate the text into a collection of constraints which we can represent by Python expressions.

      I translated: “If A had X or Y, then B had the other” into: “(A = X) ⇒ (B = Y)” and “(A = Y) ⇒ (B = X)”.

      Note that we might expect to deduce “Reg != Black” from the statement “if Reg or Mr Black had …”, but if we try this then there are no solutions.

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

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # allocate values 1-4 to the following:
      #
      #  suits    : C D S H [Clubs, Diamonds, Spades, Hearts]
      #  forenames: T R P V [Ted, Reg, Pete, Vic]
      #  surnames : B K G W [Brown, blacK, Green, White]
      
      SubstitutedExpression
      
      --digits="1-4"
      --distinct="CDSH,TRPV,BKGW"
      
      # specify the conditions
      "implies(T == D, G == 2)"
      "implies(P == D, S == 4)"
      "implies(R == D, K == 1)"
      "implies(R == C or K == C, G == 4)"
      "implies(W == H, K == C)" "implies(W == C, K == H)"
      "implies(R == H, P == D)"
      "implies(B == H, K == 4)"
      "implies(K == H, G == 2)"
      "implies(K != H, S != 4)"
      "implies(K == 1, P == 3 == S)"
      "implies(G == 2, H == 4)"
      "implies(K == 4, R == D)"
      "implies(G == 4, D == 3)"
      "implies(C == 3 or C == 4, R == H)"
      "implies(S == 3, {T, V} in [{C, S}, {D, H}])"
      "implies(W != H, {T, V} not in [{C, S}, {D, H}])"
      
      # diamonds was the 4th contract
      --assign="D,4"
      
      # mention all the variables
      "true(C, D, S, H, T, R, P, V, B, K, G, W)"
      
      --template="(C D S H) (T R P V) (B K G W)"
      --solution=""
      

      Solution: The 3 contract was in hearts. It was bid by Pete Green.

      The contracts are fully defined:

      1♣ = Ted Brown
      2♠ = Reg Black
      3♥ = Pete Green
      4♦ = Vic White

      And we see that Reg = Black.

      Like

    • Frits's avatar

      Frits 4:06 pm on 11 September 2023 Permalink | Reply

      More than 200 lines of code.

      from itertools import combinations 
      
      # allocate values 1-4 to the following:
      #
      #  suits    : C D S H [Clubs, Diamonds, Spades, Hearts]
      #  forenames: T R P V [Tom, Reg, Pete, Vic]
      #  surnames : B K G W [Brown, blacK, Green, White]
      
      # 0/1/2 for no/some/all details
      detail = 1
      
      rev = lambda x: "neq" if x == "eq" else "eq"
      
      # a specific print format
      p_frz = lambda x, y: f"{x:>3}-{''.join(sorted(y))}"
      
      # add implication if <x> then <y> 
      def implies(x, y):
        if "=" in x:
          op1 = "eq" if "==" in x else "neq"
          op2 = "eq" if "==" in y else "neq"
          # use frozenset to make it hashable
          var1, var2 = frozenset([x[0], x[-1]]), frozenset([y[0], y[-1]])
          imp[(op1, var1)] = imp.get((op1, var1), []) + [(op2, var2)]
          # if a implies b then not b implies not a
          imp[(rev(op2), var2)] = imp.get((rev(op2), var2), []) + [(rev(op1), var1)]
        else:
          if x in imp:
            if y not in imp[x]:
              # check if y results in a falsehood
              if y == (rev(x[0]), x[1]):
                print(f"ERROR {x}-{y} is false")
               
              if y[1] in fb[x[1]] and x[0] == y[0] == "eq":  
                if detail: 
                  print(f"{p_frz(x[0], x[1])} is false as it leads to "
                        f"eq-{''.join(y[1])} which is not allowed")
                add_truth("neq", x[1])
              imp[x] += [y]
          else:
             imp[x] = [y]
      
      # print implications
      def print_imp(d):
        print()  
        for (a, b), vs in sorted(d.items()):
          print(f"{p_frz(a, b)}  : ", end=' ')
          for (c, d) in vs:
            print(f"{p_frz(c, d)}", end=', ')
          print()
        print()  
            
      # add entries to truth list <t>
      def add_truth(c, s):
        s_ = frozenset(s)
        if (c, s_) in t: return
        l_ = list(s)
        if detail: print("add to t:", p_frz(c, s_))
        t.add((c, s_))
        if c != "eq": return
        
        # add forbidden entries
        for f in fb[s_]:
          if detail > 1: print("add inherited rules to t:", p_frz("neq", f))
          t.add(("neq", f)) 
      
        toadd = []
        # propagate 'eq-ab': if 'neq-ac' exists then also 'neq-bc' must be true
        for d, f in t:
          if d != "neq": continue
          for i, x in enumerate(l_):
            if x not in f: continue
            o, = f.difference([x])
            if o not in bd[l_[1 - i]]:
              toadd.append(frozenset([l_[1 - i], o]))
      
        for x in toadd:
          add_truth("neq", x)
                  
      imp, fb, bd = dict(), dict(), dict()
      
      # specify the conditions
      implies("T == D", "G == 2")
      implies("P == D", "S == 4")
      implies("R == D", "K == 1")
      implies("W == H", "K == C") 
      implies("W == C", "K == H")
      implies("R == H", "P == D")
      implies("B == H", "K == 4")
      implies("K == H", "G == 2")
      implies("K != H", "S != 4")
      implies("K == 1", "P == 3")
      implies("K == 1", "P == S")
      implies("K == 1", "S == 3")
      implies("G == 2", "H == 4")
      implies("K == 4", "R == D")
      implies("G == 4", "D == 3")
      
      #implies("R == C or K == C", "G == 4")
      implies("R == C", "G == 4")
      implies("K == C", "G == 4")
      #implies("C == 3 or C == 4", "R == H")
      implies("C == 3", "R == H")
      implies("C == 4", "R == H")
      
      #implies("S == 3", "{T, V} in [{C, S}, {D, H}]")
      #implies("W != H", "{T, V} not in [{C, S}, {D, H}]")
      implies("S == 3", "W == H")
      
      types = [x.split() for x in ["C D S H", "T P R V", "B K G W", "1 2 3 4"]]
      # buddies
      bd = {x: [y for y in types[i] if y != x] 
                for i, tp in enumerate(types) for x in tp}
      # forbidden values
      for a, b in combinations(bd.keys(), 2):
        if b in bd[a]: continue
        fb[frozenset([a, b])] = [frozenset([a, x]) for x in bd[b]] + \
                                [frozenset([b, x]) for x in bd[a]] 
      
      if detail > 1:
        print("forbidden\n---------")
        for k, vs in sorted(fb.items()):
          print(''.join(k), end="  :  ")
          for v in vs:
            print(''.join(v), end=", ")
          print()
        print()  
      
      t = set()
      # diamonds was the 4th contract
      add_truth('eq', frozenset(['D', '4']))
      
      loop, ln = 1, len(t)
      while True:
      
        if detail:
          print(f"\nloop {loop}\n------")
          if loop < 3: print_imp(imp)
      
        if detail > 1:
          print(f"\ntruths\n------")
          for x in sorted(t, key = lambda x: (x[0], sorted(x[1]) ) ):
            print(p_frz(x[0], ''.join(sorted(x[1]))))
        
        # expand implications by chaining
        for k, vs in imp.items():
          for v in vs:
            # check if v is allowed ...
            if (rev(v[0]), v[1]) in t:
              # ... if not disallow k
              add_truth(rev(k[0]), k[1])
            
            if v in imp:
              # add implications of v to k
              for i in imp[v]:
                implies(k, i)
      
        # check for 3 non-equalities in a group
        toadd = []
        for c, f in t:
          if c != "neq": continue
          lst = list(f)
          # for both elements within f
          for i in range(2):
            y = [("neq", frozenset([x, lst[1-i]])) in t for x in bd[lst[i]]]
            if sum(y) != 2: continue
            # we have 3 neq's so the remaining entry in group must be eq
            toadd.append(frozenset([lst[1-i], bd[lst[i]][y.index(0)]]))
        
        for x in toadd:
          add_truth("eq", x)
        
        # only if normal logic has been exhausted
        if len(t) == ln:
          lookup = lambda d, x: [f for c, f in d if c == "eq" and x in f and 
                                 len(f | set("1234")) == 5]
              
          # implies("S == 3", "{T, V} in [{C, S}, {D, H}]")
          # implies("W != H", "{T, V} not in [{C, S}, {D, H}]")
          s3 = ("eq", frozenset(["S", "3"])) in t
          ns3 = ("neq", frozenset(["S", "3"])) in t
          if not s3 and not ns3: break
          
          # D is 4 so try to find H
          tH = lookup(t, "H")      
          if not tH: break
      
          tT, tV = lookup(t, "T"), lookup(t, "V")    
          if not tV and not tT: break
      
          H, = tH[0].difference(["H"]) 
          DH = {'4', H}
          
          vr, ot = "V" if tV else "T", "T" if tV else "V"
         
          v, = (tV[0] if tV else tT[0]).difference([vr]) 
      
          n = DH.difference([v]).pop() if v in DH else (set("1234") - DH - {v}).pop()
          add_truth("neq" if ns3 else "eq", frozenset([ot, n]))
        
        r3 = [''.join(f.difference(["3"])) for c, f in t if c == "eq" and '3' in f]
        # is there enough data in row 3 for the answer?
        if len(r3) == 3: 
          print(f"\nanswer: {r3[0]}, {r3[1]} and {r3[2]}") 
          exit(0)
        
        ln = len(t)  
        loop += 1     
      

      Like

  • Unknown's avatar

    Jim Randell 8:54 am on 3 September 2023 Permalink | Reply
    Tags: by: Ian Adamson   

    Brain-Teaser 937: Number please? 

    From The Sunday Times, 6th July 1980 [link]

    It was so stifling that afternoon that I had been dozing on my bed with the curtains closed, the air conditioning full on, and a bottle of Vichy water on my bedside table.

    The harsh ring of the telephone brought me to my full consciousness. “This is X9 calling”, said the voice, “it’s been a hot afternoon”. This was the message I had been waiting for, so I replied, “Yes, but it’s cooler in Stockholm.”

    “Now listen carefully”, continued X9, “I want you to telephone me at eleven o’clock tonight. The first digit of my number is twice the first digit of your age; the second digit of my number is three times the second (and last) of your age. Have you got that?”

    “Yes”, I acknowledged, “but why this secrecy?”

    “There may be people listening in. My third digit is the difference of my first two digits — and the fourth is the sum of my first two digits. The final digit of my number is the sum of my third and fourth digits.”

    At first, I worked out an incorrect telephone number, as I had forgotten that I had just recently celebrated my birthday — the difference between the two telephone numbers being a multiple of my age.

    What number should I telephone?

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

    [teaser937]

     
    • Jim Randell's avatar

      Jim Randell 8:54 am on 3 September 2023 Permalink | Reply

      This Python program runs in 53ms. (Internal runtime is 95µs).

      Run: [ @replit ]

      from enigma import (cproduct, nconcat, tuples, printf)
      
      # generate possible (<age>, <number>) pairs
      def generate():
        # for a 2-digit age we have:
        # 1st digit = 1, 2, 3, 4
        # 2nd digit = 0, 1, 2, 3
        for (x, y) in cproduct([(1, 2, 3, 4), (0, 1, 2, 3)]):
          # calculate the 5-digit number <abcde>
          a = 2 * x
          b = 3 * y
          c = abs(a - b)
          d = a + b
          e = c + d
          if d > 9 or e > 9: continue
          # return viable pair
          yield (nconcat(x, y), nconcat(a, b, c, d, e))
      
      # collect the pairs in a dict
      d = dict(generate())
      
      # look for consecutive ages
      for (k1, k2) in tuples(sorted(d.keys()), 2):
        if k1 + 1 != k2: continue
        (v1, v2) = (d[k1], d[k2])
        # difference in numbers is a multiple of the current age
        if (v1 - v2) % k2 > 0: continue
        # output solution
        printf("{k2} -> {v2} [{k1} -> {v1}]")
      

      Solution: The telephone number is 43178.

      This is derived from an age of 21.

      21 → (4 = 2×2)(3 = 3×1)(1 = 4 − 3)(7 = 4 + 3)(8 = 1 + 7) = 43178

      So the incorrectly calculated number is 40448, derived from age 20:

      20 → (4 = 2×2)(0 = 3×0)(4 = 4 − 0)(4 = 4 + 0)(8 = 4 + 4) = 40448.

      And 43178 − 40448 = 2730 = 130×21.


      In fact the only other ages that give viable phone numbers are:

      10 → 20224
      11 → 23156

      But in this case 23156 − 20224 = 2932 is not divisible by 11.

      Like

    • Frits's avatar

      Frits 3:51 pm on 5 September 2023 Permalink | Reply

       
      # formula for X9's telephone number
      x9 = lambda a, b: (20224 - (f := 2*a < 3*b) * 404) * a + (2730 + f * 606) * b
      
      # Assume that for the incorrect telephone number the same rules must appply.
      # This means that the 2nd digit of current age can't be 0, 
      # also 2nd digit of current age can't be 3 (then 4th digit > 9)
      
      for n in [11, 12, 21, 31]:
        d1, d2 = n // 10, n % 10 
        
        correct = x9(d1, d2)
        incorrect = x9(d1, d2 - 1)
        # sum of 2nd and 3rd digit must be less than 10
        if any(int(x[2]) + int(x[3]) > 9 for x in (str(correct), str(incorrect))):
          continue
        
        # difference in numbers is a multiple of the current age
        if (correct - incorrect) % n: continue
        print("answer:", correct)  
      

      Like

  • Unknown's avatar

    Jim Randell 6:57 am on 27 August 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 930: All scores different 

    From The Sunday Times, 18th May 1980 [link]

    The great thing about the new football method is that goals count whoever wins the match. With luck, this will create more excitement, and reward skills better than at present, for even a side losing heavily will have some incentive to attack.

    In this new method 10 points are given for a win, five points to each side for a draw, and one point for each goal scored.

    In a recent competition between four teams — A, B, C and D — who played each other once, the points were as follows:

    A = 25
    B = 34
    C = 6
    D = 20

    Each side scored at least one goal in each match, and — rather interestingly — the score was different in every match.

    Find the score in each match.

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

    The setter is given as “the late Eric Emmet”, and it is noted in the 1994 book that Eric Emmet had died by the time this puzzle was published.

    Eric Emmet also contributed the Puzzle series of puzzles to New Scientist, as well as several Enigma puzzles (including puzzles published as late as 1991).

    [teaser930]

     
    • Jim Randell's avatar

      Jim Randell 6:57 am on 27 August 2023 Permalink | Reply

      See also: Enigma 599.

      The total number of points awarded are: 25 + 34 + 6 + 20 = 85.

      There are 6 matches in the tournament, and 10 points are awarded depending on the match outcomes, accounting for 60 points in total.

      So there are 25 goals scored. Each side scored at least one goal in each match, so there are only 13 goals unaccounted for.

      In order to get different scores in each match we need to allow at least 4, but no more than 6 goals to be scored (per team) in any match.

      And we find a solution with up to 4 goals scored (per team) in a match.

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

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # matches are:
      #
      #  A vs B = E - F
      #  A vs C = G - H
      #  A vs D = I - J
      #  B vs C = K - L
      #  B vs D = M - N
      #  C vs D = P - Q
      
      SubstitutedExpression
      
      --distinct=""
      --digits="1-4"
      
      # points for X in match X vs Y, where the score is x - y
      --code="pts = lambda x, y: compare(x, y, (0, 5, 10)) + x"
      
      # points for A, B, C, D
      "pts(E, F) + pts(G, H) + pts(I, J) == 25"
      "pts(F, E) + pts(K, L) + pts(M, N) == 34"
      "pts(H, G) + pts(L, K) + pts(P, Q) ==  6"
      "pts(J, I) + pts(N, M) + pts(Q, P) == 20"
      
      # all matches have different scores
      "seq_all_different(ordered(x, y) for (x, y) in [(E, F), (G, H), (I, J), (K, L), (M, N), (P, Q)])"
      
      # output match scores
      --header=""
      --template="AvB = {E}-{F}; AvC = {G}-{H}; AvD = {I}-{J}; BvC = {K}-{L}; BvD = {M}-{N}; CvD = {P}-{Q}"
      --solution=""
      

      Solution: The scores are: A vs B = 2-2; A vs C = 2-1; A vs D = 1-1; B vs C = 4-3; B vs D = 3-1; C vs D = 2-3.

      Like

      • Frits's avatar

        Frits 1:01 pm on 28 August 2023 Permalink | Reply

        @Jim, please elaborate on “And we find a solution with up to 4 goals scored in a match”.

        Did you add it because –digits=”1-6″ was rather slow?

        Like

        • Jim Randell's avatar

          Jim Randell 7:51 am on 29 August 2023 Permalink | Reply

          Limiting the digits to a maximum of 4 is enough to find a solution, and it runs quickly.

          For an exhaustive search we can increase the maximum digit to 6. The runtime is increased, but it is still less than 1s.

          But we can limit the total number of goals in any match (it cannot be more than 7), to reduce the runtime again. It complicates the recipe a little, but gives a complete solution that still runs quickly.

          Like

    • Frits's avatar

      Frits 11:45 am on 30 August 2023 Permalink | Reply

      Based on Jim’s setup.

       
      # points for X in match X vs Y, where the score is x - y
      pts = lambda x, y: x + 1 if x < y else x + 6 if x == y else x + 11
      
      # decompose <t> into <k> numbers from <ns> so that sum(<k> numbers) equals <t>
      def solve(t, ns, k=12, s=[]):
        if k == 1:
          if t in ns:
            yield s + [t]
        else: # perform early checks
        
          # H, L, P are already known
          if k == 9 and sum(s) != 3: return  # C must have had three losses
          
          # H, L, P, F, K, M are already known
          if k == 6:                 
            if sum(s) != 9: return   # B didn't loose and had one draw
            if s[4] <= s[1]: return  # C lost from B
          
          # H, L, P, F, K, M, E, N are already known  
          if k == 4:                  
            # B: 2 wins and a draw so (F > E and M == N) or (F == E and M > N)
            if not((s[3] > s[6] and s[5] == s[7]) or 
                   (s[3] == s[6] and s[5] > s[7])): return
            # G > 0 and Q > 0  (and G + Q > 2)
            if sum(s) > 10: return
         
          # G > H and Q > P (and G + Q > 2)
          if k == 3 and s[-1] <= s[0]:  return 
          if k == 2 and (s[-1] <= s[2] or s[-2] + s[-1] <= 2):  return 
             
          for n in ns:
            if n > t: break
            yield from solve(t - n, ns, k - 1, s + [n])
            
      # there are 25 goals scored and only 13 goals unaccounted for   
      # B must have had two wins and one draw as otherwise one win and two draws
      # leads to two "1 - 2" losses for C (who had definitely had three losses)
      
      # matches are:
      #
      #  A vs B = 1 + E  -  1 + F
      #  A vs C = 1 + G  -  1 + H
      #  A vs D = 1 + I  -  1 + J
      #  B vs C = 1 + K  -  1 + L
      #  B vs D = 1 + M  -  1 + N
      #  C vs D = 1 + P  -  1 + Q
      
      # first unaccounted goals for C followed by unaccounted goals for B
      for H, L, P, F, K, M, E, N, G, Q, I, J in solve(13, range(6)):
        games = [(E, F), (G, H), (I, J), (K, L), (M, N), (P, Q)]
        # all matches have different scores
        if len(set(seq := [tuple(sorted(x)) for x in games])) != len(seq): continue
        
        # check number of points
        if pts(E, F) + pts(G, H) + pts(I, J) != 25: continue
        if pts(J, I) + pts(N, M) + pts(Q, P) != 20: continue
        if pts(J, I) + pts(N, M) + pts(Q, P) != 20: continue
        if pts(H, G) + pts(L, K) + pts(P, Q) !=  6: continue
        
        desc = "AvB AvC AvD BvC BvD CvD".split()
        # add 1 to scores
        scores = [desc[i] + " = " + str(games[i][0] + 1) + "-" + 
                  str(games[i][1] + 1) for i in range(6)]
        print(', '.join(scores))  
      

      Like

  • Unknown's avatar

    Jim Randell 2:09 pm on 24 August 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 922: Additional letters 

    From The Sunday Times, 23rd March 1980 [link]

    It is true, of course, that there are a lot of letters in this puzzle, but in spite of that I thought that for once Uncle Bungle was going to write it out correctly.

    In fact there was no mistake until the third and last line across but, in that line one of the letters, I’m afraid, was incorrect.

    This is another addition sum with letters substituted for digits. Each letter consistently stands for the same digit whenever it appears, and different letters stand for different digits — or at least the should do — and they do, but for the mistake in the last line across:

    What is the correct numerical 10-figure answer to the addition sum?

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

    [teaser922]

     
    • Jim Randell's avatar

      Jim Randell 2:09 pm on 24 August 2023 Permalink | Reply

      We can use [[ bungled_sum() ]] solver (as seen in Puzzle 56 etc.) for this puzzle.

      The following Python command line executes in 146ms.

      Run: [ @replit ]

      >>> from bungled_sum import bungled_sum
      >>> bungled_sum(['YTBBEDMKD', 'YHDBTYYDD', 'EDYTERTPTY'], [2])
      
                                      T
      YTBBEDMKD + YHDBTYYDD = EDYTERTPHY / @[2,8] T -> H
      695513243 + 673596633 = 1369109876 / B=5 D=3 E=1 H=7 K=4 M=2 P=8 R=0 T=9 Y=6
      

      Solution: The answer to the addition sum is 1369109876.

      The complete sum is:

      695513243 + 673596633 = 1369109876

      which would have been correctly posed as:

      YTBBEDMKD + YHDBTYYDD = EDYTERTPHY

      i.e. the penultimate (tens) digit of the result should have been H (not T).

      Like

  • Unknown's avatar

    Jim Randell 5:10 pm on 20 August 2023 Permalink | Reply
    Tags: by: Robert Eastaway   

    Brain-Teaser 917: Run ’em up 

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

    Last summer, for a pleasant holiday pastime with mathematical connections, I took up the job of operating our local cricket scoreboard at matches.

    This scoreboard is the envy of all the other teams in the league. It shows:

    – the two batsmen identified by their numbers in the batting order, with their individual totals;
    – the score (i.e. the number of runs and the wickets fallen);
    – the number of overs bowled;
    – the number of extras;
    – the score of the last man out.

    During one match, while the two batsmen were in full flight, our team declared their innings closed at the end of an over, with wickets to spare. Exhausted after a busy day, I examined the board and was amazed to see that all of the numbers recorded on it used only two different digits between them.

    I noted that the total was the only three-figure number; that there were four two-figure numbers; and that two single-figure numbers appeared twice. I also observed that the score of the last man out was a factor of the total, and the division of the latter by the former still resulted in a single figure number on the board.

    I was pleased to see that all the batsmen on the board reached double figures.

    What was the final score, i.e. how many runs for how many wickets?

    How many extras were there?

    [In cricket, the batsmen are numbered from 1 upwards as they come in to bat. The world record is 77 runs in one over. The match itself was perfectly normal — no-one retired hurt etc. Ed.]

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

    [teaser917]

     
    • Jim Randell's avatar

      Jim Randell 5:11 pm on 20 August 2023 Permalink | Reply

      We start with batsmen 1 and 2 playing, and at this point there would be 0 wickets taken (and no last man out).

      When one of them is out, batsman 3 comes in to play, and there is 1 wicket taken. And when another batsman is dismissed, there are 2 wickets taken, and batsman 4 comes in to play. And so on.

      So, if w is the number of wickets taken, then the most recent of the current batsmen must be number (w + 2).

      This Python program considers possible numbers for the current batsman, and uses these along with the number of wickets taken to find the 2 available digits. It then looks for possible 3-digit numbers using those digits that can be divided by one of digits to give a 2-digit number composed of the available digits (and this is the score of the last man out).

      This is enough to get us the score in the match, and deduce the number of extras.

      This Python program runs in 51ms. (Internal runtime is 457µs).

      Run: [ @replit ]

      from enigma import (irange, subsets, nsplit, union, nconcat, div, printf)
      
      # construct <k> digit numbers from digits <ds>
      construct = lambda ds, k: subsets(ds, size=k, select="M", fn=nconcat)
      
      # consider the numbers of the batsmen on the board
      for (a, b) in subsets(irange(1, 11), size=2):
      
        # and number of wickets fallen
        w = b - 2
        if w < 1: continue
      
        # calculate digits used
        ds = union(nsplit(x) for x in (a, b, w))
        if len(ds) > 2: continue
        # suppose both available digits are determined
        assert len(ds) == 2
      
        # choose a 3-digit total score
        for t in construct(ds, 3):
          # divisible by one of the digits
          for d in ds:
            # to give the score of the last man out
            z = div(t, d)
            # which is a 2 digit number, composed of the required digits
            if z is None or z < 10 or z > 99: continue
            if not ds.issuperset(nsplit(z)): continue
      
            # output solution
            printf("score = {t} for {w} [a={a} b={b} -> w={w} ds={ds} t={t} z={z}]")
      

      Solution: (i) The score in the match was: 688 for 6.

      We have:

      batsman = 6, runs = {66, 68, 86, 88}
      batsman = 8, runs = {66, 68, 86, 88}
      score = 688 for 6
      overs = __
      extras = __
      last man out = 86

      There is a single digit value of 8 to be filled out in one of the gaps (and the other is a 2-digit number (which must be 66, 68, 86, 88)).

      The most likely scenario is that the 688 runs have been scored off more than 8 overs (= 64 balls), so the number of extras is 8.

      (Also, we wouldn’t be able to determine what the number of extras was if it was the missing 2-digit number).

      Solution: (ii) There were 8 extras.


      In the book The Sunday Times Book of Brainteasers (1994), the additional information that: “The world record is 77 runs in one over” is given.

      Presuming the record was not broken during this match, that would give a maximum of 616 runs in 8 overs, so there must be more than 8 overs. (But as noted above, if the number of extras were a 2 digit number, we could not determine it).

      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