Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 7:15 am on 14 October 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 879: Seven criminals 

    From The Sunday Times, 11th June 1978 [link]

    The instructor at the police training college spoke to the six constables in his class in these words:

    “You have been studying full-face photographs of seven criminals whom we are calling P, Q, R, S, T, U and V. Now l am going to show you one photograph, taken in profile, of each criminal, and you have to write down their names in the
    order in which I present them.”

    This was done and the constables handed in the following
    six answers:

    1. P Q R S T U V
    2. P Q R U T S V
    3. P S U V R T Q
    4. P S Q U R T V
    5. P U R V T S Q
    6. R P U Q T S V

    “I am pleased to see that each criminal has been correctly identified by at least one of you”, said the instructor. “And I note that you all have a different number of correct answers and so I can give out the prizes”.

    In what order were the photographs presented?

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

    [teaser879]

     
    • Jim Randell's avatar

      Jim Randell 7:16 am on 14 October 2021 Permalink | Reply

      It is straightforward to check all possible arrangements.

      This Python program runs in 87ms.

      Run: [ @replit ]

      from enigma import subsets, seq_all_different, join, printf
      
      # the six answers
      ans = [
        'PQRSTUV',
        'PQRUTSV',
        'PSUVRTQ',
        'PSQURTV',
        'PURVTSQ',
        'RPUQTSV',
      ]
      
      # count number of items in correct position
      correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
      
      # consider possible orderings
      for ss in subsets('PQRSTUV', size=len, select="P"):
        # how many are in the correct position?
        pos = list(correct(xs, ss) for xs in ans)
        if not seq_all_different(pos): continue
        # check each item is correct in one of the answers
        if not all(any(xs[i] == x for xs in ans) for (i, x) in enumerate(ss)): continue
        # output solution
        printf("{ss} -> {pos}", ss=join(ss, sep=" ", enc="()"))
      

      Solution: The photographs were presented in the following order: R, P, U, V, T, S, Q.

      And the number of photographs correctly identified by the constables were: 1, 2, 3, 0, 4, 5.

      There is only a single solution without the condition that each criminal was identified correctly by (at least) one of the constables. But we see that the last constable got Q and V wrong (and the rest right), but the penultimate constable got these two right, so each miscreant was identified correctly by one of the constables.

      Like

    • Frits's avatar

      Frits 10:12 am on 15 October 2021 Permalink | Reply

      Borrowing from above program and the solve() function in Teaser 3080 (One of a kind).
      The program runs in 2ms.

        
      # find members of each set, such that each member is used exactly once
      def solve(ss, ns=[], ds=set()):
        # are we done?
        if not(ss):
          yield ns
        else:
          # choose an element from the next set
          for n in ss[0]:
            if not(ds.intersection(n)):
              yield from solve(ss[1:], ns + [n], ds.union(n))
      
      # count number of items in correct position
      correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
      
      # the six answers
      ans = [
        'PQRSTUV',
        'PQRUTSV',
        'PSUVRTQ',
        'PSQURTV',
        'PURVTSQ',
        'RPUQTSV',
      ]
      
      nr_criminals = len(ans[0])
      nr_answers = len(ans)
      
      # make list of used values
      lst = [set(a[i] for a in ans) for i in range(nr_criminals)]
      
      for ss in solve(lst):
        pos = set(correct(xs, ss) for xs in ans)
        if len(pos) != nr_answers: continue
        
        print(f"{', '.join(ss)} --> {pos}")
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:57 pm on 15 October 2021 Permalink | Reply

        You can use the following to turn a collection of rows into the corresponding collection of columns:

        cols = zip(*rows)
        

        (And [[ unzip() ]] is an alias for this in enigma.py).

        We can also simplify the way the [[ solve() ]] function operates. (I’ve renamed it [[ select() ]]).

        from enigma import unzip, seq_all_different, join, printf
        
        # the six answers
        ans = [
          'PQRSTUV',
          'PQRUTSV',
          'PSUVRTQ',
          'PSQURTV',
          'PURVTSQ',
          'RPUQTSV',
        ]
        
        # select distinct elements from each set in list of sets <ss>
        def select(ss, rs=[]):
          # are we done?
          if not ss:
            yield rs
          else:
            for x in ss[0].difference(rs):
              yield from select(ss[1:], rs + [x])
        
        # count number of items in correct position
        correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
        
        # each photograph was identified correctly in at least one answer
        # so the the correct value occurs in each column
        
        # consider possible correct orderings
        for ss in select(list(set(col) for col in unzip(ans))):
          # how many are in the correct position?
          pos = list(correct(xs, ss) for xs in ans)
          if not seq_all_different(pos): continue
          # output solution
          printf("{ss} -> {pos}", ss=join(ss, sep=" ", enc="()"))
        

        I did try a version of [[ select() ]] that chooses from the column with the smallest number of values available, but it is no advantage on a problem of this size.

        Like

  • Unknown's avatar

    Jim Randell 10:11 am on 12 October 2021 Permalink | Reply
    Tags:   

    Teaser 2830: Time for Christmas 

    From The Sunday Times, 18th December 2016 [link] [link]

    For Christmas George has bought Martha a novelty digital 24-hour clock. It has an eight-digit display forming four two-digit numbers. The first three of these two-digit numbers are very conventional – the hours, the minutes and the seconds. However, the final two-digit display shows the sum of the first six displayed digits!

    On two occasions today all eight digits displayed were different and three of the four two-digit numbers were perfect squares. Between those two occasions there was a time when the eight digits displayed were again all different, but this time the sum of the eight digits was a perfect square!

    What was the eight-digit display at that in-between time?

    [teaser2830]

     
    • Jim Randell's avatar

      Jim Randell 10:12 am on 12 October 2021 Permalink | Reply

      If we accept that there are only “two occasions” in any given day, then we don’t need to consider all the times, just look until we find the second condition (p2) occurring between the two occurrences of the first condition (p1).

      This code stops when it finds the second occurrence of the first condition and runs in 61ms.

      from enigma import (irange, flatten, cproduct, printf)
      
      # output format: <hour>:<minute>:<second>:<sum> [<state>]
      fmt = "{h:02d}:{m:02d}:{s:02d}:{t:02d} [{state}]"
      
      # 2 digit squares
      squares = set(i * i for i in irange(0, 8))
      
      # initial state
      state = 0
      
      # consider times in order
      for (h, m, s) in cproduct([irange(0, 23), irange(0, 59), irange(0, 59)]):
        # collect digits
        digits = flatten((divmod(x, 10) for x in (h, m, s)), fn=list)
        # add in the sum
        t = sum(digits)
        digits.extend(divmod(t, 10))
        # all digits are different
        if len(set(digits)) != 8: continue
      
        # mark special states
        # p1 = "three of the four two digits numbers are perfect squares"
        p1 = (len(squares.intersection((h, m, s, t))) == 3)
        # p2 = "the sum of all 8 digits is a perfect square"
        p2 = (sum(digits) in squares)
        if not (p1 or p2): continue
      
        # if we're in the initial state, wait until we see a p1
        if state == 0:
          if p1:
            printf(fmt, state='first')
            state = 1
      
        # if we've seen the first p1
        elif state == 1:
          # look for the second p1
          if p1:
            printf(fmt, state='second')
            # and we are done
            break
          # or for an intermediate p2
          elif p2:
            printf(fmt, state='in-between')
      

      Solution: The display at the in-between time was 01:48:59:27.

      The times are:

      01:38:49:25 [first]
      01:48:59:27 [in-between]
      01:49:38:25 [second]

      Like

    • Jim Olson's avatar

      Jim Olson 5:24 pm on 13 October 2021 Permalink | Reply

      Am I missing something but the middle digits sum to 54 which is not a perfect square.

      Like

      • Jim Randell's avatar

        Jim Randell 5:32 pm on 13 October 2021 Permalink | Reply

        At the in-between time the sum of the 8 digits is:

        0+1+4+8+5+9+2+7 = 36 = 6²

        Like

    • Jim Olson's avatar

      Jim Olson 5:51 pm on 13 October 2021 Permalink | Reply

      Thank you I was adding the number 27 instead of the digit sum of 9.

      Like

  • Unknown's avatar

    Jim Randell 4:39 pm on 8 October 2021 Permalink | Reply
    Tags:   

    Teaser 3081: Connect four 

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

    I have four different two-digit numbers, each having at least one digit which is a three. When I multiply any three of these numbers together I get a product that, with the inclusion of a leading zero, is one or more repetitions of the repetend of the reciprocal of the fourth two-digit number. A repetend is the repeating or recurring decimal of a number. For example 1 divided by 27 is 0.037037…, giving a repetend of 037; in that case, the product would be 37 or 37037 or 37037037 etc.

    What, in ascending order, are the four two-digit numbers?

    [teaser3081]

     
    • Jim Randell's avatar

      Jim Randell 4:59 pm on 8 October 2021 Permalink | Reply

      This Python program uses the [[ recurring() ]] function from the enigma.py library to calculate the repetends. It runs in 51ms.

      Run: [ @replit ]

      from enigma import (enigma, irange, nsplit, subsets, multiply, format_recurring, printf)
      
      # find the repetend of 1/n (recurring part of the recurring decimal representation)
      recurring = enigma.cache(lambda n: enigma.recurring(1, n, recur=1))
      
      # consider 2-digit numbers containing the digit 3
      ns = list(n for n in irange(10, 99) if 3 in nsplit(n))
      
      # check this set of numbers
      def check(ss, verbose=0):
        m = multiply(ss)
        for n in ss:
          # calculate the product
          p = str(m // n)
          # find the repetend (rr)
          (i, nr, rr) = recurring(n)
          # if the repetend has leading zeros extend the product
          for d in rr:
            if d != '0': break
            p = '0' + p
          # remove copies of the repetend from the product
          k = len(rr)
          while p.startswith(rr):
            p = p[k:]
          # anything left?
          if p: return False
          if verbose: printf("-> 1/{n} = {f}; product = {p}", f=format_recurring((i, nr, rr)), p=m // n)
        # if we get this far we've done it
        return True
      
      # select 4 of the numbers
      for ss in subsets(ns, size=4):
        if check(ss):
          printf("{ss}:")
          check(ss, verbose=1)
          printf()
      

      Solution: The four numbers are: 13, 33, 37, 63.


      Without the requirement that each of the 2-digit number must contain at least one 3, we can find further sets:

      (11, 27, 37, 91)
      (11, 37, 39, 63)
      (13, 21, 37, 99)
      (13, 27, 37, 77)
      (13, 33, 37, 63) [solution to the puzzle]
      (21, 33, 37, 39)

      All these sets have a product of 999999.

      And this is a consequence of the fact that for any fraction f ≤ 1, if a k-length repdigit of 9, multiplied by f gives an integer result, then that result is a k-length repetend of f (suitably padded with leading zeros, and there will be no non-repeating part of the decimal expansion).

      So any set of numbers that multiplies to a 9-repdigit will form such a set.

      For example, multiplying the numbers in the solution to give pairs:

      999999 = (13×33)×(37×63) = 429 × 2331: 1/429 = 0.(002331)…, 1/2331 = 0.(000429)…
      999999 = (13×37)×(33×63) = 481 × 2079: 1/481 = 0.(002079)…, 1/2079 = 0.(000481)…
      999999 = (13×63)×(33×37) = 819 × 1221: 1/819 = 0.(001221)…, 1/1221 = 0.(000819)…

      Or:

      9999999 = 9 × 239 × 4649:
      1/9 = 0.(1)…; 239 x 469 = 1111111
      1/239 = 0.(0041841)…; 9 × 4649 = 41841
      1/4649 = 0.(0002151)…; 9 × 239 = 2151

      Programatically we can easily find 4-sets from the candidates that multiply together to give a 9-repdigit:

      from enigma import (irange, nsplit, subsets, multiply, seq_all_same, printf)
      
      # candidate numbers (can't be divisible by 2 or 5)
      ns = list(n for n in irange(10, 99) if 3 in nsplit(n) and n % 2 and n % 5)
      
      # look for 4-subsets that multiply to a 9-repdigit
      for ss in subsets(ns, size=4):
        p = multiply(ss)
        if seq_all_same(nsplit(p), value=9):
          printf("{ss}; product={p}")
      

      Which we could then pass to the [[ check() ]] function above to verify the solution and output information for each number.

      But there are sets that don’t have a 9-repdigit product, and do have decimal expansions with non-repeating parts, e.g.:

      9990 = 54 × 185
      1/54 = 0.0(185)…
      1/185 = 0.0(054)

      (Although I haven’t found any that aren’t a 9-repdigit multiplied by a power of 10).

      But I did find that the four 2-digit numbers {28, 74, 78, 99} have the following property:

      1/28 = 0.3(571428)…; 74 × 78 × 99 = 571428

      Unfortunately it doesn’t work for the reciprocals of the other terms, but I did notice:

      1/78 = 0.0(128205)… = 0.0128(205128)…; 28 × 74 × 99 = 205128

      Like

      • Jim Randell's avatar

        Jim Randell 11:15 pm on 8 October 2021 Permalink | Reply

        And here is a faster (but slightly longer) version, using the same [[ check() ]] function, that avoids looking at all possible 4-subsets of the candidate numbers, by discarding those with a repetend that is larger than the largest possible product.

        It runs in 48ms (and the internal runtime is 884µs).

        Run: [ @replit ]

        from enigma import (enigma, irange, nsplit, multiply, subsets, format_recurring, printf)
        
        # find the repetend of 1/n (recurring part of the recurring decimal representation)
        recurring = enigma.cached(lambda n: enigma.recurring(1, n, recur=1))
        
        # consider 2-digit numbers containing the digit 3
        ns = list(n for n in irange(10, 99) if 3 in nsplit(n))
        
        # map numbers to repetends
        # [[ and rr[0] == '0' ]] to require repetend has a leading zero
        # [[ and (not nr) ]] to require no non-repeating part
        rep = dict((n, rr) for (n, (i, nr, rr)) in ((n, recurring(n)) for n in ns) if rr)
        
        # remove any numbers with repetends that are too big
        while True:
          M = multiply(ns[-3:])
          ks = list()
          for (k, v) in rep.items():
            if int(v) > M: ks.append(k)
          if not ks: break
          for k in ks: del rep[k]
          ns = sorted(rep.keys())
        
        # check a set of numbers
        def check(ss, verbose=0):
          m = multiply(ss)
          if verbose: printf("{ss}: product = {m}")
          for n in ss:
            # calculate the product
            p = str(m // n)
            # find the repetend (rr)
            rr = rep[n]
            # if the repetend has leading zeros extend the product
            for d in rr:
              if d != '0': break
              p = '0' + p
            # remove copies of the repetend from the product
            k = len(rr)
            while p.startswith(rr):
              p = p[k:]
            # anything left?
            if p: return False
            if verbose: printf("-> 1/{n} = {f}; product = {p}", f=format_recurring(*recurring(n)), p=m // n)
          # if we get this far we've done it
          return True
        
        # select 4 of the numbers
        for ss in subsets(ns, size=4):
          if check(ss):
            check(ss, verbose=1)
            printf()
        

        Like

    • GeoffR's avatar

      GeoffR 10:35 pm on 8 October 2021 Permalink | Reply

      Instead of looking for repeating digit patterns, I thought I would try equating sets of digits in the multiplication of the three two-digit numbers with the digits in the reciprocal of the fourth two-digit number, Not a conventional approach, but it seems to work.

      A set length of nine for reciprocals/multiplications proved adequate to capture the repeating digits patterns for this set comparison method. I omitted the leading zero and decimal point for reciprocal digits set to compare them with multiplication digits. I found the full length decimal reciprocal could give a last digit which was not part of the general repeating pattern.

      from enigma import irange, nsplit
      from itertools import combinations
      
      N4 = []  # list for the groups of four two-digit numbers
      
      # consider 2-digit numbers containing the digit 3
      ns = list(n for n in irange(10, 99) if 3 in nsplit(n))
      
      for p in combinations(ns, 4):
        ab, cd, ef, gh = p
        # check 1st tuple of three numbers and the reciprocal
        s1 = set(str(ab * cd * ef)[:9]).difference(set(['0']))
        r1 = set(str(1 / gh)[:9]).difference(set(['.', '0']))
        if s1 == r1:
          # check 2nd tuple of three numbers and the reciprocal
          s2 = set(str(ab * cd * gh)[:9]).difference(set(['0']))
          r2 = set(str(1 / ef)[:9]).difference(set(['.', '0']))
          if s2 == r2:
            # check 3rd tuple of three numbers and the reciprocal
            s3 = set(str(ab * ef * gh)[:9]).difference(set(['0']))
            r3 = set(str(1 / cd)[:9]).difference(set(['.', '0']))
            if s3 == r3:
              # check 4th tuple of three numbers and the reciprocal
              s4 = set(str(cd * ef * gh)[:9]).difference(set(['0']))
              r4 = set(str(1 / ab)[:9]).difference(set(['.', '0']))
              if s4 == r4:
                L = sorted([ab, cd, ef, gh])
                if L not in N4:
                  N4.append(L)
      
      if len(N4) == 1:
        print(f"Four two digit numbers are {N4[0]}")
      
      
      

      Like

    • Frits's avatar

      Frits 5:09 pm on 9 October 2021 Permalink | Reply

      from enigma import divisors
      
      # consider 2-digit numbers containing the digit 3
      nbrs = list(n for n in range(10, 100) if '3' in str(n))
        
      # retrieve repeating decimals of 1 / n
      def repdec(n):
        c = 100
        dgts = ""
        done = dict()
        i = 0
        while True:
          if c in done:
            rep = dgts[done[c]:]
            if rep[-1] == '0':
              rep = '0' + rep[:-1]
            return rep
          done[c] = i
          q, r = divmod(c, n)
          dgts += str(q)
          c = 10 * r
          i += 1   
         
      # decompose number <t> into <k> numbers from <ns>
      # so that product of <k> numbers equals <t>
      def decompose(t, k, ns, s=[]):
        if k == 1:
          if t in ns and t >= s[-1]:
            yield s + [t]
        else:
          for n in ns:
            if n not in s and (not s or n >= s[-1]):
              yield from decompose(t // n, k - 1, ns.difference([n]), s + [n])
      
      d = dict()
      for n in nbrs:
        rep = repdec(n)
        lngth = len(rep)
        if lngth > 7 or rep == 0: continue 
        r = rep
       
        # extend up to 6 digits
        for _ in range(6 // lngth):
          prod = int(r)
         
          for _ in range(1):     # dummy loop used for breaking
            # 13 * 23 * 30 <= product <= 73 * 83 * 93 
            if not(8970 <= prod <= 563487): break 
           
            # collect all divisors of product
            divs = [x for x in divisors(prod) if x in nbrs]
           
            # skip if less than 3 valid divisors
            if len(divs) < 3: break
           
            # check if product of 3 numbers equals <prod>
            for decomp in decompose(prod, 3, set(divs)):
              n4 = tuple(sorted([n] + decomp))
              d[n4] = d.get(n4, 0) + 1
        
          r += rep  # extend up to 6 digits
      
      # sets of four 2-digit numbers must have been encountered four times
      for k, v in d.items():
        if v == 4:
          print(f"answer: {k}")
       

      Like

      • Frits's avatar

        Frits 11:15 am on 11 October 2021 Permalink | Reply

        or with a recursive function:

          
        # https://codegolf.stackexchange.com/questions/78850/find-the-repetend-of-the-decimal-representation    
        def replist(n, t=1, ns=[], *d):
          return ns[(*d, t).index(t):] or replist(n, (t % n) * 10, ns + [t // n], *d, t)   
         
        # retrieve repeating decimals of 1 / n 
        def repdec(n):
          return "".join(str(x) for x in replist(n)) 
        

        Like

        • Jim Randell's avatar

          Jim Randell 12:34 pm on 11 October 2021 Permalink | Reply

          It’s certainly short, although not necessarily very readable. But it does produce the right answer for positive reciprocals with a non-terminating decimal expansion.

          The puzzle text isn’t completely clear what the intended repetend is for fractions with a normally terminating decimal expansion. You could argue that in this case there is no repetend, and these numbers can be discarded.

          But I prefer the definition that the repetend is the shortest and earliest repeating section of the non-terminating decimal representation of the fraction.

          As every non-zero fraction has a unique non-terminating decimal expansion, this is well-defined.

          And it just so happens it is what the [[ recurring() ]] function returns when [[ recur=1 ]] is set. (The [[ recurring() ]] function was originally written for Enigma 1247, but has proved useful in several other Enigma puzzles).

          So, for example, we have (with repetends in parentheses):

          1/3 = 0.(3)…
          1/1 = 0.(9)…
          1/27 = 0.(037)…
          1/28 = 0.3(571428)…
          1/32 = 0.03124(9)…

          But, it does seem to be possible to solve this puzzle with a less well defined version of “repetend”.

          Like

    • Alex. T,Sutherland's avatar

      Alex. T,Sutherland 1:59 pm on 10 October 2021 Permalink | Reply

      Each of the answer numbers must have a repeatable characteristic.
      (a) Number of possible x3,3x numbers =17. (b) Only 6 from these 17 are repeatable.
      (c) Evaluate any 4 from these 6 (15 possibles) to satisfy the constraints.Ony one solution.
      The sum of the solution is between 140-150. Run time 25ms.

      Like

      • Jim Randell's avatar

        Jim Randell 6:41 pm on 10 October 2021 Permalink | Reply

        @Alex: I found that 8 of the numbers had viable repetends. (i.e. ones where a single repeat was not larger than the largest possible product of three numbers). Or 7 if we exclude numbers with a terminating decimal representation.

        Like

        • Tony Brooke-Taylor's avatar

          Tony Brooke-Taylor 9:50 am on 12 October 2021 Permalink | Reply

          Jim I think if you add the requirement for a leading zero you can reduce this number to five possibilities. I’d be curious to compare my 5 with Alex’s 6 and your 7, once the solution embargo is lifted.

          Like

          • Jim Randell's avatar

            Jim Randell 9:57 am on 12 October 2021 Permalink | Reply

            @Tony: Yes. If you require a leading zero in the repetend, then this reduces the number of candidates to 5. (I took the “inclusion of a leading zero” to be “(where necessary)”, rather than a strict requirement).

            Reducing the set of candidates to 5, means there are only 5 sets of 4 numbers to check.

            In fact multiplying the (numerically) first 3 numbers together, gives the repetend of one of the other 2, so the answer drops out immediately. All that’s left is to check the remaining combinations work as expected. A very simple manual solution.

            Like

    • Jim Olson's avatar

      Jim Olson 4:12 pm on 11 October 2021 Permalink | Reply

      Jim is your EnigmaticCode site down temporarily?

      Like

      • Jim Randell's avatar

        Jim Randell 4:15 pm on 11 October 2021 Permalink | Reply

        Unfortunately, yes.

        It seems WordPress.com suspended the site without warning earlier today. I have contacted them to get it reinstated.

        Hopefully normal service will be resumed before too long.

        Like

    • Jim Randell's avatar

      Jim Randell 6:06 pm on 12 October 2021 Permalink | Reply

      So here’s an interesting fact, repetend fans:

      The repetend of the reciprocal of the square of the k-length 9-repdigit, consists of the concatenation of all the k-length numbers starting from zero, in order, up to the k-length 9-repdigit itself, except that the penultimate k-length number is missing.

      (And there are corresponding results for other bases).

      Let’s see:

      >>> format_recurring(recurring(1, sq(9)))
      '0.(012345679)...'
      
      >>> format_recurring(recurring(1, sq(99)))
      '0.(0001020304050607080910111213141516171819
          2021222324252627282930313233343536373839
          4041424344454647484950515253545556575859
          6061626364656667686970717273747576777879
          80818283848586878889909192939495969799)...'
      
      >>> format_recurring(recurring(1, sq(999)))
      '0.(000001002003004005006007008009010011012013014015016017018019
          020021022023024025026027028029030031032033034035036037038039
          ...
          980981982983984985986987988989990991992993994995996997999)...'
      

      And if you want to know where that penultimate term has gone, here is Numberphile to explain it all [@youtube].

      Like

  • Unknown's avatar

    Jim Randell 9:16 am on 7 October 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 851: Hay fever foiled 

    From The Sunday Times, 6th November 1977 [link]

    Our garden has paths as shown in the plan above. At each junction of paths there is a different variety of plant. The shortest walk (along the paths) from the Marigold to the Sneezewort passes only the Tickseed, and the shortest walk from the Nasturtium to the Quitch passes two plants, one of which is the Lobelia.

    My wife suffers from hay fever and so she never walks past the irritating Rosemary, Sneezewort or Tickseed. We are still able to walk together on the shortest route from the Polyanthus to the Nasturtium (past one other plant), but she has to walk over twice as far as in going from the Orpine to the Marigold.

    List the plants, in order, which my wife passes when taking her shortest route from the Nasturtium to the Marigold.

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

    [teaser851]

     
    • Jim Randell's avatar

      Jim Randell 9:17 am on 7 October 2021 Permalink | Reply

      This Python program runs in 144ms.

      Run: [ @replit ]

      from enigma import (empty, Accumulator, ordered, tuples, SubstitutedExpression, irange, translate, join, cached, printf)
      
      # adjacency matrix
      adj = [
        [1, 3, 4], # 0
        [0, 2, 5], # 1
        [1, 3, 6], # 2
        [0, 2, 7], # 3
        [0, 5, 7, 8], # 4
        [1, 4, 6, 8], # 5
        [2, 5, 7, 8], # 6
        [3, 4, 6, 8], # 7
        [4, 5, 6, 7], # 8
      ]
      
      # distances for each edge
      dist = {
        (0, 1): 314, (1, 2): 314, (2, 3): 314, (0, 3): 314, # = 100 pi
        (0, 4): 100, (1, 5): 100, (2, 6): 100, (3, 7): 100,
        (4, 5): 157, (5, 6): 157, (6, 7): 157, (4, 7): 157, # = 50 pi
        (4, 8): 100, (5, 8): 100, (6, 8): 100, (7, 8): 100,
      }
      
      # find paths from A to B, avoiding Xs
      def paths(A, B, Xs=empty, s=[]):
        if A == B:
          yield s
        else:
          # move forward from A
          for x in adj[A]:
            if x not in s and x not in Xs:
              yield from paths(x, B, Xs, s + [x])
      
      # find minimal distance paths
      @cached
      def min_path(A, B, Xs=empty):
        r = Accumulator(fn=min, collect=1)
        for s in paths(A, B, Xs, [A]):
          d = sum(dist[ordered(x, y)] for (x, y) in tuples(s, 2))
          r.accumulate_data(d, s)
        return r
      
      # is there a minimal path with n intermediates, that include incs?
      def check(A, B, Xs, n=None, incs=[]):
        incs = set(incs)
        r = min_path(A, B, frozenset(Xs))
        for p in r.data:
          if len(p) == n + 2 and incs.issubset(p): return True
        return False
      
      # check distance avoiding Xs is over twice as far
      def check2(A, B, Xs):
        r1 = min_path(A, B)
        r2 = min_path(A, B, frozenset(Xs))
        return (r2.value > 2 * r1.value)
      
      # make a solver
      p = SubstitutedExpression(
        [
          # M -> S passes only T
          "check(M, S, [], 1, [T])",
          # N -> Q passes L and 1 other
          "check(N, Q, [], 2, [L])",
          # P -> N passes 1 other plant
          "check(P, N, [R, S, T], 1)",
          # but she has to walk over twice as far going from O -> M
          "check2(O, M, [R, S, T])",
        ],
        digits=irange(0, 8),
        env=dict(check=check, check2=check2),
        verbose=0,
      )
      
      # run the solver
      for s in p.solve():
        # map positions to symbols
        m = dict((v, k) for (k, v) in s.items())
        # remove duplicate layouts
        if not (m[1] < m[3] and m[0] < min(m[1], m[2], m[3])): continue
      
        # output solution: (outer) (inner) (centre) -> wife's path from N to M
        printf("{m}", m=translate("({0} {1} {2} {3}) ({4} {5} {6} {7}) ({8})", (lambda x: m[int(x)])))
        r = min_path(s['N'], s['M'], frozenset([s['R'], s['S'], s['T']]))
        for p in r.data:
          printf("-> {p}", p=join((m[x] for x in p), sep=" "))
        printf()
      

      Solution: The plants passed are: Orpine, Polyanthus, Quitch.


      Here is a layout of the plants (reflections/rotations of this layout also work), with the wife’s best route from N to M indicated:

      Like

      • Frits's avatar

        Frits 11:39 am on 7 October 2021 Permalink | Reply

        @Jim, I would say there is no solution as normally “twice as far” would mean twice the distance, not at least twice the distance.

        Like

        • Jim Randell's avatar

          Jim Randell 11:50 am on 7 October 2021 Permalink | Reply

          @Frits: the puzzle text says “she has to walk over twice as far”, so the wife has to travel more than twice the distance of the shortest path.

          Like

  • Unknown's avatar

    Jim Randell 9:16 am on 5 October 2021 Permalink | Reply
    Tags:   

    Teaser 2828: Return to Zenda 

    From The Sunday Times, 4th December 2016 [link] [link]

    Each postcode in Ruritania consists of ten digits, the first three showing the area, the next three showing the town, and the final four showing the house. Just twelve houses have “lucky” postcodes that have no repeated digit and which consist of two three-figure squares followed by a four-figure square. Rudolph lives in such a house and he recently met a girl called Flavia. She only told him that hers was the only house in her area with a lucky postcode, and that her area/town/house numbers were all bigger than his. He has found a postcode satisfying these conditions and sent her a Christmas card, but it has been returned as it was not her postcode.

    What is Rudolph’s postcode?

    [teaser2828]

     
    • Jim Randell's avatar

      Jim Randell 9:16 am on 5 October 2021 Permalink | Reply

      I think the wording of this puzzle could be a bit clearer. But with a reasonable assumption that leading zeros are not allowed in the squares we get a unique solution.

      This Python program runs in 54ms.

      Run: [ @replit ]

      from enigma import (irange, is_duplicate, cproduct, subsets, filter_unique, unpack, join, printf)
      
      # find n-digit squares from a^2 to b^2 with no repeated digits (as
      # strings) leading zeros are allowed
      def squares(n, a, b):
        r = list()
        for i in irange(a, b):
          s = str(i * i).zfill(n)
          if not is_duplicate(s):
            r.append(s)
        return r
      
      # 3 and 4 digit squares
      s3s = squares(3, 10, 31)
      s4s = squares(4, 32, 99)
      
      # collect "lucky" (pandigital) postcodes
      lucky = list((area, town, house)
        for (area, town, house) in cproduct([s3s, s3s, s4s])
          if len(set(area + town + house)) == 10)
      printf("[{n} lucky postcodes]", n=len(lucky))
      
      # choose the 12 lucky postcodes
      for postcodes in subsets(lucky, size=12):
      
        # flavia's postcode is the only (possible) lucky postcode in the area
        fs = filter_unique(postcodes, unpack(lambda a, t, h: a)).unique
      
        # consider rudolph's postcode
        for (ra, rt, rh) in postcodes:
          # find possible postcodes for flavia
          f = list((a, t, h) for (a, t, h) in fs if a > ra and t > rt and h > rh)
          # and there must be more than one possibility
          if len(f) > 1:
            fmt = lambda s: join(s, sep=":", enc="()")
            printf("{r} -> {f}",r=fmt((ra, rt, rh)), f=join(map(fmt, f), sep=' '))
      

      Solution: Rudolph’s postcode is (324:576:1089).

      And there are two possible postcodes for Flavia: (961:784:3025) and (361:784:9025). So Rudolph chose the wrong one.

      We can allow the squares to have leading zeros by changing the value of the [[ a ]] parameter in the calls to [[ squares() ]]. But if we do this we find that are many possible solutions.


      The 12 “lucky” postcodes (area:town:house) are:

      (169:784:3025)
      (196:784:3025)
      (324:576:1089)
      (324:576:9801)
      (361:784:9025)
      (576:324:1089)
      (576:324:9801)
      (784:169:3025)
      (784:196:3025)
      (784:361:9025)
      (784:961:3025)
      (961:784:3025)

      Flavia’s postcode is unique by area:

      (169:784:3025)
      (196:784:3025)
      (361:784:9025)
      (961:784:3025)

      And Rudolph’s postcode must be one of the 12 lucky code above, that has two candidate postcodes for Flavia that are larger in each component. There is only one possibility for Rudolph’s postcode:

      (324:576:1089) → (361:784:9025) or (961:784:3025)


      Without leading zeros there are exactly 12 lucky postcodes, so we know all possible lucky postcodes are in use. (And this is probably the scenario the setter had in mind).

      However, if we allow leading zeros in the squares we can find 34 lucky postcodes, but we know only 12 of them are in use. But how does Flavia know that hers is the only lucky postcode in her area? Is it the only possible lucky postcode in her area? Or just the only one currently in use? In my program I have implemented the latter.

      And using this interpretation we find there are many thousands of situations that lead to a solution, with 6 possible values for Rudolph’s postcode. So I think it is probably safe to assume that the setter intended there to be no leading zeros in the squares.

      Like

      • Frits's avatar

        Frits 11:49 am on 9 June 2025 Permalink | Reply

        or more efficient

        # collect "lucky" (pandigital) postcodes
        lucky = list()
        for (area, house) in product(s3s, s4s):
          if len(ah := set(area + house)) == 7:
            for town in s3s:
              if ah.isdisjoint(town):
                lucky.append((area, town, house))
        

        Like

    • Frits's avatar

      Frits 11:53 am on 5 October 2021 Permalink | Reply

      Checking lucky postcodes outside “SubstitutedExpression” would have been easier and faster but this was more challenging.

        
      from enigma import SubstitutedExpression
      
      # the alphametic puzzle 
      p = SubstitutedExpression(
        [# Rudolph
         "is_square(ABC)",
         "is_square(DEF)",
         "is_square(GHIJ)",
         
         # does more than one Flavia candidate exist for Rudolph (ABC:DEF:GHIJ) ? 
         # Flavia's postcode (a:b:c) is unique by area
         "sum(1 for a in range(ABC + 1, 1000) if is_square(a) and \
                   sum(1 for t in range(100, 1000) if is_square(t) \
                           if len(set(str(a) + str(t))) == 6 \
                         for u in range(1000, 10000) if is_square(u) and \
                         len(set(str(a) + str(t) + str(u))) == 10 \
                       ) == 1 \
                for b in range(DEF + 1, 1000) if is_square(b) \
                  if len(set(str(a) + str(b))) == 6 \
                for c in range(GHIJ + 1, 10000) if is_square(c) and \
                len(set(str(a) + str(b) + str(c))) == 10 \
             ) > 1",
         
        ],
        answer="(str(ABC) + ':' + str(DEF) + ':' + str(GHIJ))",
        distinct=("ABCDEFGHIJ"),
        verbose=0,
      )
      
      # print answer
      for (_, ans) in p.solve():
        print(f"Rudolph's postcode is {ans}")
      

      Like

    • Frits's avatar

      Frits 1:33 pm on 5 October 2021 Permalink | Reply

      Similar.

        
      import sqlite3
      
      sqs = [str(i**2) for i in range(10, 100)]
      
      luckies = []
      for a in [x for x in sqs if len(x) == 3]:
        for b in [x for x in sqs if len(x) == 3]:
          ab = a + b
          if len(set(ab)) != 6: continue
          for c in [x for x in sqs if len(x) == 4]:
            if len(set(ab + c)) != 10: continue
            luckies.append([a, b, c])
      
      # connect to SQLite database that resides in the memory
      sqlite_Connection = sqlite3.connect('temp.db')
      c1 = sqlite3.connect(':memory:')
      
      c1.execute("CREATE TABLE postcodes ( \
                          area INT, \
                          town INT, \
                          house INT \
                  )")
      
      # insert entries into table
      for (x, y, z) in luckies:
        stmnt = "INSERT INTO postcodes VALUES ("
        stmnt += x + ", " + y + ", " + z + ")"
        c1.execute(stmnt)
      
      c1.commit()
      
      stmnt = "SELECT area, town, house, " 
      stmnt += "(SELECT COUNT(1) FROM postcodes B" 
      stmnt += " WHERE B.AREA > A.AREA and" 
      stmnt += "       B.TOWN > A.TOWN and" 
      stmnt += "       B.HOUSE > A.HOUSE and"
      stmnt += "       NOT EXISTS (SELECT 1"  # Flavia's postcode is unique by area
      stmnt += "         FROM postcodes C"
      stmnt += "          WHERE C.AREA = B.AREA and" 
      stmnt += "          (C.TOWN != B.TOWN or"
      stmnt += "           C.HOUSE != B.HOUSE)" 
      stmnt += "         )"
      stmnt += "       ) as nFlavia "
      stmnt += "FROM postcodes A " 
      stmnt += "WHERE nFlavia > 1;"
      
      for ans in c1.execute(stmnt):
        print(f"Rudolph's postcode is {ans[:-1]}")
        
      c1.close()  
      
      if (sqlite_Connection):
        sqlite_Connection.close()
      

      Like

    • GeoffR's avatar

      GeoffR 3:41 pm on 5 October 2021 Permalink | Reply

      A good variety of solutions for this teaser.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: sq3 = {x*x | x in 10..31};  
      set of int: sq4 = {x*x | x in 32..99};
       
      % Rudolph's digits in 3:3:4 digits for area:town:house numbers
      var 1..9:A; var 0..9:B; var 0..9:C; 
      var 1..9:D; var 0..9:E; var 0..9:F; 
      var 1..9:G; var 0..9:H; var 0..9:I; var 0..9:J;
      constraint all_different ([A,B,C,D,E,F,G,H,I,J]);
      
      % Flavia's digits - 1st set are abc:def:ghij
      var 1..9:a; var 0..9:b; var 0..9:c; 
      var 1..9:d; var 0..9:e; var 0..9:f; 
      var 1..9:g; var 0..9:h; var 0..9:i; var 0..9:j;
      constraint all_different([a,b,c,d,e,f,g,h,i,j]);
      
      % Flavia's digits - 2nd set are mno:pqr:stuv
      var 1..9:m; var 0..9:n; var 0..9:o; 
      var 1..9:p; var 0..9:q; var 0..9:r; 
      var 1..9:s; var 0..9:t; var 0..9:u; var 0..9:v;
      constraint all_different ([m,n,o,p,q,r,s,t,u,v]);
      
      % Rudolph's numbers
      var 100..999: ABC = 100*A + 10*B + C;
      var 100..999: DEF = 100*D + 10*E + F;
      var 1000..9999: GHIJ = 1000*G + 100*H + 10*I + J;
      
      % Flavia's numbers - 1st set
      var 100..999: abc = 100*a + 10*b + c;
      var 100..999: def = 100*d + 10*e + f;
      var 1000..9999: ghij = 1000*g + 100*h + 10*i + j;
      
      % Flavia's numbers - 2nd set
      var 100..999: mno = 100*m + 10*n + o;
      var 100..999: pqr = 100*p + 10*q + r;
      var 1000..9999: stuv = 1000*s + 100*t + 10*u + v;
      
      % Square constraints for Rudolph's and Flavia's numbers
      constraint ABC in sq3 /\ DEF in sq3 /\ GHIJ in sq4;
      constraint abc  in sq3 /\ def in sq3 /\ ghij in sq4;
      constraint mno in sq3 /\ pqr in sq3 /\ stuv in sq4;
      
      % Flavia's area/town/house numbers were all bigger than Rodolph's numbers
      % We conclude she has two numbers as Rudolph made a mistake on the first.
      constraint abc > ABC /\ def > DEF /\ ghij > GHIJ;
      constraint mno > ABC /\ pqr > DEF /\ stuv > GHIJ;
      
      % Flavia's two sets of numbers are different areas/ house numbers
      % ... but make Flavia's town the same in both sets of numbers
      constraint mno > abc /\ pqr == def /\ ghij > stuv;
      
      solve satisfy;
      
      output ["Rudolph's numbers : " ++ show([ABC, DEF, GHIJ])
      ++ "\n" ++ "Flavia's numbers (1st set): " ++ show([abc, def,ghij])
      ++ "\n" ++ "Flavia's numbers (2nd set): " ++ show([mno, pqr, stuv]) ];
      
      % Rudolph's numbers : [324, 576, 1089]
      % Flavia's numbers (1st set): [361, 784, 9025]
      % Flavia's numbers (2nd set): [961, 784, 3025]
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:06 am on 3 October 2021 Permalink | Reply
    Tags: by: Cdr. P. A. Maitland   

    Brain Teaser: What’s his number? 

    From The Sunday Times, 20th December 1959 [link]

    A professor of mathematics was driving in an unfamiliar transatlantic city to visit a friend who lived in a very long street. Stopping to check the street-name he found he had entered the street he was looking for at the end where the high numbers finished. The final number started a train of thought: after a short calculation he smiled in satisfaction and drove on to his friend’s house.

    His friend was astonished when he said: “I have just discovered a most curious thing: did you know that, in this street, the sum of the house-numbers larger than your number is equal to the sum of the house-numbers smaller than your number?”

    Given that the street contained between 500 and 3,500 houses, how many houses were there in it and what was the number of the friend’s house?

    This is one of the occasional Holiday Brain Teasers published in The Sunday Times prior to the start of numbered Teasers in 1961. A prize of 3 guineas was offered.

    [teaser-1959-12-20] [teaser-unnumbered]

     
    • Jim Randell's avatar

      Jim Randell 9:06 am on 3 October 2021 Permalink | Reply

      (See also: Enigma 1725).

      We assume the houses are numbered consecutively from 1 without duplication or omission.

      If the friend’s house is x of y, we have:

      sum(1 … x − 1) = sum(x + 1 … y)
      sum(1 … x − 1) = sum(1 … y) − sum(1 … x)
      T(x − 1) + T(x) = T(y)
      (1/2)(x − 1)x + (1/2)x(x + 1) = T(y)
      x² = T(y)

      And y is in the range 500 to 3500.

      We can easily solve this programatically. The following short Python program runs in 53ms.

      from enigma import (irange, tri, is_square, printf)
      
      for y in irange(500, 3500):
        x = is_square(tri(y))
        if x:
          printf("house {x} of {y}")
      

      Solution: There were 1681 houses in the street. The friend’s house was number 1189.


      Manually, we need to find a triangular number that is also a square number (and in a specific range).

      A recurrence relation for the square triangular numbers [@wikipedia] is:

      N[0] = 0
      N[1] = 1
      N[k + 2] = 34 N[k + 1] − N[k] + 2

      and if: N[k] = (s[k])² = T[t[k]], we have:

      s[0] = t[0] = 0
      s[1] = t[1] = 1
      s[k + 2] = 6 s[k + 1] − s[k]
      t[k + 2] = 6 t[k + 1] − t[k] + 2

      Calculating these sequences we see:

      s[2] = 6; t[2] = 8
      s[3] = 35; t[3] = 49
      s[4] = 204; t[4] = 288
      s[5] = 1189; t[5] = 1681
      s[6] = 6930; t[6] = 9800

      Only the values for k = 5 are in the required range, so:

      x = s[5] = 1189
      y = t[5] = 1681

      We can implement these steps programatically as follows:

      from enigma import (fib, unpack, printf)
      
      S = fib(0, 1, fn=unpack(lambda a, b: 6 * b - a))
      T = fib(0, 1, fn=unpack(lambda a, b: 6 * b - a + 2))
      
      for (k, (s, t)) in enumerate(zip(S, T)):
        f = (499 < t < 3501)
        printf("s[{k}] = {s}; t[{k}] = {t} {f}", f=['', '[*** SOLUTION ***]'][f])
        if t > 3500: break
      

      As these sequences increase t[k] / s[k] gives rational approximations to √2. In the case of the solution we have:

      t[5] / s[5] = 1681 / 1189 = 41 / 29 = 1.4137931…

      Like

    • John Crabtree's avatar

      John Crabtree 2:55 pm on 5 October 2021 Permalink | Reply

      Fortunately for manual solvers this brain teaser is accessible via the Pell equation, ie
      (2y + 1)^2 – 2(2x)^2 = 1
      where (2y + 1)/(2x) are rational approximations to √2.
      Letting a = 2x and b = 2y + 1 leads to
      a[0] = 0, b[0] = 1, x[0] = 0, y[0] = 0
      a[1] = 2, b[1] = 3, x[1] = 0, y[1] = 1
      a[2] = 12, b[2] = 17, x[2] = 6, y[2] = 8
      a[k + 2] = 6 a[k + 1] – a[k]
      b[k + 2] = 6 b[k + 1] – b[k]
      Then one needs to calculate three more (a, b) pairs so that y is in the permitted range.

      My guess is that this was the first appearance of the Pell equation in a ST Brain Teaser.

      Like

    • GeoffR's avatar

      GeoffR 6:36 pm on 7 October 2021 Permalink | Reply

      I programmed this teaser as the arithmetic sum of the house numbers left (smaller) and right (larger) than my friend’s house number.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 500..3500: N;    % His_House
      var 500..3500: L;    % Last_House
      
      % There are (N - 1) houses left of house N, and last house is (N - 1)
      % There are (L - N) houses right of house N, and 1st house is (N + 1)
      % Using the arithmetic sum formula: S = n/2 * (2*a + (n-1)* d)
      % Left sum of numbers = Right sum of numbers
      
      constraint (N - 1) * N div 2 == (L - N) * (2 *(N + 1) + (L - N - 1)) div 2;
      
      solve satisfy;
      
       output [ "Number of my friend's house is " ++ show(N)
       ++ "\n" ++ "Number of houses in road is " ++ show(L) ];
       
      % Number of my friend's house is 1189
      % Number of houses in road is 1681
      % ----------
      % ========== 
      
      

      Like

    • GeoffR's avatar

      GeoffR 8:53 am on 8 October 2021 Permalink | Reply

      A short Python programme verified the answer.

      # check sums of lower and higher house numbers for his number (1189)
      from enigma import irange
      
      lower_sum, higher_sum = 0, 0
      
      # sum of numbers lower than his number
      for n in irange(1, 1188):
          lower_sum += n
      
      # sum of numbers higher than his number
      for m in irange(1190, 1681):
          higher_sum += m
      
      print(f"Lower sum = {lower_sum}, Higher sum = {higher_sum}")
      # Lower sum = 706266, Higher sum = 706266
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:15 pm on 1 October 2021 Permalink | Reply
    Tags:   

    Teaser 3080: One of a kind 

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

    The raffle tickets at the Mathematical Society Dinner were numbered from 1 to 1000. There were four winning tickets and together they used each of the digits from 0 to 9 once only. The winning numbers could be classified uniquely as one square, one cube, one prime and one triangular number. For example, 36 is a triangular number as 1+2+3+4+5+6+7+8 = 36, but it cannot be a winner as 36 is also a square. The tickets were all sold in strips of five, and two of the winning numbers were from consecutive strips. The first prize was won by the holder of the smallest-numbered winning ticket, which was not a cube.

    List the four winning numbers in ascending order.

    [teaser3080]

     
    • Jim Randell's avatar

      Jim Randell 4:34 pm on 1 October 2021 Permalink | Reply

      The following Python program runs in 64ms.

      Run: [ @replit ]

      from enigma import (irange, singleton, is_cube, is_square, is_triangular, is_prime, empty, tuples, printf)
      
      # collect numbers with no repeated digits as strings
      def collect(fns, a=1, b=1000):
        rs = list(list() for _ in fns)
        for n in irange(a, b):
          # remove numbers with repeated digits
          s = str(n)
          if len(s) != len(set(s)): continue
          # does the number satisfy a single condition
          j = singleton(i for (i, fn) in enumerate(fns) if fn(n))
          if j is not None: rs[j].append(s)
        return rs
      
      # collect each category
      cats = collect([is_cube, is_square, is_triangular, is_prime])
      
      # find members of each set, such that each digit is used exactly once
      def solve(ss, ns=[], ds=empty):
        # are we done?
        if not ss:
          if len(ds) == 10:
            yield ns
        else:
          # choose an element from the next set
          for n in ss[0]:
            if not ds.intersection(n):
              yield from solve(ss[1:], ns + [n], ds.union(n))
      
      # allocate tickets to strips
      strip = lambda n: (n + 4) // 5
      
      # find candidate winning tickets
      for ns in solve(cats):
        # smallest number is not a cube
        ns = sorted(int(n) for n in ns)
        if is_cube(ns[0]): continue
        # find numbers on consecutive strips
        if any(b == a + 1 for (a, b) in tuples(map(strip, ns), 2)):
          printf("{ns}")
      

      Solution: The winning numbers are: 49, 53, 216, 780.

      Like

    • GeoffR's avatar

      GeoffR 12:05 pm on 4 October 2021 Permalink | Reply

      For the 10 unique digits used in this teaser, I found only two arrangements to partition the 10 different digits to give four numbers, so that no number was greater than 1000.

      These two 10-digit arrangements were 1:3:3:3 and 2:2:3:3.

      The first 10-digit arrangement i.e. 1:3:3:3 did not produce a solution, using a similar MiniZinc programme for the second 10-digit arrangement i.e. 2:2:3:3, shown below.

      % A Solution in MiniZinc
      include "globals.mzn";
       
      var 1..9:A; var 0..9:B; var 1..9:C; var 1..9:D; var 1..9:E; 
      var 0..9:F; var 0..9:G; var 1..9:H; var 0..9:I; var 0..9:J; 
      
      % four winning tickets used each of the digits from 0 to 9 once only
      constraint all_different([A, B, C, D, E, F, G, H, I, J]);
      
      predicate is_sq(var int: y) =
        exists(z in 1..ceil(sqrt(int2float(ub(y))))) (
              z*z = y );
              
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) ((i < x) -> (x mod i > 0));
      
      predicate is_tri( var int: n) =
        exists ( x in 1..n div 2)
             ( x * (x+1) div 2 = n);
      
      predicate is_cube(var int: c) =
         let {
            var 0..ceil(pow(int2float(ub(c)),(1/3))): z
         } in z*z*z = c;
         
      % Assume number digit splits are 2:2:3:3 to use the ten different digits
      var 10..99:AB = 10*A + B;
      var 10..99:CD = 10*C + D;
      var 100..999:EFG = 100*E + 10*F + G;
      var 100..999:HIJ = 100*H + 10*I + J;
      
      % Reducing 24 no.possible permutations, for 4 items, 
      % to say 9 permutations for two repeated groups of digit patterns,
      % omitting AB as a cube for the first 2-digit number, as specified,
      % proved sufficient to find the unique answer for the four numbers.
      constraint 
         (is_sq(AB) /\ is_prime(CD) /\ is_cube(EFG) /\ is_tri(HIJ)  )
      \/ (is_sq(AB) /\ is_cube(CD) /\ is_prime(EFG) /\ is_tri(HIJ)  )
      \/ (is_sq(AB) /\ is_tri(CD) /\ is_prime(EFG) /\ is_cube(HIJ)  )
      
      \/ (is_prime(AB) /\ is_sq(CD) /\ is_cube(EFG) /\ is_tri(HIJ)  )
      \/ (is_prime(AB) /\ is_cube(CD) /\ is_sq(EFG) /\ is_tri(HIJ)  )
      \/ (is_prime(AB) /\ is_tri(CD) /\ is_sq(EFG) /\ is_cube(HIJ)  )
      
      \/ (is_tri(AB) /\ is_sq(CD) /\ is_prime(EFG) /\ is_cube(HIJ)  )
      \/ (is_tri(AB)/\ is_prime(CD) /\ is_sq(EFG)  /\ is_cube(HIJ)  )
      \/ (is_tri(AB) /\ is_cube(CD) /\ is_sq(EFG) /\ is_prime(HIJ)  );
      
      % put four numbers in increasing order
      constraint increasing ( [ AB, CD, EFG, HIJ]);
      
      % two of the winning numbers were from consecutive strips of 5 tickets
      constraint ((CD + 4) div 5 - (AB + 4) div 5 == 1)
      \/ ((EFG + 4) div 5 - (CD + 4) div 5 == 1)
      \/ ((HIJ + 4) div 5 - (EFG + 4) div 5 == 1) ;
      
      output [ "Four numbers are " ++ show([AB, CD, EFG, HIJ]) ];
      

      Like

    • Frits's avatar

      Frits 3:29 pm on 4 October 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      from itertools import permutations as perm
      
      checknum = lambda n: n in td
      gettype = lambda n: td[n]
      
      # two ticket numbers must be in adjacent strips
      def consecutive(li):
       strips = [(x - 1) // 5 for x in li]
       if any(strips[i + 1] == x + 1 for i, x in enumerate(strips[:-1])):
         return True
       return False
      
      # perfect squares and perfect cubes between 1000 and 9999
      squares = set(i ** 2 for i in range(1, 32))
      # as smallest ticket may not be a cube start with 27
      cubes = set(i ** 3 for i in range(3, 11))
      tris = set((i * (i + 1)) // 2 for i in range(1, 45))
      
      # set of prime numbers up to 997
      P = {2, 3, 5, 7}
      P |= {x for x in range(11, 33, 2) if all(x % p for p in P)}
      P |= {x for x in range(33, 1000, 2) if all(x % p for p in P)}
      
      td = dict()  # dictionary of tickets
      # build dictionary of "classified uniquely" ticket numbers
      for n in range(1, 1000):
       # skip numbers with duplicate digits
       if len(set(str(n))) != len(str(n)): continue
       cnt = 0
       for i, y in enumerate([squares, cubes, tris, P]):
         if n in y:
           cnt += 1
           i1 = i
       # classified uniquely?
       if cnt == 1:
         td[n] = i1
      
      # determine digits that occur in all cubes
      cubes = [k for k, v in td.items() if v == 1]
      mand_dgts = set(str(cubes[0]))
      for c in cubes[1:]:
       mand_dgts &= set(str(c))
      
      # remove non-cube entries which use digits that occur in all cubes
      if mand_dgts:
       td = dict((k, v) for k, v in td.items()
                 if v == 1 or all(c not in str(k) for c in mand_dgts))
      
      # the alphametic puzzle (numbers AB, CDE, FGH and IJK are increasing)
      p = SubstitutedExpression(
       [
        "A == 0 or C == 0",   # AB and CDE must have total length of 4
        "A + C = X",          # X is non-zero and represents A or C
      
        # check AB
        "checknum(AB)",
        "gettype(AB) != 1",   # not a cube
      
        # check CDE
        "AB < CDE and checknum(CDE)",
        "gettype(CDE) not in {gettype(AB)}",
        "consecutive([AB, CDE]) = Y",
      
        # check FGH
        "C < F",
        "checknum(FGH)",
        "gettype(FGH) not in {gettype(AB), gettype(CDE)}",
        "Y or (G == 9 and F < 8) or consecutive([CDE, FGH])",
        #"consecutive([CDE, FGH]) = Z",
        #"Y or Z or (G == 9 and F < 8)",
      
        # check IJK
        "F < I",
        "checknum(IJK)",
        "gettype(IJK) not in {gettype(AB), gettype(CDE), gettype(FGH)}",
      
        # two ticket numbers must be in adjacent strips
        #"consecutive([AB, CDE, FGH, IJK])",
        "Y or consecutive([CDE, FGH, IJK])",
        #"Y or Z or consecutive([FGH, IJK])",
       ],
      
       answer="AB, CDE, FGH, IJK",
       verbose=0,
       d2i=dict([(0, "FI")] +
                [(1, "I")] +
                [(8, "C")] +
                [(9, "CF")]),
       distinct="BDEFGHIJKX",
       env=dict(consecutive=consecutive, checknum=checknum, gettype=gettype),
       reorder=0,
      )
      
      # print answer
      for (_, ans) in p.solve():
       print(f"{ans}")
      

      Like

    • GeoffR's avatar

      GeoffR 4:47 pm on 4 October 2021 Permalink | Reply

      This Python programme lists all 3-digit primes, squares, cubes and triangular numbers, without repeating digits, with reference to a 1:3:3:3 digits split solution.

      from enigma import is_prime
      
      L1, L2, L3, L4 = [], [], [], []
      
      #1. 3-digit squares without repeating digits
      print('3-digit squares')
      L1 = [ x * x for x in range(10, 32) if len(set(str(x*x))) == len(str(x*x)) ]
      print(L1)
      
      #2. set of 3-digit tri numbers, without repeating digits
      print('3-digit triangular numbers')
      for n in range(14,45):
          tri = n * (n + 1)//2
          if len(set(str(tri))) == len(str(tri)):
              L2.append(tri)
      
      print(L2); print(len(L2))
      
      #3. 3-digit cubes. without repeating digits
      print('3-digit cubes')
      
      L3 = [ x*x*x for x in range(5,10) if len(set(str(x*x*x))) == len(str(x*x*x)) ]
      print(L3)
      
      #4. 3-digit primes, without repeating digits
      print('3-digit primes')
      for n in range(100,1000):
          if is_prime(n) and len(set(str(n))) == len(str(n)):
              L4.append(n)
      
      print(L4); print(len(L4))
      
      # 3-digit squares, non repeating digits
      # [169, 196, 256, 289, 324, 361, 529, 576, 625, 729, 784, 841, 961] 13 no.
      
      # 3-digit triangular numbers, non repeating digits
      # 105, 120, 136, 153, 190, 210, 231, 253, 276, 325, 351, 378,
      # 406, 435, 465, 496, 528, 561, 630, 703, 741, 780, 820, 861,
      # 903, 946] 26 no.
      
      # 3-digit cubes, non repeating digits
      # [125, 216, 512, 729] 4 no.
      
      # 3-digit primes, non repeating digits
      # 103, 107, 109, 127, 137, 139, 149, 157, 163, 167, 173, 179,
      # 193, 197, 239, 241, 251, 257, 263, 269, 271, 281, 283, 293,
      # 307, 317, 347, 349, 359, 367, 379, 389, 397, 401, 409, 419,
      # 421, 431, 439, 457, 461, 463, 467, 479, 487, 491, 503, 509,
      # 521, 523, 541, 547, 563, 569, 571, 587, 593, 601, 607, 613,
      # 617, 619, 631, 641, 643, 647, 653, 659, 673, 683, 691, 701,
      # 709, 719, 739, 743, 751, 761, 769, 809, 821, 823, 827, 829,
      # 839, 853, 857, 859, 863, 907, 937, 941, 947, 953, 967, 971,
      # 983] 97 no.
      
      
      
      

      Manual Analysis – why a 1:3:3:3 digits split is not viable
      ———————————————————-

      Considering a 1:3:3:3 digits split, the single digit can be a square (1,4,9), a prime (2,3,5), a triangular number (1,3,6) but not a cube(1,8) as per teaser conditions.

      For any combination of the three remaining 3-digit numbers, I found there was usually a duplicate hundreds digit (1-9), after choosing the first 3-digit number. The 400 series for 3-digit squares, as 400, 441 etc was excluded due to repeating digits.

      Obviously, this prevents finding four separate numbers with ten different digits.

      I also checked for any possibility of consecutive numbers in two strips of five tickets at the end of one hundred series e.g. end of 190’s and the start of a new hundred series e.g. start of 200’s.

      One instance of a 3-digit triangular number(496) and a consecutive 3-digit prime(503) was found, leaving the digits (1,2,7,9) to form a 3-digit cube and a single digit square, or a single digit cube and a 3-digit square. This was not found to be possible. The only 3-digit numbers found were either not in any of the four number categories, or duplicate primes to 503.

      The over-riding reasons why three 3-digit suitable numbers could not be found was the duplication of the hundreds digit and the teaser requirement to obtain two numbers in two consecutive strips of five tickets.

      Like

  • Unknown's avatar

    Jim Randell 10:04 am on 30 September 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 869: New type football 

    From The Sunday Times, 19th March 1978 [link]

    In this puzzle four football teams are going to play each other once during the course of the season. The incomplete table below shows the situation when some of the matches have been played (2 points for a win, 1 for a draw as usual).

    The digits have been replaced by letters and each letter stands for the same digit wherever it appears and different letters stand for different digits.

    The table looks like this:

    List the matches played and the score in each match.

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

    [teaser869]

     
    • Jim Randell's avatar

      Jim Randell 10:04 am on 30 September 2021 Permalink | Reply

      We know the goals for/against values for A and B, so we can calculate scores for matches involving A or B, which only leaves the score in the C vs D match to be decided, and as the overall goals for C is t, we can calculate potential scores for C in the match. If the match is a draw, then D’s score is the same as C’s. If it is a win for C, then D scored fewer goals. And if it is a win for D, then D scored more goals. I put an upper limit on the number of goals in this last case, but it is not a situation that is reached.

      This Python program uses the [[ Football() ]] helper class from the enigma.py library. It runs in 54ms.

      Run: [ @replit ]

      from enigma import Football, irange
      
      # scoring system
      football = Football(points=dict(w=2, d=1))
      
      # the table
      table = dict(played='x?pk', w='?hx?', l='??h?', d='k?k?', points='??m?')
      (gf, ga) = ('hmt?', 'pm??')
      
      # label the teams
      (A, B, C, D) = (0, 1, 2, 3)
      
      # find match outcomes
      for (ms, d) in football.substituted_table(table):
      
        # calculate scores for games involving A and B
        for ss in football.substituted_table_goals(gf, ga, ms, d=d, teams=[A, B]):
      
          # only the C vs D match is undecided
          sCD = list()
          m = ms[(C, D)]
          if m == 'x':
            sCD = [None]
          else:
            # calculate known goals for/against C
            (f, a) = football.goals([ss[(A, C)], ss[(B, C)]], [1, 1])
            # so the score in the C vs D match is (x, y)
            for x in irange(0, 9 - f):
              t = f + x
              if t in d.values(): continue
              if m == 'd':
                sCD.append((x, x))
              elif m == 'w':
                sCD.extend((x, y) for y in irange(0, x - 1))
              elif m == 'l':
                sCD.extend((x, y) for y in irange(x + 1, 10))  # upper limit on goals scored
      
          for ss[(C, D)] in sCD:
            # output solution
            football.output_matches(ms, ss, teams="ABCD", d=d)
      

      Solution: The played matches are: A vs B = 0-0; A vs C = 0-3; B vs C = 5-5; C vs D = 1-0.

      The A vs D, B vs D matches are not yet played.

      Like

  • Unknown's avatar

    Jim Randell 9:49 am on 28 September 2021 Permalink | Reply
    Tags:   

    Teaser 2827: Password 

    From The Sunday Times, 27th November 2016 [link] [link]

    My computer password consists of different digits written in decreasing order.

    I can rearrange the digits to form a perfect cube.

    A further rearrangement gives a perfect square.

    Another rearrangement gives a prime number.

    A further rearrangement gives a number that is divisible by the number of digits in the password.

    Yet another rearrangement gives a number that is divisible by all but one of its digits.

    What is my password?

    [teaser2827]

     
    • Jim Randell's avatar

      Jim Randell 9:50 am on 28 September 2021 Permalink | Reply

      This Python program runs in 245ms.

      Run: [ @replit ]

      from enigma import group, unpack, powers, inf, is_duplicate, ordered, nsplit, intersect, nconcat, subsets, is_prime, find, update, printf
      
      # collect numbers by digit content
      def numbers(s):
        def generate():
          for n in s:
            if n > 9876543210: break
            # only collect numbers with distinct digits
            if is_duplicate(n): continue
            yield (ordered(*nsplit(n), reverse=1), n)
        return group(generate(), by=unpack(lambda ds, n: ds), f=unpack(lambda ds, n: n))
      
      cubes = numbers(powers(0, inf, 3))
      squares = numbers(powers(0, inf, 2))
      
      divides = lambda n, ds: sum(d != 0 and n % d == 0 for d in ds)
      
      # select distinct elements from each set
      def select(ss, xs):
        i = find(xs, None)
        if i == -1:
          yield xs
        else:
          # choose an elements from ss[i]
          for x in ss[i]:
            if x not in xs:
              yield from select(ss, update(xs, [(i, x)]))
      
      # look for keys common to squares and cubes
      for ds in intersect(s.keys() for s in (squares, cubes)):
        k = len(ds)
      
        # find rearrangements (we need at least 6)...
        rs = set(nconcat(*s) for s in subsets(ds, size=k, select="P") if s[0] > 0)
        if len(rs) < 6: continue
      
        # ... divisible by k
        r1s = set(n for n in rs if n % k == 0)
        if not r1s: continue
      
        # ... divisible by all but one of the digits in ds
        r2s = set(n for n in rs if divides(n, ds) == k - 1)
        if not r2s: continue
      
        # ... primes
        r3s = set(n for n in rs if is_prime(n))
        if not r3s: continue
      
        # select distinct members for each set
        n = nconcat(*ds)
        for xs in select([[n], cubes[ds], squares[ds], r3s, r1s, r2s], [None] * 6):
          printf("password = {n}")
          printf("-> cubes = {rs}", rs=cubes[ds])
          printf("-> squares = {rs}", rs=squares[ds])
          printf("-> primes = {r3s}")
          printf("-> divisible by {k} = {r1s}")
          printf("-> divisible by all but one of {ds} = {r2s}")
          printf("-> selected = {xs}")
          break  # only need one example
      

      Solution: The password is 9721.

      The program ensures five different rearrangements of the password satisfying the conditions can be made, for example:

      9721 = password
      2197 = cube
      7921 = square
      2179 = prime
      7912 = divisible by 4 (length of password)
      1792 = divisibly by 7, 2, 1 (but not 9)

      In fact there are 50 different sets of rearrangements that we could choose for this particular password.


      We can do some analysis to reduce the number of passwords considered by looking at digital roots (which are unchanged by rearrangement):

      the digital root of a square is 1, 4, 7, 9
      the digital root of a cube is 1, 8, 9
      the digital root of prime (except 3) is 1, 2, 4, 5, 7, 8

      So we need only consider passwords with a digital root of 1. (At this point considering cubes leads to 12 candidate passwords).

      Also, there must be at least 6 different rearrangements, so we need at least 3 digits, but it cannot have exactly 3 digits, as then the requirement for a rearrangement divisible by the number of digits would require a rearrangement that is divisible by 3, which would also have a digital root that was divisible by 3. (At this point considering cubes leads to 10 candidate passwords).

      Similarly, the number cannot have 9 digits, as it would have to have a rearrangement with a digital root of 9. Nor 10 digits, as again, the digital root would have to be 9.

      And if the password had length 6, it would have to have a rearrangement divisible by 6, which would require the rearrangement to be divisible by 3, and so its digital root would also be divisible by 3.

      So the password has 4, 5, 7, or 8 digits, and a digital root of 1. (At this point considering cubes leads to 6 candidate passwords).

      Additionally the password is not divisible only one of its digits, and it cannot be divisible by 0, 3, 6, 9, so no more than one of those four digits can occur in it.

      This is enough to narrow the password down to a single candidate, which gives the answer to the puzzle. (Although for completeness we should check that the appropriate rearrangements can be made).

      Like

    • Hugh Casement's avatar

      Hugh Casement 1:36 pm on 28 September 2021 Permalink | Reply

      All six rearrangements ending in 2 are integer multiples of 4 (and of course of 1 and 2).
      1279, 1297, 2179, 2719, 2791, 2917, 2971, 7129, 7219, 9127, 9721 are all prime.
      I have certainly known more satisfying Teasers.

      Like

    • Frits's avatar

      Frits 12:02 pm on 4 October 2021 Permalink | Reply

      The singles and lst2 logic is not really needed as Jim’s select function is sufficient.
      The program runs in 15ms.

       
      from enigma import is_cube, is_square, is_prime, find, update
      from itertools import permutations as perm
      
      # select distinct elements from each row
      def select(ss, xs):
        i = find(xs, None)
        if i == -1:
          yield xs
        else:
          # choose an elements from ss[i]
          for x in ss[i]:
            if x not in xs:
              yield from select(ss, update(xs, [(i, x)]))
      
      def check(n):
        # we need 6 arrangements
        
        # 0: I can rearrange the digits to form a perfect cube.
        # 1: A further rearrangement gives a perfect square.
        # 2: Another rearrangement gives a prime number.
        # 3: A rearrangement that is divisible by the number of digits.
        # 4: A rearrangement that is divisible by all but one of its digits.
        # 5: original number
      
        s = str(n)
        ndigits = len(s)
        lst = [[] for _ in range(5)]
        
        # check all permutations of original number
        for p in perm(s):
          m = int("".join(p)) 
          
          if m == n: continue
        
          if is_cube(m): 
            lst[0] += [m]
          if is_square(m): 
            lst[1] = lst[1] + [m]
          if is_prime(m): 
            lst[2] += [m]
          if m % ndigits == 0: 
            lst[3] += [m]
          if sum(x != '0' and m % int(x) == 0 for x in p) == ndigits - 1: 
            lst[4] += [m]
            
        # if any row is empty return False
        if any(not x for x in lst): return False
        
        # add 6th arrangement (original number)
        lst += [[n]]
        
        singles = [x[0] for x in lst if len(x) == 1]
        
        # build list of non-singles with numbers not occurring in singles
        lst2 = [[y for y in x if y not in singles] for x in lst if len(x) > 1]  
        lngth = len(lst2)
        
        # if each remaining row contains enough items return True
        if all(len(x) >= lngth for x in lst2):
          return True
        
        # select distinct members for each row
        for x in select(lst, [None] * 6):
          return True
        
        return False
      
      # add digit to each number in the list so that order remains decreasing
      def addLastDigit(li=range(1, 10)):
        lngth = len(str(li[0])) if li else 0
        return [10 * n + i for n in li for i in range(10 - lngth) if i < (n % 10)]
            
            
      # list of 3-digit numbers with different digits written in decreasing order
      nums = addLastDigit(addLastDigit())
      
      while True:
        for n in nums:
          if check(n):
            print("password:", n)
            break  # only need one example
        else:  # no break
          if len(str(nums[0])) > 9: break
          # add another digit
          nums = addLastDigit(nums)
          continue
          
        break    # break if nested break occurred 
      

      Like

  • Unknown's avatar

    Jim Randell 10:47 am on 26 September 2021 Permalink | Reply
    Tags:   

    Brain-Teaser: Silver collection 

    From The Sunday Times, 31st July 1960 [link]

    Seven South Sea Island brothers found on the beach a broken box and mixed silver coins scattered. These were old British coins (six-pence, shilling, florin, half-crown, and crown). 135 of them bore a man’s head and 54 a woman’s. The two younger brothers claimed the latter and their elders shared the former.

    Being ignorant of the value of the coins they agreed to take twenty-seven each. First they laid the coins in heaps of each size. The senior brother then took seven coins from each of two heaps and smaller numbers from the other three heaps to make up twenty-seven coins. Each other brother then took the same numbers but each from a different order of heaps.

    Unknown to themselves, the five elders had equal money value. The boys also had equal value, but greater than their elders.

    What were the values?

    [Note: Respective values: 6d, 12d, 24d, 30d, 60d]

    This is one of the occasional Holiday Brain-Teasers published in The Sunday Times prior to the start of numbered Teasers in 1961. A prize of 3 guineas was offered.

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

    [teaser-1960-07-31] [teaser-unnumbered]

     
    • Jim Randell's avatar

      Jim Randell 10:49 am on 26 September 2021 Permalink | Reply

      This Python program runs in 51ms.

      Run: [ @replit ]

      from enigma import (decompose, group, subsets, printf)
      
      # denominations of coins
      denominations = [6, 12, 24, 30, 60]
      
      # total for quantities of coins in qs (wrt denominations)
      total = lambda qs: sum(x * y for (x, y) in zip(qs, denominations))
      
      # decompose 27 - 14 = 13 into 3 numbers between 1 and 6
      for qs in decompose(13, 3, min_v=1, max_v=6, sep=0):
        qs += (7, 7)
      
        # collect permutations by total amount
        d = group(subsets(qs, size=len, select="mP"), by=total)
      
        # look for 5 permutations for the elders
        for (k1, vs1) in d.items():
          if not (len(vs1) > 4): continue
          # and 2 permutations, with a greater sum for the youngers
          for (k2, vs2) in d.items():
            if not (k2 > k1 and len(vs2) > 1): continue 
            printf("elders = {k1} [from {vs1}]")
            printf("youngers = {k2} [from {vs2}]")
            printf()
      

      Solution: The elders had selected coins with a value of 798d (= 66s 6d). The youngers had selected coins with a value of 834d (= 69s 6d).

      Like

    • Hugh Casement's avatar

      Hugh Casement 3:13 pm on 26 September 2021 Permalink | Reply

      I confess I cheated by working backward from Jim’s solution, but was able to deduce that there were 48 crowns, 44 half crowns, 38 florins, 32 shillings, and 27 sixpences.
      Total 189 coins with a value £23 11s 6d = 5658 d.

      If the five piles were arranged in order of decreasing value, the seniors (in some order) took
      7, 7, 4, 3, 6; 7, 7, 3, 6, 4; 7, 6, 4, 7, 3; 7, 4, 7, 6, 3; 6, 7, 7, 3, 4 coins from the respective piles.
      The youngsters took 7, 7, 6, 3, 4 and 7, 6, 7, 4, 3.

      There would be other ways of making up the values 798 and 834 with 27 coins, but not with as many as five of one and two of the other, using the same numbers in different orders.

      Like

  • Unknown's avatar

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

    Teaser 3079: Hall of residence 

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

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

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

    [teaser3079]

     
    • Jim Randell's avatar

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

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

      This run-file executes in 68ms.

      Run: [ @replit ]

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

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

      The complete allocations are:

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


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

      Like

      • Jim Randell's avatar

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

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

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

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

        Like

    • Frits's avatar

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

      A lot slower.

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

      Like

      • Frits's avatar

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

        @Jim,

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

        Like

        • Jim Randell's avatar

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

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

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

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

          Like

  • Unknown's avatar

    Jim Randell 8:11 am on 23 September 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 855: Pity the poor postman 

    From The Sunday Times, 4th December 1977 [link]

    Extract (genuine!) from a national daily, 27th January 1977:

    Pity the poor postman trying to deliver letters along Stonebow Road in the village of Drake’s Broughton, in north-west England. Due to local government confusion the road boasts five houses with the number 1, four others are number 2, three have number 4, and two are numbered 6.

    To add to the postman’s woes there are four families called Davies in Stonebow Road, plus two named Bridges, three named Barker, and two named Webb.

    On the unwarranted supposition that:

    (i) the occupants of the said houses included all of the said families; but
    (ii) the postman’s problem was somewhat alleviated by the fact that no two families of the same name had the same house-number; and
    (iii) the house-numbers of the Webbs totalled to more than did those of the Bridges, while those of all the families with two of the four names totalled the  same as did those of all the families with the other two names.

    What were the house-numbers of the Barkers and Webbs respectively?

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

    [teaser855]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 23 September 2021 Permalink | Reply

      This Python program runs in 49ms.

      Run: [ @replit ]

      from enigma import (multiset, subsets, partitions, printf)
      
      # the pool of house numbers
      ns = multiset.from_pairs([(1, 5), (2, 4), (4, 3), (6, 2)])
      
      # choose 2 house numbers for the Bridges
      for Brs in subsets(ns.keys(), size=2):
        sBrs = sum(Brs)
      
        # choose 2 house number for the Webbs
        ns1 = ns.difference(Brs)
        for Ws in subsets(ns1.keys(), size=2):
          sWs = sum(Ws)
          if not (sWs > sBrs): continue
      
          # choose 4 numbers for the Davies
          ns2 = ns1.difference(Ws)
          for Ds in subsets(ns2.keys(), size=4):
            sDs = sum(Ds)
      
            # choose 3 numbers for the Barkers
            ns3 = ns2.difference(Ds)
            for Bas in subsets(ns3.keys(), size=3):
              sBas = sum(Bas)
      
              # consider partitions of the sums ...
              for (p1, p2) in partitions([sBrs, sWs, sDs, sBas], 2):
                # that give the same total
                if sum(p1) == sum(p2):
                  printf("Bridges = {Brs}; Webbs = {Ws}; Davies = {Ds}; Barkers = {Bas} [{p1}, {p2}]")
      

      Solution: The Barkers are at 1, 4, 6. The Webbs are at 1, 4.

      And the total sum of their numbers is 16.

      The Davies are at 1, 2, 4, 6, and The Bridges are at 1, 2.

      The total sum of their numbers is also 16.

      There are house numbers 1, 2, 2 remaining.

      Like

  • Unknown's avatar

    Jim Randell 8:57 am on 21 September 2021 Permalink | Reply
    Tags: , ,   

    Teaser 2823: Queuing 

    From The Sunday Times, 30th October 2016 [link] [link]

    Tickets for the event went on sale at 09:30. The queue started at 09:00 when 2 people arrived, then 4 more at 09:01, 6 more at 09:02 and so on until 60 more arrived at 09:29. Just 16 people arrived at 09:30, 16 more at 09:31, 16 more at 09:32 and so on. Tickets were sold steadily at a rate of 25 per minute (one per person).

    Joe and I were the first to arrive at our respective minutes, but we had identical waiting times before being sold our tickets, and no-one waited for less time to get a ticket. Joe was lucky to get the last ticket to be sold.

    At what time did Joe join the queue?

    This version of the text is provided by the setter, and differs from the version published in The Sunday Times.

    [teaser2823]

     
    • Jim Randell's avatar

      Jim Randell 8:58 am on 21 September 2021 Permalink | Reply

      The problem with this puzzle (particularly in the form it was published in the newspaper) was working out the intended mechanics of the situation.

      We suppose that all people joining the queue, do so simultaneously at the start of each specified minute. And that when the tickets are sold, they sold sequentially (a single ticket booth), with each sale taking exactly 1/25 minute (= 0.04m = 2.4s). This is apparently what the setter intended.

      The following Python program simulates the sale of tickets as described, and looks for minimal wait times that occur for the first member of a joining clump, until the value is repeated (then the earlier one is the setter, and the later one Joe, and Joe gets the last ticket, so sales end).

      It produces a log of events as it goes, and runs in 72ms.

      Run: [ @replit ]

      from enigma import irange, inf, sprintf, printf
      
      # format a time, (m=0, f=0) = 09:00.00
      def fmt(m, f=0):
        (h, m) = divmod(m, 60)
        return sprintf("{h:02d}:{m:02d}.{f:02d}", h=h + 9, f=4 * f)
      
      # the queue, record groups as (number of people, start minute)
      q = [(0, -1)]
      
      # extend the queue, add n people with value m
      def extend(q, n, m):
        if n > 0:
          q.append((n, m))
      
      # pop the queue, identifying the first in a group, return (x, flag)
      def pop(q):
        ((n, m), flag) = (q[0], 0)
        if n == 0:
          # move on to the next group
          q.pop(0)
          ((n, m), flag) = (q[0], 1)
        q[0] = (n - 1, m)
        return (m, flag)
      
      # size of the queue
      def size(q):
        return sum(n for (n, m) in q)
      
      # t = tickets sold
      # W = shortest waiting time
      # data = information for minimal wait times
      (t, W, data) = (0, inf, None)
      
      # run a timer
      for i in irange(0, inf):
        # m = minutes, f = fractions (1/25) of a minute
        (m, f) = divmod(i, 25)
      
        # on the minute
        if f == 0:
          # up to 09:29, 2m + 2 people join the queue
          # afterwards, 16 people join the queue
          n = (2 * m + 2 if m < 30 else 16)
          extend(q, n, m)
          printf("time {i}: added {n} people to queue, queue length = {q}", i=fmt(m, f), q=size(q))
      
        # from 9:30, one ticket is sold per frame
        if m > 29:
          t += 1
          # calculate waiting time (in frames)
          (x, flag) = pop(q)
          w = 25 * (m - x) + f
          printf("time {i}: joined at {x}, waited {w} frames, ticket {t}", i=fmt(m, f), x=fmt(x))
          # is this a new minimum?
          if not (w > W):
            printf("-> min wait = {w} frames for ticket {t}")
            if w < W:
              W = w
              data = list()
            # is this the first in their group?
            if flag:
              # record data
              data.append((x, m, f, t))
              # are we done?
              if len(data) == 2:
                printf("-> sold out after ticket {t}, queue length = {q}\n", q=size(q))
                # output solution
                for (n, (x, m, f, t)) in enumerate(data, start=1):
                  printf("{n} joined at {x}, served at {m}, ticket {t}", x=fmt(x), m=fmt(m, f))
                break
      

      Solution: Joe joined the queue at 10:06.

      The minimal wait time is 24.24 minutes (= 606/25 minutes).

      When the 1507 tickets sell out there are 399 people remaining in the queue.

      The setter joined the queue at 09:12, and got the 157th ticket at 09:36.24.

      Joe joined the queue at 10:06, and got the 1507th (and final) ticket at 10:30.24.

      So, if the tickets are numbered consecutively from 1, the digit sum of the two tickets is the same (and if they are numbered with leading zeros where necessary, the ticket numbers are anagrams of each other).

      Like

      • Jim Randell's avatar

        Jim Randell 9:03 am on 21 September 2021 Permalink | Reply

        The puzzle text as originally published in the newspaper read as follows:

        Tickets for the event went on sale at 09:30. The queue started at 09:00 when 2 people arrived, then 4 more at 09:01, 6 more at 09:02 and so on until 60 more arrived at 09:29. Just 16 people arrived at 09:30, 16 more at 09:31, 16 more at 09:32 and so on. Tickets were sold at 25 per minute (with one to each person in the queue) until they were sold out.

        Joe and I both had identical waiting times before being sold our tickets, despite me having arrived earlier, and no-one who got a ticket waited for less time than us.

        At what time did Joe join the queue?

        My interpretation of this was that people joined the queue simultaneously at the start of a minute. And when the tickets were sold, the 25 people at the head of the queue were processed simultaneously, at the start of each minute. (You can suppose there are 25 ticket booths, and each transaction takes exactly one minute). The wait times will always be whole numbers of minutes.

        The main problem I came across with this method was that we don’t know how many tickets there are (it is implied that they sell out at some point), but if we suppose Joe is in the last batch of 25 tickets sold we can solve the problem.

        Here is my code adapted to process the tickets 25 at a time.

        from enigma import (irange, inf, sprintf, printf)
        
        # format a time, m=0 = 09:00
        def fmt(m):
          (h, m) = divmod(m, 60)
          return sprintf("{h:02d}:{m:02d}", h=h + 9)
        
        # the queue, record groups as (number of people, start minute)
        q = [(0, -1)]
        
        # extend the queue, add n people with value m
        def extend(q, n, m):
          if n > 0:
            q.append((n, m))
        
        # pop the queue, identifying the first in a group, return (x, flag)
        def pop(q):
          ((n, m), flag) = (q[0], 0)
          if n == 0:
            # move on to the next group
            q.pop(0)
            ((n, m), flag) = (q[0], 1)
          q[0] = (n - 1, m)
          return (m, flag)
        
        # size of the queue
        def size(q):
          return sum(n for (n, m) in q)
        
        # t = tickets sold
        # W = shortest waiting time
        # data = information for minimal wait times
        (t, W, data) = (0, inf, None)
        
        # run a timer (in minutes)
        for m in irange(0, inf):
        
          # up to 9:29, 2m + 2 people join the queue
          # afterwards, 16 people join the queue
          n = (2 * m + 2 if m < 30 else 16)
          extend(q, n, m)
          printf("time {m}: added {n} people to queue, queue length = {q}", m=fmt(m), q=size(q))
        
          # from 9:30, 25 tickets are processed per minute
          if m > 29:
            for _ in irange(1, min(25, size(q))):
              t += 1
              # calculate waiting time (in frames)
              (x, flag) = pop(q)
              w = m - x 
              printf("time {m}: joined at {x}, waited {w} minutes, ticket {t}", m=fmt(m), x=fmt(x))
              # is this a new minimum?
              if not (w > W):
                printf("-> min wait = {w} minutes for ticket {t}{s}", s=(' [first]' if flag else ''))
                if w < W:
                  W = w
                  data = list()
                # is this the first in their group?
                if flag:
                  # record data
                  data.append((x, m, t))
        
            # are we done?
            if len(data) > 1:
              printf("\nsold out after {t} tickets, min wait = {w} minutes, queue length = {q}", q=size(q))
              # output solution
              for (n, (x, m, t)) in enumerate(data, start=1):
                printf("{n} joined at {x}, served at {m}, ticket {t}", x=fmt(x), m=fmt(m))
              printf()
              break
        

        We get the solution:

        minimal wait time = 24 minutes
        setter joined queue @ 9:08, served @ 9:32, ticket 73
        joe joined queue @ 9:09, served @ 9:33, ticket 91
        100 tickets sold, 894 people remaining in queue

        Like

  • Unknown's avatar

    Jim Randell 9:03 am on 19 September 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 43: [Golf match] 

    From The Sunday Times, 14th January 1962 [link]

    Brown came into the clubhouse after his match with Jones. “We were all square after nine holes”, he said, “when Jones suggested that we should play for a small side-stake on the winning of the tenth hole, double the stake on the eleventh, double again on the twelfth, and so on. I agreed. We didn’t halve any of the next nine holes, the match was decided on the eighteenth green, and I found that I had won eleven shillings and ninepence”. We asked him who won the match. “Work it out”, said Brown.

    Who won the match, which of the last nine holes did Brown win, and what was the side-stake on the tenth hole?

    This puzzle was originally published with no title.

    [teaser43]

     
    • Jim Randell's avatar

      Jim Randell 9:03 am on 19 September 2021 Permalink | Reply

      B and J were equal after the first 9 holes (i.e. 4.5 holes each). And each won 4 of the next 8 holes, so that the match was decided on the 18th hole.

      If the stake on the 10th hole was k, then it increases in powers of 2, i.e. k, 2k, 4k, 8k, 16k, …, 256k.

      In order to come out on top B must have won the 256k hole (as the remaining holes sum to 255k), and so B won the match.

      In binary, B’s stake multiplier has 5 bits and is between 100001111 (= 271) and 111110000 (= 496), and J’s multiplier is (111111111 − B).

      In order for B to win 11s + 9d = 141d the stake on the 10th hole is 141 / (B − J), and I assume the stake is some sensible amount (i.e. a multiple of 1/4 d).

      This Python program runs in 53ms.

      from enigma import (bit_permutations, div, irange, printf)
      
      # choose B's stake multiplier
      for B in bit_permutations(0b100001111):
        J = 0b111111111 - B
        k = div(564, B - J)
        if k:
          hs = list(h + 10 for h in irange(0, 8) if B & (1 << h))
          printf("holes = {hs}; stake @ 10 = {k:g}d", k=0.25 * k)
        if B == 0b111110000: break
      

      Solution: Brown won the match. Brown won the 10th, 11th, 12th, 14th, 18th holes. The stake on the 10th hole was 3d.

      The holes Brown won give: (1 + 2 + 4 + 16 + 256)k = 279k

      And the holes Brown lost give: (8 + 32 + 64 + 128)k = 232k

      The difference is: 279k − 232k = 47k, so: k = 141d / 47 = 3d

      Like

    • Hugh Casement's avatar

      Hugh Casement 10:14 am on 19 September 2021 Permalink | Reply

      To bring it up to date a bit, he won £2.35
      — though when we compare (for example) the cost of sending a letter then and now I feel an equivalent initial stake would be quite a bit more than 5p.

      Like

    • Frits's avatar

      Frits 5:52 pm on 4 October 2021 Permalink | Reply

        
      from enigma import SubstitutedExpression, div
      
      # the alphametic puzzle 
      p = SubstitutedExpression(
        [
         # Brown won 4 holes on holes 10-17
         "sum([A, B, C, D, E, F, G, H]) == 4",
         # Brown won the last hole
         "I = 1",
         # Brown's profit (141 pence) is a multiple of the stake
         "div(141, sum([2 ** i if x else -1 * 2 ** i \
              for i, x in enumerate([A, B, C, D, E, F, G, H, I])]))",
        ],
        base=2,
        answer="(A, B, C, D, E, F, G, H, I), \
               sum([2 ** i if x else -1 * 2 ** i \
                   for i, x in enumerate([A, B, C, D, E, F, G, H, I])])",
        verbose=0,
        distinct="",
      )
      
      # print answer
      for (_, ans) in p.solve():
        print(f"Brown won the match and the holes "
              f"{[i + 10 for i, x in enumerate(ans[0]) if x]}"
              f" with side-stake of {141 // ans[1]} pence")
      

      Like

  • Unknown's avatar

    Jim Randell 4:35 pm on 17 September 2021 Permalink | Reply
    Tags:   

    Teaser 3078: Digital daisy-chains 

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

    The number 798 is a “digital daisy-chain”; i.e., if you spell out each of its digits as a word, then the last letter of each digit is the first letter of the next. Furthermore, the number 182 is a “looped” digital daisy-chain because, in addition, the last letter of its last digit is the first letter of its first digit.

    I have written down a large looped digital daisy-chain (with fewer than a thousand digits!). The total of its digits is itself a digital daisy-chain.

    What is that total?

    [teaser3078]

     
    • Jim Randell's avatar

      Jim Randell 5:14 pm on 17 September 2021 Permalink | Reply

      When forming a loop the individual “links” must be “821” or “83”, and both of these have a digit sum of 11.

      So the digit sum of any loop is a multiple of 11, and we just need to find a multiple of 11 that forms a chain.

      The shortest link is length 2, so we only need to consider loops with up to 499 links, i.e. a total of 5489.

      This Python program runs in 51ms.

      Run: [ @replit ]

      from enigma import irange, int2words, nsplit, tuples, printf
      
      # digits as words
      words = list(int2words(d) for d in irange(0, 9))
      
      # does a number form a chain?
      is_chain = lambda n: all(words[x][-1] == words[y][0] for (x, y) in tuples(nsplit(n), 2))
      
      # look for viable digit sums for loops
      for n in irange(1, 499):
        t = 11 * n
        if is_chain(t):
          printf("digit sum = {t}")
      

      Solution: The total of the digits in the loop is 583.

      The shortest loop would be “83” repeated 53 times (= 106 digits), and we can use “821” instead in any of the links giving us loops of up to 159 digits.

      The next highest possible sum would be 8382, which would require 762 links, so the loops would have between 1524 and 2286 digits.

      Like

      • Jim Randell's avatar

        Jim Randell 10:34 pm on 18 September 2021 Permalink | Reply

        As suggested by Frits below, it is faster to generate possible “chain” numbers, and look for those that are divisible by 11 (although I separated these conditions out, rather than interleaving them).

        Run: [ @replit ]

        from enigma import irange, int2words, group, subsets, unpack, div, printf
        
        # digits as words
        digits = irange(0, 9)
        words = list(int2words(d) for d in digits)
        
        # adjacency map for next digit
        adj = group(
          subsets(digits, size=2, select="M"),
          st=unpack(lambda x, y: words[x][-1] == words[y][0]),
          by=unpack(lambda x, y: x),
          f=unpack(lambda x, y: y),
        )
        
        # generate chain numbers less than M
        def chains(M):
          ss = list(digits)
          while ss:
            n = ss.pop(0)
            yield n
            if n:
              # expand the chain
              for d in adj.get(n % 10, ()):
                n_ = n * 10 + d
                if n_ < M:
                  ss.append(n_)
        
        # look for possible chain numbers ...
        for t in chains(500 * 11):
          # that are a multiple of 11
          n = div(t, 11)
          if n:
            printf("digit sum = {t}")
        

        Like

    • GeoffR's avatar

      GeoffR 9:39 pm on 17 September 2021 Permalink | Reply

      A programme with a similar basis to Jim’s solution.

      # Using Jim's number range/step 
      # Two digit numbers are 11, 22, 33, 44, 55, 66, 77, 88, 99 
      # They cannot be digital daisy chains as last letter
      # of first word is not the starting letter of second word
      
      from enigma import nsplit
      
      # Dictionary to look up words from digits
      DN = {0:'zero', 1:'one', 2:'two', 3:'three', 4:'four',
            5:'five', 6:'six', 7:'seven', 8:'eight', 9: 'nine'}
      
      for n in range(110, 5500, 11):
          # check three digit numbers
          if 100 < n < 1000:
              d1, d2, d3 = nsplit(n)
              # allocate words to digits
              w1 = DN[d1]
              w2 = DN[d2]
              w3 = DN[d3]
              # check last letter of one word is same
              # as first letter of next word
              if w1[-1] == w2[0] and w2[-1] == w3[0]:
                  print(f"Total is {n}.")
      
          # check four digit numbers
          if 1000 < n < 5500:
              d1, d2, d3, d4 = nsplit(n)
              # allocate words to digits
              w1 = DN[d1]
              w2 = DN[d2]
              w3 = DN[d3]
              w4 = DN[d4]
              # check last letter of one word is same
              # as first letter of next word
              if w1[-1] == w2[0] and w2[-1] == w3[0] \
                 and w3[-1] == w4[0]:
                  print(f"Total is {n}.")
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 12:32 pm on 18 September 2021 Permalink | Reply

      I did further work on digital daisy chains, finding 2, 3, 4 and 5-digit examples.

      Of the values found, five numbers were of the looped digital chain type i.e. 38, 83, 182, 218 and 821, with their digits summing to eleven.

      from itertools import permutations
      
      W = ['zero', 'one', 'two', 'three', 'four',
           'five', 'six', 'seven', 'eight', 'nine']
      
      for p in permutations(W, 2):
          w1, w2 = p
          if w1 == 'zero': continue
          if w1[-1] == w2[0]:
              print(w1, w2)
      
      # 18, 21, 38, 58, 79, 82, 83, 98
      print(); print()
      
      for p in permutations(W, 3):
          w1, w2, w3 = p
          if w1 == 'zero': continue
          if w1[-1] == w2[0] and w2[-1] == w3[0]:
              print(w1, w2, w3)
      
      # 182, 183, 218, 382, 582, 583, 798, 821, 982, 983
      
      # There were also 6 no. 4-digit digital daisy chains
      # i.e. 2183, 3821, 5821, 7982, 7983, 9821
      # A 5-digit digital daisy chain number was found i.e. 79821
      
      
      

      Like

    • Frits's avatar

      Frits 7:50 pm on 18 September 2021 Permalink | Reply

      Doing it a bit differently and not using modulo.

      dw = 'zero one two three four five six seven eight nine'.split()
      
      # credit: B. Gladman
      links = set((i, j) for j in range(10) for i in range(10) 
      			if dw[i][-1] == dw[j][0])
       
      # look for chains of 3 or 4 digits with the alternating sum  divisible by 11
      def chain(k, lst, rs=[], asum=0):
        if k == 5: return   # total is fewer than 5490
        else:  
          if k > 2 and asum in {-11, 0, 11}:
            t = int("".join(str(w) for w in (rs[0] + [y for x, y in rs[1:]])))
            if t <= 5489:	
              print("total =", t)
          for x, y in lst:
            if not rs or x == rs[-1][-1]:
      		# total must be a multiple of 11
      		# use alternating sum for testing divisibility by 11
              new = x - y if k == 1 else (-1)**k * y 
              chain(k + 1, lst, rs + [[x, y]], asum + new)
      
      # look for daisy-chain total
      chain(1, links)
      

      Like

  • Unknown's avatar

    Jim Randell 9:13 am on 16 September 2021 Permalink | Reply
    Tags: by: Adrian Winder   

    Brain-Teaser 186: [Adrian Winder’s problem] 

    From The Sunday Times, 15th November 1964 [link]

    A is 73 feet from a straight river, and B is on the same side of the river but not so far from it. M and N are the (distinct) points on the river nearest to A and B respectively. The lengths of AB, MN, and BN are whole numbers of feet.

    A man walks from A to B via the river, taking the shortest possible route, and this is also whole number of feet.

    How far does the man walk, and what is the direct distance from A to B?

    This puzzle was originally published with no title.

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

    The puzzle is included as a postscript to the puzzles in the book, and the following text appears in the foreword:

    Perhaps the most outstanding example of teasing is Brain Teaser 186, of 15th November 1964, which was based on a deceptively simple diagram by an undergraduate, Adrian Winder, who died in a road accident just before his puzzle was published. Few correct answers were submitted; within the year there were 250 requests for the full solution; and still, from time to time, yet another reader concedes defeat and begs to be relieved of his misery.

    Mr Winder’s problem, with his solution, is published as a fitting postscript to this collection.

    — Anthony French

    This brings the total number of puzzles available on S2T2 to 550, but this is less than 20% of the Brain Teaser puzzles published in The Sunday Times.

    [teaser186]

     
    • Jim Randell's avatar

      Jim Randell 9:14 am on 16 September 2021 Permalink | Reply

      The direct route (h = AB) and the indirect route (z = AB′ = z1 + z2) are the hypotenuses of right-angled triangles (AXB and AXB′ respectively):

      h² = x² + d²
      z² = (146 − x)² + d²

      For a given value of x we can determine values for h, d and z by looking at the divisors of x²:

      h² − d² = x²
      (h − d)(h + d) = x² = a⋅b
      h = (a + b) / 2
      d = (b − a) / 2
      z = √((146 − x)² + d²)

      This Python program runs in 51ms. (Internal run time is 1.3ms).

      from enigma import (irange, divisors_pairs, div, is_square, printf)
      
      for x in irange(1, 72):
        for (a, b) in divisors_pairs(x, 2):
          h = div(a + b, 2)
          d = div(b - a, 2)
          if not (h and d): continue
          t = 146 - x
          z = is_square(t * t + d * d)
          if not z: continue
          printf("x={x}; h={h} d={d} z={z}")
      

      Solution: The man walks 169 ft between A and B. The direct distance is 123 ft.

      B is 46 ft from the river (BN = 46). And M and N are 120 ft apart (MN = 120).

      Like

    • Frits's avatar

      Frits 11:32 am on 16 September 2021 Permalink | Reply

      This Python program runs in 75ms (with PyPy).

        
      from enigma import SubstitutedExpression, is_square
      
      # h^2 = x^2 + d^2                      d < 2592 ((72^2 - 1) / 2) 
      # z^2 = (146 – x)^2 + d^2  
      
      # the alphametic puzzle
      p = SubstitutedExpression([
          "DE < 26",
          "0 < XY < 73", 
          "is_square(XY**2 + DEFG**2) = HIJK",
          "is_square((146 - XY)**2 + DEFG**2) = ZABC",
          "DEFG > 0",
        ],
        answer="(ZABC, HIJK)",
        distinct="",        # allow symbols with same values
        d2i=dict([(k, "D") for k in range(3, 10)]),
        verbose=256,
      )
       
      # print answers
      for (_, ans) in p.solve(verbose=0):
        print(f"The man walks {ans[0]} ft between A and B. "
              f"The direct distance is {ans[1]} ft.")
      

      Like

    • Frits's avatar

      Frits 12:34 pm on 16 September 2021 Permalink | Reply

      This Python program runs in 4ms.

        
      from enigma import pythagorean_triples, is_square
      
      # h^2 = x^2 + d^2                      h < 2593 ((72^2 + 1) / 2) 
      # z^2 = (146 – x)^2 + d^2  
      # x < 73
      
      for (p, q, h) in pythagorean_triples(2592):
        if p > 72: continue
        if q > 72:
          # (x, d) must be (p, q)
          z = is_square((146 - p)**2 + q**2)
        else:
          # (x, d) can be (p, q) or (q, p)
          z = is_square((146 - p)**2 + q**2)
          if not z:
            z = is_square((146 - q)**2 + p**2)
          
        if z:
          print(f"The man walks {z} ft between A and B. "
                f"The direct distance is {h} ft.")
      

      Like

      • Jim Randell's avatar

        Jim Randell 1:58 pm on 16 September 2021 Permalink | Reply

        Here’s a version that only uses the list of Pythagorean triples (and doesn’t use [[ is_square() ]]).

        from enigma import (pythagorean_triples, ordered, printf)
        
        # we can show:
        #
        #   d <= (x^2 - 1) / 2
        #
        # and max x = 72, so:
        #
        #   d < 2592
        #
        # hence:
        #
        #   z^2 <= 145^2 + 2592^2
        #   z < 2597
        
        # find pythagorean triples, map: (a, b) -> c
        triples = dict(((a, b), c) for (a, b, c) in pythagorean_triples(2597))
        
        # consider triples
        for ((a, b), c) in triples.items():
          if a > 72: continue
          # consider x=a, d=b, h=c
          z = triples.get(ordered(b, 146 - a))
          if z:
            printf("x={a}; h={c} d={b} z={z}")
          if b < 73:
            # consider x=b, d=a, h=c
            z = triples.get(ordered(a, 146 - b))
            if z:
              printf("x={b}; h={c} d={a} z={z}")
        

        Although it is not quite as fast as my original program.

        Like

    • John Crabtree's avatar

      John Crabtree 2:52 pm on 22 September 2021 Permalink | Reply

      In theory there should be a way to solve brain teasers manually. Today I take this to mean without the use of computers or programable calculators. When this teaser was published in 1964, handheld calculators were some way off.

      Putting h = d + m and z = d + n, and then
      d = (x^2 – m^2)/(2m) = ((146 – x)^2 – n^2)/(2n) with n > m
      where if x is odd, m and n are odd, and if x ix even, m and n are even.
      Effectively, this is a way of finding Pythagorean triples with a common shorter side.

      Then for each value of x, one can look for values of m and n which give the same value of d. I did this with the aid of a calculator. I could have done it by hand with a table of squares, but it would have been tedious. The ratio of n/m is approximately (146 – x)^2/x^2. One can use this to reduce the number of calculations. The results took up two and half sides of paper. I found x = 27, m = 3, n = 49, which gave d = 120. The solution = d + n = 169.

      In summary, this is very challenging but accessible teaser. If Brian Gladman’s table of Pythagorean triples sorted by shorter sides went to 140, it would be very straightforward.

      Like

      • John Crabtree's avatar

        John Crabtree 5:02 pm on 22 September 2021 Permalink | Reply

        The man walks d + n = 169 feet. The direct distance is d + m = 123 feet.

        Like

  • Unknown's avatar

    Jim Randell 9:41 am on 14 September 2021 Permalink | Reply
    Tags:   

    Teaser 2822: Losing weight 

    From The Sunday Times, 23rd October 2016 [link] [link]

    The members of our local sports club are split into two groups. Originally the groups had the same number of members, and the first group contained member Waites who weighed 100kg (and indeed that was the average weight of the members of the first group). Then the club trainer decided that the groups were overweight and that, for each of the next ten weeks, for each group the average weight of the members should be reduced by one kilogram.

    This was achieved by simply transferring a member from the first group to the second group each week, and Waites was the tenth member to be transferred.

    How many members are there in the club?

    [teaser2822]

     
    • Jim Randell's avatar

      Jim Randell 9:42 am on 14 September 2021 Permalink | Reply

      This Python program considers the number of members in each group, and performs the transfers to see if the conditions of the puzzle are met.

      It runs in 54ms.

      from enigma import (irange, inf, printf)
      
      # S = starting average weight of group A
      # K = number of transfers from A -> B
      # W = weight of transfer number K
      (S, K, W) = (100, 10, 100)
      
      # suppose each group has n members
      for n in irange(K + 1, inf):
        m = 2 * n
        printf("n={n} ({m} members)")
        # initial total weight of the group is...
        t0 = S * n
        # K members of the group are transferred
        for k in irange(1, K):
          # the total weight becomes
          t1 = (S - k) * (n - k)
          w = t0 - t1
          # average weights for group A and group B
          (a, b) = (S - k, w + n + k - 1)
          printf("{k}: transfer = {w}, A avg {a0} -> {a}, B avg {b0} -> {b}", a0=a + 1, b0=b + 1)
          t0 = t1
      
        if w == W:
          printf("*** SOLUTION: n={n} ({m} members) ***")
          break
        printf()
      

      Solution: There are 38 members in the club.

      The average weight in Group 1 decreases by 1 kg per week from 100 to 90, and the average weight in Group 2 decreases by 1 kg per week from 138 to 128. After 10 weeks there are 9 members in Group 1 and 29 members in Group 2.


      Analytically:

      If we suppose each group has n members, the the total weight of Group 1 starts out at 100n.

      After the first transfer the average weight of Group 1 is 99 kg, so the total weight is 99(n − 1).

      And the difference between these two totals accounts for the weight of the first person transferred:

      w[1] = 100n − 99(n − 1)
      w[1] = n + 99

      After second transfer the average weight of Group 1 is 98kg, so the weight of the second person transferred is:

      w[2] = 99(n − 1) − 98(n − 2)
      w[2] = n + 97

      In general at the kth transfer, we have:

      w[k] = (100 − k + 1)(n − k + 1) − (100 − k)(n − k)
      w[k] = n + 101 − 2k

      And we are told the weight of the 10th person transferred is 100 kg:

      w[10] = 100
      n + 101 − 20 = 100
      n = 19

      So, there are 2n = 38 members in total.

      Like

  • Unknown's avatar

    Jim Randell 4:33 pm on 10 September 2021 Permalink | Reply
    Tags:   

    Teaser 3077: Shuffling series schedules 

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

    A TV company planned a set of programmes to fill a weekly slot (one programme per week for many weeks) with six consecutive series of three different types (Arts, Biography and Comedy). None of the series was followed by another of the same type (e.g. there could be an Arts series for three weeks then a Comedy series for four weeks and so on). Then it decided to change the order of the series within the same overall slot, but to minimise disruption it would not alter the gaps between series of the same type. It did this by scheduling each of the three Arts series 6 weeks earlier than first planned, each of the two Biography series 20 weeks later than first planned, and the Comedy series 21 weeks earlier than first planned.

    How many programmes are in each of the six series after the change, in order?

    [teaser3077]

     
    • Jim Randell's avatar

      Jim Randell 5:31 pm on 10 September 2021 Permalink | Reply

      The following Python program finds orders that satisfy the given conditions, and then choose a “before” and “after” ordering, and uses them to construct a collection of 6 equations in 6 variables, which it then solves for positive integer solutions.

      It runs in 56ms.

      Run: [ @replit ]

      from enigma import subsets, tuples, multiset, matrix, as_int, map2str, join, printf
      
      # the programs
      progs = "AAABBC"
      
      # label a sequence, e.g. label(ABCABA) -> (A1 B1 C1 A2 B2 A3)
      def label(ps):
        m = multiset()
        s = list()
        for p in ps:
          m.add(p)
          s.append(p + str(m.count(p)))
        return s
      
      # find orderings of the programs with no consecutive letters
      orders = list(label(s) for s in subsets(progs, size=len, select="mP") if all(x != y for (x, y) in tuples(s, 2)))
      
      # choose a before and after ordering
      for (before, after) in subsets(orders, size=2, select="P"):
      
        # name the variables
        vs = sorted(before)
      
        # construct a collection of equations
        eqs = list()
        for v in vs:
          eq = [0] * len(vs)
          # add in the after amounts
          for q in after:
            if q == v: break
            eq[vs.index(q)] += 1
          # and subtract the before amounts
          for q in before:
            if q == v: break
            eq[vs.index(q)] -= 1
          eqs.append(eq)
          
        # solve the equations, for positive integers
        try:
          rs = list(as_int(x, '+') for x in matrix.linear(eqs, [-6, -6, -6, 20, 20, -21]))
        except ValueError:
          continue
      
        # output solution
        printf("runs: {rs} [before = {bf}; after = {af}]",
          rs=map2str(zip(vs, rs), sep=" ", enc=""),
          bf=join(before, sep=" ", enc="()"),
          af=join(after, sep=" ", enc="()"),
        )
      

      Solution: The number of programmes in each series after the change are: 5, 6, 9, 6, 5, 6.

      The two schedules are:

      Before: B1 (6); A1 (5); B2 (6); A2 (9); C1 (6); A3 (5)
      After: A1 (5); C1 (6); A2 (9); B1 (6); A3 (5); B2 (6)


      Manually:

      Neither A1 nor C1 can start the “before” sequence (as the As and Cs must be earlier in the “after” sequence), so the before sequence must start with B1, and the 5 remaining spaces must be (A1, ?, A2, ? A3), in order for the As not to be consecutive:

      before = (B1, A1, C1, A2, B2, A3)
      before = (B1, A1, B2, A2, C1, A3)

      Both these sequences end in A3, so A3 cannot be last in the “after” sequence, and in order for the As to be non-consecutive we have (A1, ?, A2, ?, A3, ?), giving:

      after = (A1, B1, A2, B2, A3, C1)
      after = (A1, B1, A2, C1, A3, B2)
      after = (A1, C1, A2, B1, A3, B2)

      The first of these is eliminated as C1 must occur 21 weeks earlier than in “before”, so cannot come at the end. So B2 comes at the end.

      We can go ahead and look at the equations produced by the 4 possibilities:

      before = (B1, A1, C1, A2, B2, A3)
      after = (A1, B1, A2, C1, A3, B2)

      [A1] (0) − (B1) = −6 ⇒ B1 = 6
      [B1] (A1) − (0) = +20 ⇒ A1 = 20
      [C1] (A1 + B1 + A2) − (B1 + A1) = −20 ⇒ A2 = −20 [***impossible***]

      before = (B1, A1, C1, A2, B2, A3)
      after = (A1, C1, A2, B1, A3, B2)

      [A1] (0) − (B1) = −6 ⇒ B1 = 6
      [C1] (A1) − (B1 + A1) = −21 ⇒ −6 = −21 [***impossible***]

      So “before” must be (B1, A1, B2, A2, C1, A3):

      before = (B1, A1, B2, A2, C1, A3)
      after = (A1, B1, A2, C1, A3, B2)

      [B1] (A1) − (0) = +20 ⇒ A1 = 20
      [A1] (0) − (B1) = −6 ⇒ B1 = 6
      [A2] (A1 + B1) − (B1 + A1 + B2) = −6 ⇒ B2 = 6
      [C1] (A1 + B1 + A2) − (B1 + A1 + B2 + A2) = −21 ⇒ B2 = 21
      [B2] (A1 + B1 + A2 + C1 + A3) − (B1 + A1) = +20 ⇒ A3 = −7 [***impossible***]

      So, there is only one possibility left:

      before = (B1, A1, B2, A2, C1, A3)
      after = (A1, C1, A2, B1, A3, B2)

      [A1] (0) − (B1) = −6 ⇒ B1 = 6
      [A3] (A1 + C1 + A2 + B1) − (B1 + A1 + B2 + A2 + C1) = −6 ⇒ B2 = 6
      [A2] (A1 + C1) − (B1 + A1 + B2) = −6 ⇒ C1 = 6
      [C1] (A1) − (B1 + A1 + B2 + A2) = −21 ⇒ A2 = 9
      [B1] (A1 + C1 + A2) − (0) = +20 ⇒ A1 = 5
      [B2] (A1 + C1 + A2 + B1 + A3) − (B1 + A1) = +20 ⇒ A3 = 5

      Hence: A1=5, A2=9, A3=5, B1=6, B2=6, C1=6.

      Like

      • Frits's avatar

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

        @Jim

        Just wondering, how did you come up with this linear equations approach?

        Like

        • Jim Randell's avatar

          Jim Randell 1:46 pm on 12 September 2021 Permalink | Reply

          There are six unknowns we need to work out (the number of programs in each series), and we were given six known values (the difference between the start weeks of each series in the “before” and “after” schedules). So it seemed like a good candidate for a collection of 6 equations in 6 unknowns.

          The start week of each series is just the sum of the number of programs of the preceding series, so we can express the difference between the start weeks of a series in both schedules as the difference between these sums, and that gives us six linear equations in the six unknowns.

          The [[ matrix.linear() ]] solver from the enigma.py library will reject any system of equations that is inconsistent or incomplete, and we look for solutions where the results are all positive integers, and only a single answer popped out, and in a very reasonable time, so there was no need for any additional analysis.

          Manually, a little bit of analysis shows us that there are only 2 possible “before” sequences, and 2 possible “after” sequences, and only one of the 4 possible pairings give equations that solve to give positive integers.

          Like

  • Unknown's avatar

    Jim Randell 9:48 am on 9 September 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 859: Sick transit 

    From The Sunday Times, 8th January 1978 [link]

    The State of Inertia, in a last-ditch effort to revive an ailing economy, has decided to go ahead with the controversial innovation of adding two new digits

    POW! (Written ↑)
    WHAM! (Written ↓)

    thus symbolising the stark alternatives facing the nation.

    In a massive referendum on the relative merits, the country came down in favour of POW! carrying the greater weight and accordingly WHAM! is interposed in a lower position than POW! among the ten old digits, the usual order of which is retained.

    Teething troubles from the consequential change to duodecimal-based arithmetic and to the new values of some of the old digits, are minimised by the free provision to everyone of school-age or over of PEST, an appropriate Pocket Electronic Summary Tabulator.

    To enable a check to be made on the correct working of the instruments every PEST comes with the answers to 35 × 64 and 54 × 66, one consisting entirely of the new shapes and the other of neither of them.

    What are the two answers ?

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

    [teaser859]

     
    • Jim Randell's avatar

      Jim Randell 9:49 am on 9 September 2021 Permalink | Reply

      Using P for “POW!” and W for “WHAM!”:

      If we interpret “interposed” to mean that P and W are inserted between 2 existing symbols, then we find a unique solution.

      The following Python program runs in 51ms.

      Run: [ @replit ]

      from enigma import (subsets, irange, base2int, int2base, join, printf)
      
      # categorise a string
      # 'new' -> entirely composed of new symbols
      # 'old' -> entirely composed of old symbols
      # 'mix' -> a mixture of new and old symbols
      def categorise(s):
        s = set(s)
        assert bool(s)
        if s.issubset('PW'): return 'new'
        if not s.intersection('PW'): return 'old'
        return 'mix'
      
      # make possible base 12 symbol sets
      for (w, p) in subsets(irange(1, 9), size=2):
        syms = list("0123456789")
        syms.insert(p, 'P')
        syms.insert(w, 'W')
      
        # convert between strings and numbers
        s2n = lambda s: base2int(s, base=12, digits=syms)
        n2s = lambda n: int2base(n, base=12, digits=syms)
      
        # calculate: A = '35' * '64', B = '54' * '66'
        A = n2s(s2n('35') * s2n('64'))
        B = n2s(s2n('54') * s2n('66'))
        # one is entirely composed of new symbols and one entirely composed of old
        if set(map(categorise, (A, B))) == {'old', 'new'}:    
          printf("digits 0-11 = {syms}", syms=join(syms))
          printf("  35 * 64 = {A}; 54 * 66 = {B}")
      

      Solution: The two sums are: 35 × 64 = 2445 and 54 × 66 = ↓↑↑↓.

      The digits from 0 to 11 are represented by:

      0 1 2 3 W 4 5 P 6 7 8 9

      So the sums are:

      35 × 64 = 2445 [Inertial base 12]
      36 × 85 = 2556 [standard base 12]
      42 × 101 = 4242 [standard decimal]

      54 × 66 = WPPW [Inertial base 12]
      65 × 88 = 4774 [standard base 12]
      77 × 104 = 8008 [standard decimal]

      However, if we allow W and P to be inserted anywhere (but still with W before P), we find an additional solution using the following digits:

      W 0 1 2 3 P 4 5 6 7 8 9

      And the sums are:

      35 × 64 = 2194 [Inertial base 12]
      47 × 86 = 32B6 [standard base 12]
      55 × 102 = 5610 [standard decimal]

      54 × 66 = PPWW [Inertial base 12]
      76 × 88 = 5500 [standard base 12]
      90 × 104 = 9360 [standard decimal]

      We can see this additional solution by using the following call to [[ subsets() ]] in line 15:

      subsets(irange(0, 10), size=2, select='R')
      

      Like

  • Unknown's avatar

    Jim Randell 10:37 am on 7 September 2021 Permalink | Reply
    Tags: ,   

    Teaser 2820: Three ages 

    From The Sunday Times, 9th October 2016 [link] [link]

    Today is Alan’s, Brian’s and Colin’s birthday. If I write down their ages in a row in that order then I get a six-figure number. If I write down their ages in a row in the reverse order (i.e., Colin’s followed by Brian’s followed by Alan’s) then I get a lower six-figure number. When I divide the difference between these two six-figure numbers by the total of their three ages the answer is Alan’s age multiplied by Colin’s.

    What are Alan’s, Brian’s and Colin’s ages?

    As published there are 2 possible solutions to this puzzle.

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

    [teaser2820]

     
    • Jim Randell's avatar

      Jim Randell 10:38 am on 7 September 2021 Permalink | Reply

      If we assume the ages have 2 digits each (something not mentioned in the puzzle text), then we can quickly use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this problem.

      The following run file executes in 187ms.

      #! python3 -m enigma -rr
      
      # A = UV
      # B = WX
      # C = YZ
      
      SubstitutedExpression
      
      --distinct=""
      
      "(UV * YZ) * (UV + WX + YZ) + YZWXUV = UVWXYZ"
      

      And this finds two viable solutions.

      However it is not a great deal of work to write a program that considers ages that include 1-digit and 3-digit ages (with a suitable upper limit).

      The following Python program runs in 179ms, and finds the same two solutions.

      Run: [ @replit ]

      from enigma import (irange, printf)
      
      # permissible ages
      ages = irange(1, 120)
      
      for A in ages:
        for B in ages:
          AB = str(A) + str(B)
          if len(AB) > 5: break
          for C in ages:
            ABC = AB + str(C)
            if len(ABC) > 6: break
            if len(ABC) < 6: continue
            d = int(ABC) - int(str(C) + str(B) + str(A))
            if A * C * (A + B + C) == d:
              printf("A={A} B={B} C={C} [A:B:C={A}{B}{C} C:B:A={C}{B}{A}; d={d}]")
      

      Solution: The are two possible answers: Alan = 22, Brian = 61, Colin = 18; Alan = 99, Brian = 70, Colin = 33.

      These could be reduced to a single solution by adding one of the following conditions:

      (a) “None of the ages include the digit 0” → (22, 61, 18)
      (b) “Alan is older than Brian, who is older than Colin” → (99, 70, 33)

      The published solution is: “22, 61, 18 (or 99, 70, 33)”.

      We can run the Python program above to consider ages up to 9999, and we find that without constraining the values of A, B, C there are 5 possible solutions:

      (22, 61, 18)
      (37, 326, 3)
      (51, 830, 3)
      (78, 252, 6)
      (99, 70, 33)

      Like

    • GeoffR's avatar

      GeoffR 12:13 pm on 7 September 2021 Permalink | Reply

      # Using 2-digit ages: Alan = ab, Brian = cd, Colin = ef
      for ab in range(10, 100):
        for cd in range (10, 100):
          for ef in range(10, 100):
            abcdef = 10000*ab + 100*cd + ef  # ages in order
            efcdab = 10000*ef + 100*cd + ab  # ages in reverse order
            if abcdef < efcdab:continue
            diff = abcdef - efcdab
            if diff == (ab * ef) * (ab + cd + ef):
              print(f"Alan = {ab}, Brian = {cd}, Colin = {ef}")
              
      # Alan = 22, Brian = 61, Colin = 18
      # Alan = 99, Brian = 70, Colin = 33
      
      
      

      Like

      • Frits's avatar

        Frits 2:46 pm on 7 September 2021 Permalink | Reply

          
        # Using 2-digit ages: Alan = A, Brian = B, Colin = C
        # difference <d> does not depend on B
        for A in range(10, 100):
          sA = str(A)
          for C in range(10, A):
            # use dummy "10" for the value of B
            d = int(sA + "10" + str(C)) - int(str(C) + "10" + sA)
            (sumABC, r) = divmod(d, A * C)
            if r != 0: continue
            B = sumABC - A - C
            if not (9 < B < 100): continue
            print(f"Alan = {A}, Brian = {B}, Colin = {C}")
        

        Like

        • Jim Randell's avatar

          Jim Randell 4:11 pm on 7 September 2021 Permalink | Reply

          Or (still assuming 2-digit ages):

          from enigma import (irange, subsets, div, printf)
          
          # assuming 2 digit ages
          for (C, A) in subsets(irange(10, 99), size=2):
            B = div(9999 * (A - C), A * C)
            if B is None: continue
            B -= A + C
            if B < 10 or B > 99: continue
            printf("A={A} B={B} C={C}")
          

          or:

          #! python3 -m enigma -rr
          SubstitutedExpression
          
          --distinct=""
          
          "div(9999 * (UV - YZ), UV * YZ) - UV - YZ = WX"
          

          Like

      • Frits's avatar

        Frits 6:18 pm on 7 September 2021 Permalink | Reply

         
        # Using 2-digit ages: Alan = A, Brian = B, Colin = C
        #
        #  d = 9999 * (A - C) = (A * C) * (A + B + C)
        #    9999 = 3 * 3 * 11 * 101
        #
        # as A * C cannot be a multiple of 101 the sum A + B + C must be 101 or 202
        # this means that A * C must be a multiple of 99
        #            and (A * C) / (A - C) is 49.5 or 99
        
        for A in [x for x in range(11, 100) if x % 3 == 0 or x % 11 == 0]:
         for i, y in enumerate([A + 99, 2 * A + 99]):
            (C, r) = divmod(99 * A, y)
            if r > 0 or not (9 < C < 99): continue
            
            B = 101 - A - C if i == 0 else 202 - A - C
            if not (9 < B < 100): continue     # probably not needed
            
            print(f"Alan = {A}, Brian = {B}, Colin = {C}") 
        

        Like

      • Frits's avatar

        Frits 9:22 pm on 7 September 2021 Permalink | Reply

        Allowing for ages up to 999, again we don’t need to loop over B.

          
        # allow for high 3-digit ages
        for A in range(10, 1000):
          sA = str(A)
          
          if A < 100:
            # see previous posting
            rangeC = list(range(1, 10)) + \
                     [int((99 * A) / (A + 99)), int((99 * A) / (2 * A + 99))] 
          else:
            rangeC = range(1 , A  % 100)
          
          for C in rangeC:
            sC = str(C)
            if sC[0] > sA[0]: continue
            AC = sA + sC
            if len(sA + sC) > 5: break
            
            prodAC = A * C
            
            # allow Brian to be born on 9th October 2016
            minB = 10**(5 - len(AC)) if len(AC) != 5 else 0
            
            # how much does d decrement due to one increment of B? 
            if len(sA) > len(sC):
              difB = int((len(sA) - len(sC)) * "9" + "0")
            else:
              difB = 0
            
            # calculate difference for first B entry
            d = int(sA + str(minB) + sC) - int(sC + str(minB) + sA)  
            
            # rule: (A * C) * (A + minB + C) + n * (A * C) = d - n * difB 
            (n, r) = divmod(d - prodAC * (A + minB + C), prodAC + difB)
            
            if r > 0 or n < 0: 
              continue
            
            B = minB + n
            sB = str(B)
            
            ABC = sA + sB + sC
            if len(ABC) != 6: continue
            d = int(ABC) - int(sC + sB + sA)
            if prodAC * (A + B + C) == d:
              print(f"A={A} B={B} C={C} [A:B:C={A}{B}{C} C:B:A={C}{B}{A}; d={d}]")
        

        Like

        • Jim Randell's avatar

          Jim Randell 10:20 pm on 7 September 2021 Permalink | Reply

          Here’s my take on isolating B from the equation, by adjusting the definition of [[ ages() ]] you can allow whatever range of ages you want (including up to 9999).

          from itertools import product
          from enigma import (irange, div, printf)
          
          # decompose t into n numbers, min m
          def decompose(t, n, m=1, s=[]):
            if n == 1:
              if not (t < m):
                yield s + [t]
            else:
              for x in irange(m, t - n + 1):
                yield from decompose(t - x, n - 1, m, s + [x])
          
          # acceptable k digit ages
          def ages(k):
            if k == 3:
              return irange(100, 120)
            if k < 3:
              return irange(10**(k - 1), (10**k) - 1)
          
          # consider number of digits a, b, c; a + b + c = 6
          for (a, b, c) in decompose(6, 3):
            # age ranges
            (rA, rB, rC) = (ages(k) for k in (a, b, c))
            if not (rA and rB and rC): continue
            # multipliers
            mA = 10**(b + c) - 1
            mB = (10**c) - (10**a)
            mC = 10**(a + b) - 1
            # consider values for A and C
            for (A, C) in product(rA, rC):
              AC = A * C
              B = div(A * mA - C * mC - AC * (A + C), AC - mB)
              if B is not None and B in rB:
                printf("A={A} B={B} C={C}")
          

          Like

          • Frits's avatar

            Frits 10:53 pm on 7 September 2021 Permalink | Reply

            @Jim, very concise.

            Almost twice as fast with PyPy (for ages up to 999) although having 4 times as many (innermost) loops (187920 and 44649).

            Like

          • Frits's avatar

            Frits 12:16 pm on 8 September 2021 Permalink | Reply

            @Jim,

            While trying decompose(11, 3) I got into memory problems (5,6 GB memory used).

            Although itertools product is supposed to be a generator the problem disappeared when using separate loops for A and C (25 MB memory used).

            Like

            • Jim Randell's avatar

              Jim Randell 12:41 pm on 8 September 2021 Permalink | Reply

              I suspect internally [[ product() ]] is remembering all the values of the inner loops, because it doesn’t know that range objects are restartable iterators. Which will end up using a lot of memory if the ranges are large.

              So in the general case it would be better to use two for loops. (Which was how it was originally before I decided to save a line and a level of nesting).

              Like

          • Jim Randell's avatar

            Jim Randell 4:00 pm on 9 September 2021 Permalink | Reply

            Since I end up writing [[ decompose() ]] functions a lot in puzzles, I’ve added a helper function to enigma.py to generate them for you (see [[ Decompose() ]] and [[ decompose() ]]).

            For example, this program becomes:

            from enigma import (decompose, irange, div, printf)
            
            # acceptable <k> digit ages
            def ages(k):
              if k == 3:
                return irange(100, 120)
              if k < 3:
                return irange(10**(k - 1), (10**k) - 1)
            
            # consider number of digits (a, b, c), a + b + c = 6
            for (a, b, c) in decompose(6, 3, increasing=0, sep=0, min_v=1):
              # age ranges
              (rA, rB, rC) = (ages(k) for k in (a, b, c))
              if not (rA and rB and rC): continue
              # multipliers
              mA = 10**(b + c) - 1
              mB = (10**c) - (10**a)
              mC = 10**(a + b) - 1
              # consider values for A and C
              for A in rA:
                for C in rC:
                  AC = A * C
                  B = div(A * mA - C * mC - AC * (A + C), AC - mB)
                  if B is not None and B in rB:
                    printf("A={A} B={B} C={C}")
            

            Like

    • Frits's avatar

      Frits 5:52 pm on 9 September 2021 Permalink | Reply

      @Jim, Thanks, I will look into it.

      Calculating rC after A is known is faster:

        
      rC = range(10 ** (c - 1), (int(str(A)[0]) + 1) * 10 ** (c - 1))
      

      Like

    • Frits's avatar

      Frits 2:07 pm on 10 September 2021 Permalink | Reply

      Finding a galactic object for “Brian” 9.5 billion years old.
      This program runs in 8 seconds with PyPy.

       
      from enigma import decompose
      from math import ceil
      
      # acceptable k digit ages
      def ages(k):
        if k == 11:
          # universe is approx. 13.8 billion years old
          return range(10 ** (k - 1), 138 * 10 ** (k - 3))
        else:  
          return range(10 ** (k - 1), (10 ** k))
        
      L = 13  # number of digits concatenated A, B and C
      
      print("number of digits =", L)
      cnt = 0 
      cntsols = 0
      
      # consider number of digits a, b, c; a + b + c = L
      for (a, b, c) in decompose(L, 3, increasing=0, sep=0, min_v=1):
        # skip if A * C * (A + B + C) will have too many digits
        if 2 * a + c - 2 > L or 2 * c + a - 2 > L: 
          continue
        
        # group A range by first digit
        rA = [range(i * 10**(a - 1),  i * 10**(a - 1) + 10**(a - 1)) 
              for i in range(1, 10)]
        rB = ages(b)
        
        # multipliers
        mA = 10 ** (b + c) - 1
        mB = (10 ** c) - (10 ** a)
        mC = 10 ** (a + b) - 1
        
        # consider values for A and C
        for i, grp in enumerate(rA, 1):
          rC = range(10 ** (c - 1), (i + 1) * 10 ** (c - 1))
          
          # check first entry in C loop (see below)
          A = i * 10**(a - 1)  
          C = rC[0]
          if A * C == mB: 
            C = rC[1]     # next entry, we don't want to divide by zero
          
          numeratorB = A * mA - C * mC - A * C * (A + C)
          denominatorB = A * C - mB
          
          # numeratorB decreases and denominatorB increases as C becomes higher
          (B, r) = divmod(numeratorB, denominatorB)
          
          d1 = A * mA + B * mB - C * mC
          if d1 <= 0:    
            # we need a positive distance
            if numeratorB > 0: 
              # check when numeratorB becomes negative
              # (mC + A**2 + A * C) * C - A * mA = 0
              # quadratic equation: A * C**2 + (mC + A**2) * C - A * mA = 0
              newC1 = (-1 * (mC + A**2) + ((mC + A**2)**2 + 4 * A**2 * mA)**.5) 
              newC1 /= (2 * A)
              newC2 = mB / A      # when will denominatorB become positive again?
              newCs = sorted([newC1, newC2])
              low = ceil(newCs[0])
              hgh = int(newCs[1])
              rC = range(low, hgh + 1)
         
          for A in grp:
            for C in rC:
              cnt += 1
              AC = A * C
              
              # difference rule: A * mA + B * mB - C * mC = A * C * (A + B + C)
              if AC == mB: continue  # don't divide by zero
              
              (B, r) = divmod(A * mA - C * mC - AC * (A + C), AC - mB)
              
              if B < 0: break  
              
              if r == 0 and B in rB:
                d1 = int(str(A) + str(B) + str(C)) - int(str(C) + str(B) + str(A))
                d2 = AC * (A + B + C)
                
                print(f"A={A} B={B} C={C}, AC={AC} diff1={d1} diff2={d2}")
                cntsols += 1
            
      print("number of solutions", cntsols, "loop count =", cnt) 
      

      Like

    • GeoffR's avatar

      GeoffR 1:49 pm on 12 September 2021 Permalink | Reply

      
      ' A Solution in Visual Basic 2019
      Module Module1
        Public Sub Main()
          Dim A, B, C As Integer ' for Alan, Brian and Colin
          Dim AGES, Rev_AGES, DIFF_AGES As Integer
          For A = 10 To 99
            For B = 10 To 99
              If B = A Then
                Continue For
              End If
              For C = 10 To 99
                If C = B Or C = A Then
                  Continue For
                End If
                AGES = 10000 * A + 100 * B + C
                Rev_AGES = 10000 * C + 100 * B + A
                If AGES > Rev_AGES Then
                  DIFF_AGES = AGES - Rev_AGES
                  If DIFF_AGES = (A + B + C) * (A * C) Then
                    Console.WriteLine("Alan = {0}, Brian = {1}, Colin = {2}", A, B, C)
                  End If
                End If
              Next   'C
            Next   'B
          Next   'A
          Console.ReadKey()   'Freeze console screen
        End Sub
      End Module
      
      'Alan = 22, Brian = 61, Colin = 18
      'Alan = 99, Brian = 70, Colin = 33
      
      
      
      

      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