Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:34 pm on 1 March 2024 Permalink | Reply
    Tags:   

    Teaser 3206: Support measures 

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

    Two buildings on level ground with vertical walls were in need of support so engineers placed a steel girder at the foot of the wall of one building and leaned it against the wall of the other one. They placed a shorter girder at the foot of the wall of the second building and leaned it against the wall of the first one. The two girders were then welded together for strength.

    The lengths of the girders, the heights of their tops above the ground, the distance between their tops and the distance between the two buildings were all whole numbers of feet. The weld was less than ten feet above the ground and the shorter girder was a foot longer than the distance between the buildings.

    What were the lengths of the two girders?

    [teaser3206]

     
    • Jim Randell's avatar

      Jim Randell 4:53 pm on 1 March 2024 Permalink | Reply

      See: Enigma 775 for the calculation of the height of the weld above the ground.

      If the distance between the buildings is d, and the girders have lengths g1 and g2, and the top ends of the girders meet the walls at heights of h1 and h2, and cross each other at a height of H, and the tops are a distance z apart, then we have the following three Pythagorean triangles:

      (d, h1, g1)
      (d, h2, g2)
      (d, h1 − h2, z)

      subject to:

      h1 > h2
      g2 = d + 1
      H = (h1 × h2) / (h1 + h2) < 10

      This Python program considers possible Pythagorean triangles for the girders (up to a reasonable maximum length).

      It runs in 59ms. (Internal runtime is 409µs).

      from enigma import (defaultdict, pythagorean_triples, ihypot, fdiv, printf)
      
      # index pythagorean triples by non-hypotenuse sides
      ts = defaultdict(list)
      for (x, y, z) in pythagorean_triples(250):
        ts[x].append((y, z))
        ts[y].append((x, z))
      
      # consider possible distances
      for (d, vs) in ts.items():
        # the second girder
        for (h2, g2) in vs:
          if not (g2 == d + 1): continue
      
          # the first girder
          for (h1, g1) in vs:
            if not (h1 > h2): continue
            # check the height of the weld is < 10
            if not (h1 * h2 < 10 * (h1 + h2)): continue
            # calculate the distance between the tops
            z = ihypot(h1 - h2, d)
            if z is None: continue
      
            # output solution
            H = fdiv(h1 * h2, h1 + h2)
            printf("g1={g1} g2={g2}; h1={h1} h2={h2} H={H:.2f}; d={d} z={z}")
      

      Solution: The girders were 109 ft and 61 ft long.

      The distance between the buildings is 60 ft, so the girders reach 11 ft and 91 ft up the walls, and the weld is about 9.81 ft above the ground (= 1001/102). The tops of the girders are 100 ft apart.


      Without the restriction on the height of the weld there is a further theoretical solution where the buildings are 6160 ft apart, and the girders have lengths of 9050 ft and 6161 ft (they reach 6630 ft and 111 ft up the buildings, and cross at a height of 109.17 ft).

      But this is the only other solution for girders with lengths up to 10 million feet.

      Like

      • Frits's avatar

        Frits 8:09 am on 2 March 2024 Permalink | Reply

        @Jim, you seem to reject the possibility of h1 = 2 * h2 as then the “ts” entry may only have 2 entries.

        Like

        • Jim Randell's avatar

          Jim Randell 8:18 am on 2 March 2024 Permalink | Reply

          @Frits: Yes, I was supposing the three Pythagorean triangles were all different. Which is not necessarily the case.

          Like

      • Frits's avatar

        Frits 1:42 pm on 2 March 2024 Permalink | Reply

        Using analysis of my other program.

        from enigma import (defaultdict, pythagorean_triples, subsets)
        
        # set of valid widths
        ws = {2 * n * (n + 1) for n in range(1, 10)}
        
        M = 2717 # maximum any length
        # index pythagorean triples by non-hypotenuse sides
        ts = defaultdict(list)
        for (x, y, z) in pythagorean_triples(M):
          if x in ws:
            ts[x].append((y, z))
          if y in ws:
            ts[y].append((x, z))
        
        # smaller height must be less than 20
        for k, vs in sorted(ts.items()):
          svs = sorted(vs)
          # pick two Pythagorean triangles
          for (c1, c2) in subsets(svs, 2):
            if c1[0] > 19: break
            if 2 * c1[0] == c2[0]:
              print(f"answer: {c1[1]} and {c2[1]}")
        
          # pick three Pythagorean triangles
          for (c1, c2 ,c3) in subsets(svs, 3):
            if c1[0] > 19: break
            if c1[0] + c2[0] == c3[0]:
              print(f"answer: {c1[1]} and {c3[1]}")
              if c2[0] < 20:
                print(f"    or  {c2[1]} and {c3[1]}")
        

        Like

      • Frits's avatar

        Frits 2:05 pm on 2 March 2024 Permalink | Reply

        Similar

            
        from enigma import (defaultdict, pythagorean_triples, subsets)
        
        # set of valid widths
        ws = {2 * n * (n + 1) for n in range(1, 10)}
        
        M = 2717 # maximum any length 
        # index pythagorean triples by non-hypotenuse sides
        ts = defaultdict(list)
        for (x, y, z) in pythagorean_triples(M):
          if x in ws: 
            ts[x].append((y, z))
          if y in ws:
            ts[y].append((x, z))
        
        # smaller height must be less than 20
        for k, vs in sorted(ts.items()):
          svs = sorted(vs)
          # pick two Pythagorean triangles
          for (c1, c2) in subsets(svs, 2):
            if c1[0] > 19: break
            if 2 * c1[0] == c2[0]:
              print(f"answer: {c1[1]} and {c2[1]}")
            
            if (c3 := (m := (c1[0] + c2[0]), (k**2 + m**2)**.5)) in vs: 
              print(f"answer: {c1[1]} and {c3[1]}")
              if c2[0] < 20:
                print(f"    or  {c2[1]} and {c3[1]}")
        

        Like

        • Frits's avatar

          Frits 2:23 am on 5 March 2024 Permalink | Reply

          To explore the full solution set we can use maximum length 16199:

          # w^2 + h^2 = z^2   (with even w) 
          # look for hypotenuse <z> so that (z + 1)^2 - z^2 > w^2
          M = (max(ws)**2 - 1) // 2   # maximum any length
          

          Like

    • Frits's avatar

      Frits 8:05 am on 2 March 2024 Permalink | Reply

      There is an upper bound for the width and/or smaller height but not always for the greater height.

          
      from enigma import is_square
      
      #   |\     |        whole numbers: g1, g2, h1, h2, g3 and w
      #   | \    |        g1^2 = h1^2 + w^2
      # h1|  \g1 |        g2^2 = h2^2 + w^2
      #   |   \ _/        g3^2 = (h1 - h2)^2 + w^2
      #   | g2/\ |h2      g2 = w + 1 
      #   |_/___\|        h3 < 10
      #       w            
      
      # h2^2 = g2^2 - w^2 = 2w + 1
      
      # we have:
      # 1/h3 = 1/h1 + 1/h2 with h3 < 10
      
      # 1/h1 + 1/h2 > 0.1  or 10 * (h1 + h2) > h1 * h2)
      # (h2 - 10) * h1 < 10 * h2 or h1 < 10 * h2 / (h2 - 10)
      
      # also h1 > h2 so when is 10 * h2 / (h2 - 10) equal to h2?
      # h2^2 - 20h2 = 0, h2(h2 - 20) = 0 --> h2 = 20 
      # w < 200 and h2 < 20 
      
      # h2 must be odd as h^2 = 2w + 1 --> h2 = 2n + 1, h2 < 20, n < 10
      for n in range(1, 10):
        h2 = 2 * n + 1
        # h2^2 = 4n^2 + 4n + 1 = 2w + 1
        w = 2 * n * (n + 1)
        w2 = w * w
        ub_h1, r = divmod(10 * h2, h2 - 10)
        if not r: 
          ub_h1 -= 1
        if ub_h1 < 0: 
          ub_h1 = 2717 # Burj Khalifa
      
        for h1 in range(h2 + 1, ub_h1 + 1):
          if not (g1 := is_square(h1**2 + w2)): continue
          if not (g3 := is_square((h1 - h2)**2 + w2)): continue
          print(f"answer: {(g2 := w + 1)} and {g1}")
      

      Like

    • GeoffR's avatar

      GeoffR 2:29 pm on 2 March 2024 Permalink | Reply

      Using WP Reader to post a MiniZinc solution.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Assumed UB of 120 for the six integers required
      var 1..120:g1; var 1..120:g2; var 1..120:g3; var 1..120:w;
      var 1..120:h1; var 1..120:h2; var 1.0..10.0:h3;
      
      % Reusing Frits diagram for this solution
      % g1 and g2 are the girder lengths, w is width between walls
      % h1 and h2 are wall heights where girders meet the walls
      % g3 is the distance between their tops, h3 the weld height
      
      constraint g2 == w + 1 /\ g2 < g1;
      constraint g1 * g1 == h1 * h1 + w * w;
      constraint g2 * g2 == h2 * h2 + w * w;
      constraint g3 * g3 == (h1 - h2) * (h1 - h2) + w * w;
      
      solve satisfy;
      
      output["g1, g2, g3 = " ++ show([g1, g2, g3]) ++
      "\n" ++ "h1, h2, w = " ++ show([h1, h2, w]) ];
      

      Like

      • Jim Randell's avatar

        Jim Randell 7:47 am on 3 March 2024 Permalink | Reply

        @GeoffR: I think for a complete solution you also need a constraint for the weld height (although in this case it doesn’t eliminate any solution candidates).

        My MiniZinc specification looks like this:

        %#! python3 -m minizinc use_embed=1
        
        {var("1..250", ["d", "g1", "g2", "h1", "h2", "z"])};
        
        predicate is_pythagorean(var int: x, var int: y, var int: z)
          = (x * x + y * y = z * z);
        
        % first girder
        constraint is_pythagorean(d, h1, g1);
        
        % second girder
        constraint is_pythagorean(d, h2, g2);
        constraint h2 < h1;
        constraint g2 = d + 1;
        
        % weld height H = (h1 * h2) / (h1 + h2)
        constraint h1 * h2 < 10 * (h1 + h2);
        
        % tops
        constraint is_pythagorean(d, h1 - h2, z);
        
        solve satisfy;
        

        Like

    • GeoffR's avatar

      GeoffR 4:31 pm on 3 March 2024 Permalink | Reply

      @Jim:

      A neat MiniZinc solution.

      Yes, the weld height would give a complete solution, although this is not a strict requirement of the teaser description.

      I was more interested in the derivation of your formula for the weld height.

      If x is the distance from left wall h1 to the weld height (h3), by similar triangles:

      h3/x = h2/w and h1/w = h3/(w – x).

      Algebraic manipulation gives 1/h3 = 1/h1 + 1/h2
      or h3 = h1.h2/(h1 + h2).

      As Brian has mentioned to me, this is the same formula in optics for the focal length, object and image distances, or two resistors in parallel.

      Like

    • Frits's avatar

      Frits 7:06 am on 5 March 2024 Permalink | Reply

      I believe this program fully explores the solution set (without calculating complex upper bounds).

      from math import isqrt
      
      # for any pythagorean triangle with side y and hypothenuse z: 
      # if z = y + i then the square of other side = 2 * i * y + i^2
      
      # index pythagorean triples by non-hypotenuse sides
      # set of valid widths (h2 = 2n + 1)
      ts = {w: [(dm[0], dm[0] + i) for i in range(w - 2, 0, -2) 
                if (dm := divmod(w**2 - i**2, 2 * i))[1] == 0] 
                for w in [2 * n * (n + 1) for n in range(1, 10)]}
      
      # smaller height must be less than 20
      for n, (w, vs) in enumerate(ts.items(), start=1):
        # set up pythagorean triangle for smaller height 
        t1 = (2 * n + 1, w + 1) 
        # pick other Pythagorean triangles
        for t2 in vs:
          if t2 == t1: continue
          
          # we have a solution if greater height is double the small height
          if 2 * t1[0] == t2[0]:
            print(f"answer: {t1[1]} and {t2[1]} feet")
          
          # for non-hypotenuse sides h2 and (h1 - h2) check if h1^2 + w^2 is square
          if (z := isqrt((z2 := w**2 + (t1[0] + t2[0])**2)))**2 == z2:
            print(f"answer: {t1[1]} and {z} feet")
            if t2[0] < 20:
              print(f"    or  {t2[1]} and {z} feet")
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:32 pm on 5 March 2024 Permalink | Reply

        @Frits: Well done on reaching a complete solution.

        I have to admit I just did a search for “longest steel girder” and chose a suitable upper value. (And I was surprised that the actual answer involved girders that were so long).

        But I thought I would do a complete solution too:

        If we know one of the non-hypotenuse sides of a Pythagorean triangle, say a in the triple (a, b, c), where c is the hypotenuse, then we have:

        a² = c² − b² = (c + b)(c − b)

        So by dividing a² into 2 divisors (say x and y), then we can find potential (b, c) pairs to make a Pythagorean triple as follows:

        b = |x − y| / 2
        c = (x + y) / 2

        And there are only a finite number of divisor pairs, so there are only a finite number Pythagorean triangles that share a leg.

        We can now provide a complete solution to the problem.

        As Frits notes above, h2 must be an odd number between 3 and 19, and for a given value of h2 we can recover the distance between the buildings d and the length of the shorter girder g2:

        d = (sq(h2) − 1) / 2
        g2 = d + 1

        So we can consider possible values for h2, calculate the corresponding value for d and then generate all possible Pythagorean triangles that have one leg d, and look for those with a longer g1 and corresponding h1, and then we can check for those that give an integer distance between the tops.

        This Python program has an internal runtime of 442µs.

        Run: [ @replit ]

        from enigma import (irange, sq, div, ihypot, divisors_pairs, fdiv, printf)
        
        # h2 is odd, < 20
        for h2 in irange(3, 19, step=2):
          d = div(sq(h2) - 1, 2)
          g2 = d + 1
        
          # look for pythagorean triples with a leg of d
          for (y, x) in divisors_pairs(d, 2):
            (h1, g1) = (div(x - y, 2), div(x + y, 2))
            if (not h1) or (not g1): continue
            # does this give a longer girder
            if not (g1 > g2): continue
            # check weld height H < 10
            if not (h1 * h2 < 10 * (h1 + h2)): continue
            # check distance between tops
            z = ihypot(d, h1 - h2)
            if z is None: continue
            # output solution
            H = fdiv(h1 * h2, h1 + h2)
            printf("g1={g1} g2={g2}; h1={h1} h2={h2} H={H:.2f}; d={d} z={z}")
        

        Like

        • Frits's avatar

          Frits 1:32 am on 8 March 2024 Permalink | Reply

          @Jim, I keep forgetting this trick.

          Processing the divisor pairs in reverse order allows for an early break with respect to the weld height check.

          Like

  • Unknown's avatar

    Jim Randell 9:06 am on 29 February 2024 Permalink | Reply
    Tags:   

    Teaser 2602: Over fifty years 

    From The Sunday Times, 5th August 2012 [link] [link]

    We have now had over fifty years of Teasers and to celebrate this I found three special numbers using only non-zero digits and I wrote the numbers down in increasing order. Then I consistently replaced all the digits by letters, with different letters for different digits.

    In this way the numbers became:

    FIFTY
    YEARS
    TEASER

    In fact the third number was the sum of the other two.

    Which number does TEASER represent?

    The first numbered Teaser puzzle was published 63 years ago on 26th February 1961 (although there was a gap of almost a year in 1978/9). However the earliest Teaser puzzle I have found was published in The Sunday Times on 2nd January 1949.

    [teaser2602]

     
    • Jim Randell's avatar

      Jim Randell 9:07 am on 29 February 2024 Permalink | Reply

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

      It runs in 64ms. (Internal runtime of the generated program is 445µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      --invalid="0,AEFIRSTY"
      
      "FIFTY + YEARS = TEASER"
      
      --extra="F < Y"
      

      Solution: TEASER = 158453.

      The complete sum is:

      62619 + 95834 = 158453

      Liked by 1 person

      • GeoffR's avatar

        GeoffR 3:12 pm on 29 February 2024 Permalink | Reply

        Trying a posting using WP Reader…

        from itertools import permutations
        
        for p1 in permutations ('123456789', 8):
            F, I, T, Y, E, A, R, S = p1
            if int(F) < int(Y):
                FIFTY = int(F + I + F + T + Y)
                YEARS = int(Y + E + A + R + S)
                TEASER = int(T + E + A + S + E + R)
                if FIFTY + YEARS == TEASER:
                    print(f"Sum is {FIFTY} + {YEARS} = {TEASER}")
        
        # Sum is 62619 + 95834 = 158453
        

        Like

  • Unknown's avatar

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

    Brain teaser 980: Is anybody there? 

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

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

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

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

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

    SET
    A
    SUNDAY
    TIMES
    BRAIN
    TEASER
    IN
    ???

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

    Which month?

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

    [teaser980]

     
    • Jim Randell's avatar

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

      In the 1994 book the words are presented as:

      SET A SUNDAY TIMES BRAINTEASER IN ???

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


      See: Teaser 2647.

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

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

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

      Run: [ @replit ]

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

      Solution: The month is March.

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

      This can be achieved with the following set of orbits:

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

      One of the letters remains unassigned.

      Like

  • Unknown's avatar

    Jim Randell 9:03 am on 25 February 2024 Permalink | Reply
    Tags: ,   

    Teaser 2572: Abominable 

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

    In the art class Pat used five circles for his new design, Snowman. One circle made the head and another touching circle (with radius three times as big) made the body. Two equal circles (of radius one centimetre less than that of the head) made the arms (one on each side) and they touched both the body and the head. The fifth circle enclosed the other four, touching each of them. The radius of the head’s circle was a whole number of centimetres.

    How many centimetres?

    [teaser2572]

     
    • Jim Randell's avatar

      Jim Randell 9:04 am on 25 February 2024 Permalink | Reply

      See also: Teaser 2926.

      The arrangement looks like this:

      If we consider the head, body, one arm and the enclosing circle we have four mutually tangent circles, so we can use Descartes’ Theorem [ @wikipedia ] to express a relationship between their curvatures.

      If the curvatures are represented by k (where: k = 1 / r, i.e. the curvature is the reciprocal of the corresponding radius), then we have:

      (Σk)² = 2Σ(k²)

      In this instance the circles have radii of r, 3r, r − 1, 4r and so the curvatures are:

      k1 = 1/r
      k2 = 1/3r
      k3 = 1/(r − 1)
      k4 = −1/4r

      The curvature of the fourth circle is negative as it circumscribes the other three circles.

      We can solve the puzzle numerically by simply considering possible r values.

      The following program runs in 65ms. (Internal runtime is 102µs).

      Run: [ @replit ]

      from enigma import (Rational, irange, inf, sq, printf)
      
      Q = Rational()
      
      # consider possible values for r
      for r in irange(2, inf):
      
        # calculate the 4 curvatures
        ks = (Q(1, r), Q(1, 3 * r), Q(1, r - 1), Q(-1, 4 * r))
      
        # check Descartes' Theorem
        if sq(sum(ks)) == 2 * sum(map(sq, ks)):
          printf("r = {r}")
          break
      

      Solution: The head has a radius of 13 cm.

      Or we can solve it by finding the roots of a polynomial.

      This Python program runs in 66ms. (Internal runtime is 3.2ms).

      Run: [ @replit ]

      from enigma import (Polynomial, sq, printf)
      
      # consider the numerators of the curvatures, where the denominator is 12r(r-1)
      ks = [
        12 * Polynomial([-1, 1]),  # k1 = 12(r-1) / 12r(r-1) = 1/r
         4 * Polynomial([-1, 1]),  # k2 = 4(r-1) / 12r(r-1) = 1/3r
        12 * Polynomial([ 0, 1]),  # k3 = 12r / 12r(r-1) = 1/(r-1)
        -3 * Polynomial([-1, 1]),  # k4 = -3(r-1) / 12r(r-1) = -1/4r
      ]
      
      # Descartes' Theorem: sq(sum(ks)) = 2 * sum(map(sq, ks))
      p = sq(sum(ks)) - 2 * sum(map(sq, ks))
      
      # solve the polynomial for positive integer solutions
      for r in p.rational_roots(domain='Z', include='+'):
        printf("r = {r}")
      

      Analytically, we find the polynomial we are solving is:

      r² − 26r + 169 = 0

      (r − 13)² = 0
      r = 13

      Like

  • Unknown's avatar

    Jim Randell 4:46 pm on 23 February 2024 Permalink | Reply
    Tags:   

    Teaser 3205: Colum’s columns of Bob’s bobs 

    From The Sunday Times, 25th February 2024 [link] [link]

    Colum played with Grandpa Bob’s collection of shilling coins. He used them to make minted-year columns for all years from 1900 to 1940 inclusive (in order, in line and touching). Exactly twelve columns had just one coin. This arrangement resembled a bar chart. Pleasingly, it was symmetric about the 1920 column (the tallest of all) and each column’s coin tally was a factor of its year. The total tally was a prime number.

    Later, Bob found eight more shilling coins in an old tin. Colum found that these added one to each of eight columns. Curiously, everything stated above about the arrangement still applied. If I told you the year and final tally for one of those eight columns you could be sure of the total final tally.

    Give this final total.

    [teaser3205]

     
    • Jim Randell's avatar

      Jim Randell 5:23 pm on 23 February 2024 Permalink | Reply

      My original brute force solution was quick to write, but slow to run. It ran in 4.6s. But with a few tweaks we can modify the approach to give a program that runs in a much more acceptable time.

      This Python program runs in 210ms.

      Run: [ @replit ]

      from enigma import (
        defaultdict, irange, divisors, update,  is_prime,
        group, item, singleton, subsets, printf
      )
      
      # fill slots 0-20 with possible values
      vs = [None] * 21
      # slot 20 must be an odd divisor of 1920, and be the largest value
      vs[20] = set(x for x in divisors(1920) if x > 1 and x % 2 == 1)
      m = max(vs[20])
      # remaining slots are divisors of 1900 + i and 1940 - i
      for i in irange(20):
        vs[i] = set(x for x in divisors(1900 + i) if x < m and (1940 - i) % x == 0)
      
      # find possible (left half) sequences
      def solve(k, ss):
        # are we done?
        if k == 21:
          # final value must be the largest
          (final, rest) = (ss[-1], ss[:-1])
          if not (final > max(rest)): return
          # total tally must be prime
          t = 2 * sum(rest) + final
          if not is_prime(t): return
          # there must be 12 1's (in the full sequence)
          if rest.count(1) != 6: return
          # return (<tally>, <sequence>)
          yield (t, ss)
        elif ss[k] is not None:
          # move on to the next index
          yield from solve(k + 1, ss)
        else:
          # choose a value for this index
          for v in vs[k]:
            if v == 1 and ss.count(1) > 5: continue
            yield from solve(k + 1, update(ss, [(k, v)]))
      
      # fill out indices with only one value
      seqs = list(singleton(xs) for xs in vs)
      
      # group complete sequences by tally
      g = group(solve(0, seqs), by=item(0), f=item(1))
      
      # record (<pos>, <value2>) -> <total2>
      ss = defaultdict(set)
      # choose the initial number of coins
      for k1 in sorted(g.keys()):
        k2 = k1 + 8
        if not (k2 in g): continue
      
        # look for first sequence
        for vs1 in g[k1]:
          # there must be (at least) 4 indices with a possible +1
          incs = list(j for (j, x) in enumerate(vs1[:-1]) if x + 1 in vs[j])
          if len(incs) < 4: continue
      
          # look for second sequence
          for js in subsets(incs, size=4):
            # find (<index>, <value2>) values for +1 indices
            rs = list((j, vs1[j] + 1) for j in js)
            # record (<index>, <value2>) -> <total2>
            for x in rs: ss[x].add(k2)
      
            # output scenario
            printf("{k1} -> {vs1}")
            printf("{k2} -> {vs2}", vs2=update(vs1, rs))
            printf("{rs}")
            printf()
      
      # look for keys that give a unique final total
      rs = set()
      for k in sorted(ss.keys()):
        printf("{k} -> {vs}", vs=ss[k])
        t = singleton(ss[k])
        if t is not None:
          rs.add(t)
      printf()
      
      # output solution
      printf("final tally = {rs}", rs=sorted(rs))
      

      Solution: The final total was 139 coins.


      There are 13 possible scenarios that fit the before/after requirements. 11 of these have totals of (131, 139), one has totals of (101, 109), and one has totals of (149, 157).

      And we are told the 1908 (or 1932) column was increased from 2 coins to 3 coins, then that narrows down the possibilities to just 8 of the (131, 139) totals, and so the required answer is 139.

      Here is an example scenario:

      1900, 1940: 4→5
      1901, 1939: 1
      1902, 1938: 2→3
      1903, 1937: 1
      1904, 1936: 8
      1905, 1935: 5
      1906, 1934: 2
      1907, 1933: 1
      1908, 1932: 2→3
      1909, 1931: 1
      1910, 1930: 10
      1911, 1929: 3
      1912, 1928: 2
      1913, 1927: 1
      1914, 1926: 2→3
      1915, 1925: 5
      1916, 1924: 2
      1917, 1923: 3
      1918, 1922: 2
      1919, 1921: 1
      1920: 15
      total: 131→139


      Although my program is a generic solution, there are some handy shortcuts that make a manual solution easier:

      We only need to consider the leftmost 21 values (as the arrangement is symmetric), and there are 6 values that can only have the value 1 (i.e. the two years in question are co-prime), and this gives the 12 columns containing just one coin, and so none of the other columns can have the value 1.

      And this leaves gives a further 5 columns that now only have a single candidate value, and one of them must have the value 5, which means column for 1920 (which must be the largest value) can only be 15.

      Of the remaining 8 columns with multiple candidate values, only 4 of them contain consecutive values, and these give the columns that must differ between the two sequences, so all non-consecutive values can be removed from them.

      Of the four columns with consecutive values, only one of them has a choice of values:

      1908/1932 = 2→3 or 3→4

      So this must be the column we are told that gives us a unique final tally.

      We can consider the two cases:

      [A] = [4→5; 1; 2→3; 1; {2, 4, 8}; {3, 5}; 2; 1; 2→3; 1; {2, 5, 10}; 3; {2, 4, 8}; 1; 2→3; 5; {2, 4}; 3; 2; 1; 15]

      [B] = [4→5; 1; 2→3; 1; {2, 4, 8}; {3, 5}; 2; 1; 3→4; 1; {2, 5, 10}; 3; {2, 4, 8}; 1; 2→3; 5; {2, 4}; 3; 2; 1; 15]

      In the first case the minimum tally for the first sequence is 99 and the maximum is 147.

      So the only prime values in this range where (n + 8) is also prime are: 101 and 131.

      101 is the minimum tally +2, so cannot be made from this case, so it seems likely that being told the 1908/1932 column goes from 2→3 means the final tally goes from 131→139, and this provides the answer.

      We just need to show that there is an initial sequence that gives 131 and that the second case gives rise to more than one tally.

      It is easy enough to find an initial sequence that give 131 (the minimum tally +16), here are all 8:

      [4, 1, 2, 1, 2, 3, 2, 1, 2, 1, 10, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 2, 5, 2, 1, 2, 1, 10, 3, 8, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 4, 3, 2, 1, 2, 1, 10, 3, 8, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 4, 5, 2, 1, 2, 1, 10, 3, 4, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 3, 2, 1, 2, 1, 10, 3, 2, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 3, 2, 1, 2, 1, 10, 3, 4, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 5, 2, 1, 2, 1, 2, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 5, 2, 1, 2, 1, 10, 3, 2, 1, 2, 5, 2, 3, 2, 1, 15] → 131

      In the second case the range is 101 .. 149, which gives primes of: 101, 131, 149.

      So immediately we see a sequence that gives a tally of 101, as this is the minimum possible:

      [4, 1, 2, 1, 2, 3, 2, 1, 3, 1, 2, 3, 2, 1, 2, 5, 2, 3, 2, 1, 15] → 101

      And a tally of 149 is also straightforward, as it is the maximum possible:

      [4, 1, 2, 1, 8, 5, 2, 1, 3, 1, 10, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 149

      And this is enough to solve the puzzle, but for completeness here are the 3 sequences that give 131 (min +15):

      [4, 1, 2, 1, 4, 5, 2, 1, 3, 1, 5, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 3, 2, 1, 3, 1, 5, 3, 8, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 5, 2, 1, 3, 1, 5, 3, 4, 1, 2, 5, 4, 3, 2, 1, 15] → 131

      And together these are the 13 sequences found by my program.

      Like

  • Unknown's avatar

    Jim Randell 5:16 pm on 19 February 2024 Permalink | Reply
    Tags:   

    Teaser 2671: On the face of it 

    From The Sunday Times, 1st December 2013 [link] [link]

    At the beginning of last year my wife and I were each given a clock. When we first looked at them the hands on both clocks showed the correct time but thereafter (although the two clocks coincided at least once more during the year) they generally showed different times.

    The problem was that my clock gained a certain whole number of minutes each day, and the same was true of my wife’s clock, but hers was even faster than mine. Actually my clock showed the correct time at least once in every month last year but that was not the case for my wife’s clock.

    How many minutes did each clock gain in a day?

    [teaser2671]

     
    • Jim Randell's avatar

      Jim Randell 5:17 pm on 19 February 2024 Permalink | Reply

      In this puzzle I am assuming that the clocks are standard 12-hour analogue clocks, that are not adjusted during the year (even for daylight savings time), and the correct time is also not adjusted for DST, and that the puzzle takes place in the year 2012 (it was originally published in 2013).

      Clock 1 showed the correct time at least once each month in 2012, but Clock 2 did not.

      Some time in January both clocks are correct, and some other time during the year (2012 had 366 days), both clocks are showing the same time. And the first time this happens Clock 2 is exactly 12 hours ahead of Clock 1. So during the course of up to 366 days, it has gained 720 minutes over Clock 1, this means the Clock 2 is gaining at least 2 minutes per day more than Clock 1.

      If it gains only 2 minutes more then it will take 360 days to get 720 minutes ahead of Clock 1, and so this will only work if the clocks are first looked at during the first 6 days of January. If it gains 3 or more minutes, any time in January will suffice.

      But if Clock 2 gains 720 minutes (or more) in any 29 day period, then it cannot avoid showing the right time in every month. So Clock 2 must gain less than 25 minutes per day.

      For each possible set of gains per day, this Python program looks for the earliest time in January when the clocks could give a correct time.

      It runs in 56ms. (Internal runtime is 302µs).

      Run: [ @replit ]

      from datetime import (datetime, timedelta)
      from enigma import (irange, irangef, fdiv, repeat, inc, divf, arg, printf)
      
      # year
      Y = arg(2012, 0, int)
      
      # calculate max gain for clock 2 (depends on number of days in Feb)
      M = divf(720, (datetime(Y, 3, 1) - timedelta(days=1)).day)
      
      # return day offsets when a clock gaining <x> minutes per day
      # has gained multiples of 720 minutes
      offsets = lambda x: list(irangef(0, 366, step=fdiv(720, x)))
      
      # look for earliest time when clocks are correct
      # with clock 1 gaining x min/day
      for x in irange(1, M - 2):
        # day offsets for clock 1
        ds1 = offsets(x)
        # clock 1 is correct at least once in each month
        if len(ds1) < 12: continue
      
        # clock 2 gains at least 2 min/day more than clock 1
        for y in irange(x + 2, M):
      
          # choose a time in Jan when the clocks are correct
          for t in repeat(inc(timedelta(minutes=1)), datetime(Y, 1, 1, 0, 0, 0)):
            if t.month > 1: break
      
            # find dates when clock 1 shows the correct time
            ts1 = list(t + timedelta(days=x) for x in ds1)
            # look for these occurring in 12 different months
            if not (len({t.month for t in ts1}) == 12): continue
      
            # find the dates when clock 2 is correct
            ts2 = list(t + timedelta(days=x) for x in offsets(y))
            # look for these occurring in fewer than 12 different months
            if not (len({t.month for t in ts2}) < 12): continue
            # check when the clocks are showing the same time
            s = t + timedelta(days=fdiv(720, y - x))
            if s.year > Y: continue
      
            # earliest solution found
            printf("clock 1 gains {x} mins/day")
            for z in ts1: printf("-> clock 1 correct @ {z}")
            printf("clock 2 gains {y} mins/day")
            for z in ts2: printf("-> clock 2 correct @ {z}")
            printf("clocks read same time @ {s}")
            printf()
            break
      

      Solution: The clocks gain 22 minutes and 24 minutes per day.

      If the clocks both show the correct time at midnight on 1st January 2012 then clock 1 shows the correct time at:

      2012-01-01 @ 00:00
      2012-02-02 @ 17:27
      2012-03-06 @ 10:54
      2012-04-08 @ 04:21
      2012-05-10 @ 21:49
      2012-06-12 @ 15:16
      2012-07-15 @ 08:43
      2012-08-17 @ 02:10
      2012-09-18 @ 19:38
      2012-10-12 @ 13:05
      2012-11-23 @ 06:32
      2012-12-26 @ 00:00

      We see that each of the 12 correct times occurs in a different month.

      And clock 2 shows the correct time at:

      2012-01-01 @ 00:00
      2012-01-31 @ 00:00
      2012-03-01 @ 00:00
      2012-03-31 @ 00:00
      2012-04-30 @ 00:00
      2012-05-30 @ 00:00
      2012-06-29 @ 00:00
      2012-07-29 @ 00:00
      2012-08-28 @ 00:00
      2012-09-27 @ 00:00
      2012-10-27 @ 00:00
      2012-11-26 @ 00:00
      2012-12-26 @ 00:00

      Although this clock shows 13 correct times in the same interval that the other clock shows 12 correct times, it has 2 correct times in January, 2 correct times in March, and no correct times in February.

      After 360 days, on 2012-12-26 @ 00:00, clock 1 has gained 7920 min = 5.5 days, and clock 2 has gained 8640 min = 6 days, so both clocks are showing the same (correct) time of 12:00.


      However, if we were to run the same puzzle in 2013 (not a leap year), we still get a solution with 22 min/day and 24 min/day (although dates after Feb are a day earlier, so clock 2 has 2 correct times in May not March), but we also find there is a second solution, where clock 1 gains 23 min/day and clock 2 gains 25 min/day.

      For example if the clocks both clocks show the correct time at 9:36am on 2nd January 2013, then clock 1 shows the correct time at:

      2013-01-02 @ 09:36
      2013-02-02 @ 16:54
      2013-03-06 @ 00:12
      2013-04-06 @ 07:30
      2013-05-07 @ 14:49
      2013-06-07 @ 22:07
      2013-07-09 @ 05:25
      2013-08-09 @ 12:43
      2013-09-09 @ 20:02
      2013-10-11 @ 03:20
      2013-11-11 @ 10:38
      2013-12-12 @ 17:56

      And clock 2 shows the correct time at:

      2013-01-02 @ 09:36
      2013-01-31 @ 04:48
      2013-03-01 @ 00:00
      2013-03-29 @ 19:12
      2013-04-27 @ 14:24
      2013-05-26 @ 09:36
      2013-06-24 @ 04:48
      2013-07-23 @ 00:00
      2013-08-20 @ 19:12
      2013-09-18 @ 14:24
      2013-10-17 @ 09:36
      2013-11-15 @ 04:48
      2013-12-14 @ 00:00

      After 360 days, on 2012-12-28 @ 09:36, clock 1 has gained 8280 min = 5.75 days, and clock 2 has gained 9000 min = 6.25 days, so both clocks are showing the same (incorrect) time of 3:36.

      And a third solution with the clocks gaining 22 min/day and 25 min/day, where the clocks start on 2013-01-02 @ 09:36 and show the same time after 240 days at 2013-08-30 @ 09:36.

      Like

    • Frits's avatar

      Frits 9:51 am on 23 February 2024 Permalink | Reply

         
      from datetime import date, timedelta
      from math import ceil
      
      DAYS = 366
      
      # dictionary month number per day (day 31 becomes month 2 (2012-02-01 00:00))
      d = {i: (date(2012, 1, 1) + timedelta(days=i)).month 
              for i in range(1, DAYS + 1)}
      
      # "I" need to have a correct time in at least another 11 months 
      mn = ceil(11 * 720 / DAYS)
      # maximum gain for not to have a guaranteed correct time at least once a month
      mx = ceil(720 / 29) - 1
      
      for my in range(mn, mx - 1):
        mths = set(d[ceil((m * 720) / my)]
                   for m in range(1, (my * DAYS) // 720 + 1)) 
        # at least twelve different months?
        if len({1} | mths) == 12:
          mytimes = set(ceil((m * 720) / my) 
                        for m in range(1, (my * DAYS) // 720 + 1))
        
          # my wife's clock is gaining at least 2 minutes per day more than my clock
          for wife in range(my + 2, mx + 1):
            mths = set(d[ceil((m * 720) / wife)]
                       for m in range(1, wife * DAYS // 720 + 1)) 
            if len({1} | mths) < 12:
              wifetimes = {ceil((m * 720) / wife) 
                           for m in range(1, wife * DAYS // 720 + 1)}
              # the two clocks coincided at least once more during the year     
              if mytimes & wifetimes:
                print(f"answer: {my} and {wife}")  
      

      Like

  • Unknown's avatar

    Jim Randell 4:39 pm on 16 February 2024 Permalink | Reply
    Tags: ,   

    Teaser 3204: Pressing problem 

    From The Sunday Times, 18th February 2024 [link] [link]

    The Mint’s machine can press circular coins from square plates of precious metal in straight rows with no gaps between coins. Currently, the coins are pressed with the same number of coins in each column and row, with their centres lying on the same vertical and horizontal straight lines.

    As newly appointed Warden of the Mint, Newton set about reviewing its operational efficiency. He found that more rows can be squeezed into the same plate by shifting some rows so that each coin in it touches two coins in the row above; each of these rows will have one fewer coin in it. With this method, the best that can be done is to produce exactly 8 per cent more coins per plate.

    How many more coins per plate can be produced in this way?

    Note: The intention of this puzzle is that the size of the plate allows the rows and columns of the coins in the original (square-packed) configuration to fit exactly with no extra metal around the edges. Without this condition the puzzle has multiple solutions.

    [teaser3204]

     
    • Jim Randell's avatar

      Jim Randell 4:59 pm on 16 February 2024 Permalink | Reply

      Note: See my comment below for a better program that solves the intended puzzle (and can also be used to find the multiple solutions for the original puzzle where the plate can have non-integer sizes).

      If the coins have diameter of 1, then in hexagonal packing the distance between the centres of alternate rows is (√3)/2.

      This Python program considers increasing sized squares until we achieve an 8% better yield from hexagonal packing vs. square packing.

      It runs in 56ms. (Internal runtime is 342µs).

      Run: [ @replit ]

      from enigma import (irangef, inf, intf, divf, sqrt, printf)
      
      r34 = sqrt(3, 4)
      
      # pack the coins in a square of side <x>
      # return (<number in square packing>, <number in hex packing>)
      def pack(x):
        n = intf(x)  # number of rows in square packing
        ns = n * n
        h = divf(x - 1, r34) + 1  # number of rows in hex packing
        nh = n * h
        if x % 1 < 0.5: nh -= divf(h, 2)
        return (ns, nh)
      
      # consider increasing sheet size
      for x in irangef(1.0, inf, step=0.01):
        if not (x % 1 < 0.5): continue
        (ns, nh) = pack(x)
        # does hex packing give 8% more coins? (nh/ns = 1.08)
        if 100 * nh == 108 * ns:
          printf("{d} additional coins [x={x:.2f}: ns={ns} nh={nh}]", d=nh - ns)
          break
      

      Solution: [see my comment below]

      Like

      • Frits's avatar

        Frits 12:31 pm on 17 February 2024 Permalink | Reply

        @Jim, as must be a divisible by 25 I thought you would use a different step.

        The puzzle doesn’t ask for the first solution (you use “break”).
        I am always in favour of exploring the full solution space. I still have to figure out if we can use a break based on logic

        Like

        • Jim Randell's avatar

          Jim Randell 12:55 pm on 17 February 2024 Permalink | Reply

          @Frits: You can remove the [[ break ]] to look for further solutions. But eventually you reach a point where the hexagonal packing is always better than an 8% improvement so you can stop then (without finding any further layouts – although there is a range of square sizes that give the required solution).

          Like

    • NigelR's avatar

      NigelR 9:35 am on 18 February 2024 Permalink | Reply

      It seems odd – bordering on bizarre – that, to make this teaser work in the way that most seem to be interpreting it, you have to assume that the original sheet is bigger than it needs to be by 0.33 of the coin diameter, or some 6.6%. By extension, and also noting that the puzzle is curiously worded: ‘… by shifting some rows…’ rather than: ‘…by shifting alternate rows..’, there are several solutions possible. For example, strictly consistent with the teaser as worded and using the same assumption, there is a valid solution for a much larger plate but leaving the first n rows unchanged. Only the last few rows are shifted, with a full row being added on the bottom. This yields the same required increase in coin number, but with the original sheet being oversized by a smaller percentage!

      Like

      • Jim Randell's avatar

        Jim Randell 9:53 am on 18 February 2024 Permalink | Reply

        @NigelR: Would that be “best that can be done” with that size of square though?

        I supposed the size of square depends on the pressing machine rather than the coins, as it may be used to make coins of several different sizes. And presumably the unused metal is then recycled into new plates.

        Like

        • NigelR's avatar

          NigelR 11:10 am on 18 February 2024 Permalink | Reply

          @Jim: Point taken – while my scheme would yield the right gain in coin number, it would not be optimal so is not a valid solution. I’m still troubled by the apparently arbitrary size of the original plate….!!

          Like

    • Frits's avatar

      Frits 10:43 am on 18 February 2024 Permalink | Reply

      I followed the same method as Jim and Brian but am not convinced that this is a correct approach as the wording of the puzzle isn’t fully clear to me.

      from fractions import Fraction as RF
      
      # final number of coins = 27/25 * n^2 so n must be a multiple of 5
      n = 5
      while True:
       
        # final situation: m rows with m * n - m / 2 coins and m > n
        # (starting with a row of n coins followed by a row of n - 1 coins, etc)
      
        # we may have a solution if m * n - m / 2 equals 1.08 * n^2
        # or m = (54 * n^2) / (50 * n - 25)
        # or m = 1.08 * n + 27 / (100 * n - 50) + 0.54
      
        # assume diameter coin is 1
        # height of <m> packed rows: 1 + (m - 1) * sqrt(0.75)
        # squeeze as many rows as possible (sheet is less than (n + 1) x (n + 1):
        # n + 1 - sqrt(0.75) <= height of m rows < n + 1
       
        # n + 1 - sqrt(0.75) <= 1 + (m - 1) * sqrt(0.75) < n + 1
       
        # n <= m * sqrt(0.75)
      
        # as 1.08 * sqrt(0.75) < 1 every increment of 1 of n will result in
        # an increment of the height of less than 1
        # thus as soon for a certain <n> the height is less than <n> we can
        # stop processing
      
        # final number of rows
        m = RF(54 * n**2, 50 * n - 25)
        # whole number of rows?
        if m.denominator == 1:
          # if we can't squeeze in one more row
          if n + 1 - 3**.5 / 2 <= (h := 1 + (m - 1) * 3**.5 / 2) < n + 1:
            print(f"answer: {m * n - m // 2 - n**2} coins")
       
        # when can we stop processing?
        if not (2 * n <= m * 3**.5):
          break
      
        n += 5
      

      Like

    • Pete Good's avatar

      Pete Good 5:40 pm on 19 February 2024 Permalink | Reply

      Jim,
      I solved this teaser analytically by deriving two quadratic equations for the number of coins pressed after Newton’s change. The first equation assumes that an even number of rows can be pressed and the second one assumes that an odd number of rows can be pressed. The first quadratic equation has a whole number solution and the second has none. The solution follows directly from this result.

      Like

    • Jim Randell's avatar

      Jim Randell 12:58 pm on 20 February 2024 Permalink | Reply

      It seems the way the setter intended the puzzle to work is that that square plate is an exact number of coin diameters across, and the alternate packing consists of some (but not necessarily every other) shifted, shorter rows.

      I have added a note to the puzzle text.

      Like

      • Jim Randell's avatar

        Jim Randell 1:40 pm on 20 February 2024 Permalink | Reply

        Here is a Python program that solves the intended puzzle:

        Run: [ @replit ]

        from enigma import (Accumulator, irange, irangef, inf, sqrt, intf, divf, printf)
        
        r3 = sqrt(3)
        
        # consider increasing sheet size
        for x in irangef(1.0, inf, step=1.0):
          n = intf(x)
          ns = n * n  # square packing
        
          # find maximal packing for some shifted rows
          acc = Accumulator(fn=max)
          f = x % 1
          # max number of rows in hex packing
          m = divf(x - 1, 0.5 * r3) + 1
          # consider the number of shifted rows
          for s in irange(0, divf(m, 2)):
            # calculate the number of unshifted rows we can fit in
            u = s + intf(x - 1 - s * r3) + 1
            nh = u * n + s * (n - 1 if f < 0.5 else n)
            acc.accumulate_data(nh, (u, s))
          (nh, (u, s)) = (acc.value, acc.data)
        
          # can we fit in 8% more coins?
          if 100 * nh == 108 * ns:
            printf("{d} additional coins [x={x:.2f} u={u} s={s}: ns={ns} nh={nh}]", d=nh - ns)
            break
        

        Solution: 32 additional coins per plate can be produced.

        If the coins have diameter 1 and the plate measures 20 × 20, then the original square packing produces 400 coins per sheet.

        However, by shifting 8 of the rows, we have room for 2 more unshifted rows, for a total of 432 coins, and this is an 8% increase over the original packing.

        And this is the only solution if the size of the plate is constrained to be an exact multiple of the coin diameter.


        However if size of the plate is not so constrained there are 2 further solutions.

        If we change the step at line 6 to allow non-integer square sizes (and remove the break statement), we can find the three sizes of square that give us a possible solution. The smallest is that found by my original program, and the largest is an integer sized square that gives the intended answer.

        Here is a diagram of square packing and hexagonal packing with a 5.4 × 5.4 sheet:

        This is a full hexagonal packing, where every other row is shifted (and this was how I originally interpreted the puzzle, and was the solution found by my original program above).

        We get 25 coins/sheet using square packing and 27 coins/sheet using hexagonal packing, an 8% increase.

        The minimal size square that gives this configuration is: (5√3)/2 + 1 ≈ 5.3301…

        But for squares of size 5.5 or larger the alternate rows in the hexagonal packing do not have to be reduced by one coin (as specified by the puzzle text). So the square size must be in the range 5.33… < x < 5.50.


        And with a 10.47 × 10.47 square, the square packing gives 100 coins, but by shifting 2 rows we have room for an extra row:

        This alternate packing has 108 coins, which is also an 8% increase.

        Like

  • Unknown's avatar

    Jim Randell 4:04 pm on 15 February 2024 Permalink | Reply
    Tags:   

    Teaser 2574: Higher powers 

    From The Sunday Times, 22nd January 2012 [link] [link]

    I have chosen two different two-digit numbers. They use the same two digits as each other, but in different orders. If I start with either number and raise it to a power equal to either of the two numbers, then I find that the answer is a number whose last two digits form the number with which I started.

    What are my two numbers?

    [teaser2574]

     
    • Jim Randell's avatar

      Jim Randell 4:04 pm on 15 February 2024 Permalink | Reply

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

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "pow(AB, AB, 100) = AB"
      "pow(AB, BA, 100) = AB"
      
      "pow(BA, AB, 100) = BA"
      "pow(BA, BA, 100) = BA"
      
      "A < B"  # remove duplicate solution
      

      Solution: The numbers are 16 and 61.

      The powers can be calculated. We have:

      16^16 = 18 446744 073709 551616
      16^61 = 28 269553 036454 149273 332760 011886 696253 239742 350009 903329 945699 220681 916416

      Both of which end in 16, and:

      61^16 = 36751 693856 637464 631913 392961
      61^61 = 8 037480 562545 943774 063961 638435 258139 453693 382991 023311 670379 647429 452389 091570 630196 571368 048020 948560 431661

      both of which end in 61.

      Like

      • Hugo's avatar

        Hugo 4:26 pm on 15 February 2024 Permalink | Reply

        Successive powers of 16 end in 16, 56, 96, 36, 76, a repeating cycle.
        Successive powers of 61 end in 61, 21, 81, 41, 01, a repeating cycle.
        16 mod 5 = 1 = 61 mod 5. So for powers modulo 100 we take the first in each cycle
        (at least, I think that’s a logical conclusion!).

        Like

    • GeoffR's avatar

      GeoffR 7:01 pm on 15 February 2024 Permalink | Reply

       
      for A in range(1, 10):
          for B in range(A+1, 10):
              AB = 10*A + B
              BA = 10*B + A
              #  1st number properties
              if AB ** AB % 100 != AB:
                  continue
              if AB ** BA % 100 != AB:
                  continue
              # 2nd number properties
              if BA ** AB % 100 != BA:
                  continue
              if BA ** BA % 100 == BA:
                  print (f"Two numbers are {AB} and {BA}.")
      
      # Two numbers are 16 and 61.
      

      Like

  • Unknown's avatar

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

    Teaser 2661: Three clocks 

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

    I have three digital clocks, each showing the time as a four-digit display “hhmm” on the 24-hour scale. One keeps perfect time, one runs fast at a constant rate (less than a minute per day) and the third runs slow at exactly the same rate. Every six months I simultaneously reset the faulty clocks to the correct time. Recently I noticed that each clock displayed the same collection of digits but in different orders. In fact, on the fast clock no digit was in the correct place.

    What time did the correct clock show at that moment?

    [teaser2661]

     
    • Jim Randell's avatar

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

      If the clocks are reset every 6 months then that is at most 184 days apart. So the inaccurate clocks are less than 184 minutes adrift from the accurate clock.

      This Python program collects together times that use the same digits but rearranged, and then looks for a collection of at least 3 different times, chooses a correct time, and looks for a fast time that is a derangement of the correct time. From this we calculate the difference window for the inaccurate clocks, and we can look for a corresponding slow time that appears in the same collection.

      It runs in 67ms. (Internal runtime is 9.8ms).

      Run: [ @replit ]

      from enigma import (defaultdict, irange, cproduct, ordered, dot, printf)
      
      # collect possible times by digit content
      d = defaultdict(list)
      for (hh, mm) in cproduct([irange(0, 23), irange(0, 59)]):
        ds = divmod(hh, 10) + divmod(mm, 10)
        d[ordered(*ds)].append(ds)
      
      # digit position values (in minutes, left to right)
      pos = (600, 60, 10, 1)
      
      # look for sets of digits corresponding to at least 3 times
      for (k, vs) in d.items():
        if len(vs) < 3: continue
      
        # choose the correct time
        for corr in vs:
          mc = dot(corr, pos)
          # choose a fast time
          for fast in vs:
            # which must be a derangement of corr
            if fast == corr or any(x == y for (x, y) in zip(fast, corr)): continue
            # calculate the window of difference (in minutes)
            md = (dot(fast, pos) - mc) % 1440
            if md > 183: continue
      
            # look for possible slow times (difference may be +/- 1)
            for w in [-1, 0, +1]:
              ms = (mc - md + w) % 1440
              (hh, mm) = divmod(ms, 60)
              slow = divmod(hh, 10) + divmod(mm, 10)
              if slow not in vs: continue
      
              # output solution
              printf("correct = {corr}; fast = {fast}; slow={slow}")
      

      Solution: The correct clock was showing: 2301.

      And the inaccurate clocks are adrift from the accurate clock by 150 – 151 minutes.

      For example, using fractional minutes we can suppose the accurate clock is showing:

      23:01.75 → 2301

      If the time difference with the inaccurate clocks is 150.5 minutes (= 2h30.5m) then the fast clock is showing:

      01:32.25 → 0132

      and the slow clock is showing:

      20:31.25 → 2031

      Each display uses the digits: 0, 1, 2, 3.

      Like

    • Frits's avatar

      Frits 4:39 am on 23 February 2024 Permalink | Reply

      I have a different version that matches Jim’s speed (but with more code).

          
      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        if d > 1: vs.update('X')
        if d > 2: vs.update('AFSW')
        if d > 5: vs.update('CHU')
      
        d2i[d] = vs 
      
      # return time difference in minutes between h1:m1 and an earlier h2:m2  
      def diff_minutes(h1, m1, h2, m2):
        d = 60 * (h1 - h2) + m1 - m2 
        return d if d > 0 else 1440 + d
      
      # remove elements from list <s2> from list <s1>
      # eg diff_lists([1,3,1,2], [2,0,1]) results in [3,1]
      def diff_lists(s1, s2):
        for x in s2:
          if x in s1:
            s1.remove(x)
        return s1
      
      # correct : AB:CD
      # fast    : FG:HI
      # slow    : ST:UV
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "AB < 24", 
          # ----- logic for fast clock FG:HI ------
          # speeding things up: check for <I>
          "(D + Z) % 10 in {A, B, C, D}",
      
          # add XYZ minutes for fast clock
          "0 < XYZ < 184",
          "(60 * AB + CD + XYZ) % 1440 = OPQR",
          "OPQR // 60 = FG",
          "OPQR % 60 = HI",
      
          # the same collection of digits 
          "sorted([A, B, C, D]) == sorted([F, G, H, I])",
        
          # ----- logic for slow clock ST:UV ------
          # 2 * correct = fast + slow + 1 - W with 0 <= W <= 2
          "(2 * (60 * AB + CD) - (60 * FG + HI) - 1 + W) % 1440 = KLMN",
          "KLMN // 60 = ST",
          "KLMN % 60 = UV",
          
          # the same collection of digits 
          "sorted([A, B, C, D]) == sorted([S, T, U, V])",
        ],
        answer="ABCD",
        d2i=d2i,
        # on the fast clock no digit was in the correct place
        distinct=("AF", "BG", "CH", "DI"),
        env=dict(diff_minutes=diff_minutes, diff_lists=diff_lists),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in sorted(p.answers()):
        print(f"answer: {ans}")
      

      Like

  • Unknown's avatar

    Jim Randell 4:36 pm on 9 February 2024 Permalink | Reply
    Tags:   

    Teaser 3203: Circuit maker 

    From The Sunday Times, 11th February 2024 [link] [link]

    The racing chief tasked his designer for a new circuit. To create a 100:1 scale plan of the circuit the designer cut paper circles of 10cm radius into 12 equal sectors, then removed the top segments to create isosceles triangles with the two equal sides being 10cm. He arranged them on his desk to make a closed loop with no overlaps. Adjacent pieces always share the entire 10cm edge without overlap, but the short edges remain open.

    The chief gave the following requirements. The start-finish line must be located on a straight section of at least 140m in length. Two circular viewing towers of 19m diameter must fit into the internal area, with one at either end of the circuit.

    The designer minimised the internal area. How many triangles did he use?

    [teaser3203]

     
    • Jim Randell's avatar

      Jim Randell 5:06 pm on 9 February 2024 Permalink | Reply

      I am assuming we place triangles together so that the track runs though the long edges of the triangles, and the short edges form the edge of the track (with barriers).

      The smaller sides of the triangles would correspond to ≈5.18m (= 1 unit). So we would need 28 of them to allow a straight 140m long (27 is slightly too short). And a length of 4 of them would allow the viewing towers to fit in a central rectangle.

      So this would require 2 × (28 + 4) = 64 triangles around the inside, and another 2 × (27 + 3) + 4 × 4 = 76 to complete a simple circuit. Making 140 triangles in total, and the enclosed area is 28 × 4 = 112 sq units (≈ 3001 sq m).

      But is this simple design minimal?

      Like

      • Jim Randell's avatar

        Jim Randell 5:18 pm on 9 February 2024 Permalink | Reply

        No, it isn’t.

        I have found another design that allows the viewing towers to fit in a smaller central area (110.42 sq units ≈ 2959 sq m) (and also uses fewer triangles).

        Like

      • Jim Randell's avatar

        Jim Randell 3:47 pm on 10 February 2024 Permalink | Reply

        And now I’ve found another design that I’m fairly convinced must have a minimal enclosed area for the viewing towers.

        The total area enclosed is 623.2 sq m, and 567.1 sq m is taken up by the viewing towers, leaving just over 56 sq m of unused area.

        However this does lead to a family of designs with the same enclosed area, but different numbers of triangles. Maybe we want the smallest number of triangles made from a whole number of circles?

        Like

        • Mark Valentine's avatar

          Mark Valentine 6:25 pm on 10 February 2024 Permalink | Reply

          Interesting. Can’t visualise how there are multiple triangle numbers for the minimal area.

          Like

    • Jim Randell's avatar

      Jim Randell 9:26 am on 12 February 2024 Permalink | Reply

      I thought I would share a couple of the answers I have found (as links for now):

      This is the smallest enclosed area I found (623 sq m), but the area is split into two parts and the track can be extended without altering the area, so I don’t think this is what the setter had in mind. [ link ]

      And here is the smallest area I have found with a single enclosed area (about 820 sq m – which I calculated numerically). [ link ]

      Like

      • Brian Gladman's avatar

        Brian Gladman 2:03 pm on 12 February 2024 Permalink | Reply

        @Jim, Your first answer was my second published answer on the Sunday Times Teaser discussion group and was ruled inadmissible by Mark himself because the base sides of the triangular tiles must remain open.

        Like

        • Jim Randell's avatar

          Jim Randell 3:21 pm on 12 February 2024 Permalink | Reply

          @Brian: I thought the method of constructing the circuit could have been explained better, and I wasn’t really sure what “the short edges remain open” was meant to mean in this context, although after a while I think I worked out how the track was to be constructed.

          But as this first layout leads to multiple possible answers for the puzzle I thought it probably wasn’t what the setter was looking for, so I looked for a solution where the enclosed area was a simple polygon, and came up with the second layout. (Although I followed a systematic approach for this layout, I don’t know for sure that the enclosed area is minimal).

          Like

          • Brian Gladman's avatar

            Brian Gladman 4:37 pm on 12 February 2024 Permalink | Reply

            I got a bit bored with drawing layouts so I thought this morning about the vertical displacement in half wrapping a tower which is 1 + 2s + 2c where s and c are sin(30) and cos(30).

            For the other half of the (partial) tower wrapping we want combinations of s ‘s and c’s that fall just short of cancelling out the vertical displacement on the other side.

            The two smallest results are 4s + 2c == 0 and 2s + 3c = 0.13. The last is your second layout which hence seems the best that can be done if 0 is ruled out.

            Like

    • Jim Randell's avatar

      Jim Randell 8:48 am on 18 February 2024 Permalink | Reply

      Here are more complete notes on the solution(s) I found:

      I found a design that uses 180 triangles, and has hardly any unused enclosed space.

      The enclosed space is 623.21 sq m, but 567.06 sq m of this is taken up by the viewing towers, leaving just 56.15 sq m of unused space.

      The “spine” of the track uses two rows of 28 triangles, which gives a distance of 144.94m between the centre lines of the outside pair, and this is enough to fit the required straight.

      Although if we are allowed to extend the straight slightly into the corners, we could reduce this to two rows of 27 triangles (to give a total of 176 triangles), with a distance of 139.76m between the centre lines of the outside pair. But as we are not trying to minimise the number of triangles, and the length of the straight is an absolute minimum the total of 180 triangles is a better fit to the requirements.

      And if the designer used a whole number of circles to make the triangles, then 15 circles would give him 180 triangles (but we can expand the design by adding 4 triangles on the spine to use any larger multiple of 4 triangles while enclosing the same area).


      A variation on this puzzle is where there is a single connected area in the middle of the track, and the solution above does not provide this. (And this may be what the setter of the puzzle originally had in mind – the requirement that the “short edges remain open” is apparently meant to restrict a triangle from being zero distance away from another triangle along the short edge).

      And we can modify the solution above to give the following layout:

      This uses 168 triangles (which is 14 circles) and gives an enclosed area of 820.13 sq m (unused area = 253.07 sq m). Although I don’t know if this is the smallest possible contiguous internal area, but if we were to extend the long straights the enclosed area would increase.

      Solution: The track was constructed using 168 triangles.


      The program I used to plot the circuits, and to calculate the internal area is here [@replit].

      Like

    • Hugo's avatar

      Hugo 11:18 am on 18 February 2024 Permalink | Reply

      It is not usual for racing cars to go round sharp corners.
      Unrealistic, even in Puzzleland. One of the worst Teasers in recent times.

      Like

      • Tony Smith's avatar

        Tony Smith 8:50 am on 19 February 2024 Permalink | Reply

        Station Hairpin, Monaco Grand Prix.

        Like

  • Unknown's avatar

    Jim Randell 11:30 am on 8 February 2024 Permalink | Reply
    Tags:   

    Teaser 2659: Two by two 

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

    I have given each letter of the alphabet a different value from 0 to 25, so some letters represent a single digit and some represent two digits.

    Therefore, for example, a three-letter word could represent a number of three, four, five or six digits. With my values it turns out that:

    TWO × TWO = FOUR

    Another “obvious” fact that I can tell you is that every digit occurring in TWO is a prime!

    What is the number FOUR?

    Interestingly, one of the earliest published alphametic puzzles was:

    TWO × TWO = THREE

    (using more usual alphametic rules), which appeared almost 100 years ago in the July 1924 issue of Strand Magazine.

    [teaser2659]

     
    • Jim Randell's avatar

      Jim Randell 11:31 am on 8 February 2024 Permalink | Reply

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library (although the method of constructing alphametic words is different from the usual alphametic rules).

      It runs in 194ms. (Internal runtime of the generated program is 78ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --base=26
      
      --code="nconc = lambda *ss: int(concat(ss))"
      --macro="@TWO = nconc(T, W, O)"
      --macro="@FOUR = nconc(F, O, U, R)"
      
      "sq(@TWO) == @FOUR"
      
      --answer="@FOUR"
      
      # T, W, O are chosen from: 2, 3, 5, 7, 22, 23, 25
      --invalid="0|1|4|6|8-21|24,TWO"
      
      # [optional] speeds things up a bit
      # FOUR is a square ...
      "is_square(@FOUR)"
      # ... so R must be the final digits of a square
      --invalid="2-3|7-8|10-15|17-20|22-23,R"
      

      Solution: FOUR = 105625.

      We have:

      TWO = 3:2:5 = 325
      FOUR = 10:5:6:25 = 105625 = 325^2


      And the solution to the TWO × TWO = THREE puzzle is:

      % python3 enigma.py SubstitutedExpression "TWO * TWO = THREE"
      (TWO * TWO = THREE)
      (138 * 138 = 19044) / E=4 H=9 O=8 R=0 T=1 W=3
      [1 solution]
      

      Like

    • GeoffR's avatar

      GeoffR 4:04 pm on 8 February 2024 Permalink | Reply

      from enigma import all_different
      from math import isqrt
      
      def is_sq(x):
         return isqrt(x) ** 2 == x
      
      tup_TWO = (2, 3, 5, 7, 22, 23, 25)
       
      for F in range(1, 26):
        for O in tup_TWO:
          if O == F:continue
          for U in range(26):
            if U in (F, O):continue
            for R in (4, 9, 25): # max value of R = 25
              if R in (U, O, F):continue
              FOUR = int(str(F) + str(O) + str(U) + str(R))
              if not is_sq(FOUR):continue
              for T in tup_TWO:
                for W in tup_TWO:
                  if not all_different(T, W, O, F, U, R):continue
                  TWO = int(str(T) + str(W) + str(O))
                  if TWO * TWO == FOUR:
                    print(f"TWO = {TWO}, FOUR = {FOUR}.")
                    
      # TWO = 325, FOUR = 105625.
      
      

      Like

    • GeoffR's avatar

      GeoffR 8:55 pm on 8 February 2024 Permalink | Reply

      TWO × TWO = THREE is much easier than TWO × TWO = FOUR, as the former has only digits 0..9 to consider.

      from itertools import permutations
      
      for p1 in permutations('1234567890', 3):
         T, W, O = p1
         if T == '0':continue
         TWO = int(T + W + O)
         q1 = set('01234560789').difference([T, W, O])
         for p2 in permutations(q1, 3):
            R, H, E = p2
            THREE = int(T + H + R + E + E)
            if TWO * TWO == THREE:
               print(f"Sum: {TWO} * {TWO} = {THREE}.")
      
      # Sum: 138 * 138 = 19044.
      

      Like

  • Unknown's avatar

    Jim Randell 11:14 pm on 4 February 2024 Permalink | Reply
    Tags:   

    Teaser 2660: Oblonging 

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

    I have made a set of “oblongs”, which is what I call rectangles where the longer sides are one centimetre longer than the shorter sides. I made this set from some A4 sheets of card (without any pasting), cutting out one oblong of each of the sizes 1 cm × 2 cm, 2 cm × 3 cm, 3 cm × 4 cm, and so on up to a particular size. I needed more than one A4 sheet to do this. I have given the set to my family and challenged them to use all the card in jig-saw fashion to make another oblong. After considerable effort they have managed to do this.

    How many oblongs are in the set?

    There are now 1000 Teaser puzzles available on the site.

    [teaser2660]

     
    • Jim Randell's avatar

      Jim Randell 11:15 pm on 4 February 2024 Permalink | Reply

      See also: Enigma 1491.

      An A4 sheet is 21.0 cm × 29.7 cm, so the largest oblong that can be made is 21 × 22.

      And the total area is more than 21 × 29 = 609 sq cm, then we will need multiple A4 sheets to make the oblongs.

      This is sufficient to find a single candidate solution.

      Run: [ @replit ]

      from enigma import (irange, quadratic, printf)
      
      # consider possible oblongs a x (a + 1) and accumulate total area
      t = 0
      for a in irange(1, 21):
        t += a * (a + 1)
        # total area > A4
        if not (t > 609): continue
        # look for an oblong with same area: x(x + 1) = t
        for x in quadratic(1, 1, -t, domain='Z', include='+'):
          printf("1x2 .. {a}x{a1} -> t = {t} -> {x}x{x1}", a1=a + 1, x1=x + 1)
      

      Solution: There are 20 oblongs in the set.

      The oblongs 1×2, 2×3, …, 20×21 have total area of 3080, which is the same as a 55×56 oblong.

      This is the only possible candidate, but it is perhaps more difficult to actually fit the tiles together to make the large oblong.

      However, here is one possible packing:

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 2 February 2024 Permalink | Reply
    Tags:   

    Teaser 3202: Long odds 

    From The Sunday Times, 4th February 2024 [link] [link]

    A group of fewer than twenty of us play a simple gambling game. We have a set of cards labelled 1, 2, 3, … up to a certain number and each player is allocated one of the cards at random. There is a prize for the player with the highest number from those allocated, and a booby prize for the lowest.

    In a recent game I was unfortunately allocated a very middling number (in fact exactly half of the highest possible number). I calculated that my chance of winning the booby prize was 1 in N (where N is a whole number) and that my chance of winning the top prize was even lower at 1 in 2N.

    How many cards are there in the set, and how many players?

    [teaser3202]

     
    • Jim Randell's avatar

      Jim Randell 5:05 pm on 2 February 2024 Permalink | Reply

      This Python program runs in 71ms. (Internal runtime is 3.3ms).

      Run: [ @replit ]

      from enigma import (Rational, irange, multiply, printf)
      
      Q = Rational()
      
      # consider number of players
      for k in irange(2, 19):
      
        # consider number of cards (must be even)
        for n in irange(k + (k % 2), 100, step=2):
      
          # my card is x (= n/2)
          x = n // 2
          if x < k: continue
      
          # chance of winning the booby prize = pB
          # consider each of the (k - 1) other players
          # each of them must have one of the x cards higher than mine
          pB = multiply(Q(x + 1 - i, n - i) for i in irange(1, k - 1))
          if pB.numerator != 1: continue
      
          # chance of winning the top prize = pT
          # consider each of the (k - 1) other players
          # each of them must have one of the (x - 1) cards lower than mine
          pT = multiply(Q(x - i, n - i) for i in irange(1, k - 1))
          if not (2 * pT == pB): continue
      
          # output solution
          printf("{k} players, {n} cards -> x={x} pB={pB} pT={pT}")
      

      Solution: There were 40 cards in the set. And there were 11 players in the game.

      The cards are numbered 1 … 40, so the setter was dealt the 20 card.

      The probability of each of the remaining 10 players receiving a card that is higher than 20 is:

      pB = (20/39) × (19/38) × … × (11/30) = 1/3441

      And the probability of each of the remaining 10 players receiving a card that is lower than 20 is:

      pT = (19/39) × (18/38) × … × (10/30) = 1/6882


      Analytically:

      If my card is x, then there are 2x cards (numbered 1 … 2x).

      And if there are (k + 1) players in total (i.e. k other players), then:

      pB = product[ x/(2x − 1), (x − 1)/(2x − 2), …, (x − k + 1)/(2x − k) ]
      pT = product[ (x − 1)/(2x − 1), (x − 2)/(2x − 2), … (x − k)/(2x − k) ]

      The denominators are the same so:

      numerator(pB) = 2 numerator(pT)

      product[ x, (x − 1), …, (x − k + 1) ] = 2 product[ (x − 1), (x − 2), …, (x − k) ]
      x = 2(x − k)
      x = 2k

      And we can simplify:

      pB = product[ 2k, 2k − 1, …, k + 1 ] / product[ 4k − 1, 4k − 2, …, 3k ]

      so we can consider possible values for k (= 1 .. 18) and look for situations where pB = 1/N.

      The internal runtime of this program is 200µs.

      Run: [ @replit ]

      from enigma import (irange, factorial, fraction, printf)
      
      # consider number of players - 1
      for k in irange(1, 18):
      
        # probability of winning booby prize
        (pBn, pBd) = fraction(factorial(2 * k, k), factorial(4 * k - 1, 3 * k - 1))
        if pBn != 1: continue
      
        # output solution
        (n, k, pTd) = (4 * k, k + 1, 2 * pBd)
        printf("{k} players, {n} cards -> pB=1/{pBd} pT=1/{pTd}")
      

      Like

      • Frits's avatar

        Frits 1:18 pm on 4 February 2024 Permalink | Reply

        @Jim, as x >= k you can also start the “n” loop with 2 * k and remove the continue statement.

        Like

        • Jim Randell's avatar

          Jim Randell 1:50 pm on 4 February 2024 Permalink | Reply

          @Frits: Good point. I’ve got a much shorter program that uses some analysis to do without a loop for n at all. I’ll post it when I put the answer up (or it is on @replit).

          Like

          • Frits's avatar

            Frits 2:36 pm on 4 February 2024 Permalink | Reply

            Yes, I have done the (probably same) analysis to get rid of a loop (the program is on PuzzlingInPython).

            I try to alternate posting between your and Brian’s site.

            Like

  • Unknown's avatar

    Jim Randell 9:16 am on 1 February 2024 Permalink | Reply
    Tags:   

    Teaser 2619: First and last 

    From The Sunday Times, 2nd December 2012 [link] [link]

    A “First and last” number (FLN) is any number that is divisible by its first and last digits. Using each non-zero digit just once, I wrote down a 3-digit FLN followed by three 2-digit FLNs. Putting these in one long line in that order gave me a 9-digit FLN. When the first and last digits were removed from this 9-digit number, I was left with a 7-digit FLN.

    What was the 9-digit number?

    [teaser2619]

     
    • Jim Randell's avatar

      Jim Randell 9:16 am on 1 February 2024 Permalink | Reply

      This run file executes in 83ms. (Internal runtime of the generated program is 202µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --digits="1-9"
      
      # the following are FLNs:
      #
      #   ABC, DE, FG, HI
      #   ABCDEFGHI
      #   BCDEFGH
      
      # 3-digit FLN
      "ABC % A = 0" "ABC % C = 0"
      
      # 3x 2-digit FLNs
      "DE % D = 0" "DE % E = 0"
      "FG % F = 0" "FG % G = 0"
      "HI % H = 0" "HI % I = 0"
      
      # 9-digit FLN
      "ABCDEFGHI % A = 0" "ABCDEFGHI % I = 0"
      
      # 7-digit FLN
      "BCDEFGH % B = 0" "BCDEFGH % H = 0"
      
      --answer="ABCDEFGHI"
      --template="(ABC DE FG HI)"
      --solution=""
      

      Solution: The 9-digit FLN is: 972364815.

      So we have the following FLNs:

      3-digit: 972
      2-digit: 36, 48, 15
      9-digit: 972364815
      7-digit: 7236481

      Like

    • GeoffR's avatar

      GeoffR 9:49 pm on 1 February 2024 Permalink | Reply

      
      from itertools import permutations
      from functools import reduce
      
      nfd = lambda ns: reduce(lambda x, y: 10 * x + y, ns)
      
      # 3-digit FLN number
      for a, b, c  in permutations(range(1, 10), 3):
          abc = nfd([a, b, c])
          if not(abc % c == 0 and abc % a == 0):continue
         
          # find remaining three 2-digit FLN numbers
          q1 = set(range(1, 10)).difference({a,b,c})
          for p2 in permutations(q1):
              d, e, f, g, h, i = p2
              de, fg, hi = 10*d + e, 10*f + g, 10*h + i
              if not (de % d == 0 and de % e == 0):continue
              if not (fg % f == 0 and fg % g == 0):continue
              if not (hi % h == 0 and hi % i == 0):continue
              
              # check 9-digit and 7-digit FLN numbers
              dig9 = nfd([a, b, c, d, e, f, g, h, i])
              if not (dig9 % a == 0 and dig9 % i == 0):continue
              # remove first and last digits from 9-digit number
              dig7 = nfd([b, c, d, e, f, g, h])
              if dig7 % b == 0 and dig7 % h == 0:
                  print(f"Nine digit number = {dig9}.")
      
      # Nine digit number = 972364815.
      

      Like

  • Unknown's avatar

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

    Brain teaser 1004: Ladder limit 

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

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

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

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

    What was that height?

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

    [teaser1004]

     
    • Jim Randell's avatar

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

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

      cos(θ) = 12 / x

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

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

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

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

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

      So we need to find roots of the polynomial:

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

      This can be factorised as:

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

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

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

      h = 12x / (35 − x)

      So:

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

      And we want the maximum value.

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


      And we can do a similar thing programatically:

      Run: [ @replit ]

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

      Like

      • Jim Randell's avatar

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

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

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

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

        Like

    • Hugo's avatar

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

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

      Like

  • Unknown's avatar

    Jim Randell 4:39 pm on 26 January 2024 Permalink | Reply
    Tags:   

    Teaser 3201: Spare routes 

    From The Sunday Times, 27th January 2024 [link] [link]

    Three companies run scenic coach trips in both directions between some pairs of towns in my holiday destination. Each route between two towns is served by just one company, but has one or more alternatives via another town. In particular, every Redstart route could be replaced by a unique combination of two Bluetail routes, every Bluetail by a unique combination of two Greenfinches, and every Greenfinch by a unique combination of a Redstart and a Greenfinch. This wouldn’t be possible with fewer towns or fewer routes.

    I toured one route each day, starting from the town I’d arrived at the previous day but changing the company, never repeating a route in either direction, and involving as many of the routes as I could.

    Which companies did I use, in order (as initials, e.g., R, B, G, B)?

    [teaser3201]

     
    • Jim Randell's avatar

      Jim Randell 8:51 pm on 26 January 2024 Permalink | Reply

      There is probably some (isomorphic) duplication in the minimal graphs found, and this is not an efficient approach for larger graphs (but fortunately the minimal graphs found are quite small).

      Run: [ @replit ]

      from enigma import (Accumulator, irange, inf, subsets, ordered, singleton, join, map2str, printf)
      
      # find routes between x and y via z
      def route(m, x, y, zs, ss):
        for z in zs:
          if z == x or z == y: continue
          if ordered(m.get(ordered(x, z), 'X'), m.get(ordered(y, z), 'X')) == ss:
            yield z
      
      # requirements: colour -> replacements
      reqs = {
        'R': ('B', 'B'),  # R can be replaced by B+B
        'B': ('G', 'G'),  # B can be replaced by G+G
        'G': ('G', 'R'),  # G can be replaced by G+R
      }
      
      # check the requirements on map <m> with nodes <ns>
      def check(m, ns):
        vs = set()
        for ((x, y), v) in m.items():
          if v != 'X': vs.update((x, y))
          ss = reqs.get(v)
          # look for unique replacement
          if ss and singleton(route(m, x, y, ns, ss)) is None: return False
        # check all nodes are non-trivial
        return vs.issuperset(ns)
      
      # find paths on map <m> without repeated edges, and different colours on adjacent edges
      # vs = visited vertices
      # ks = visited edges
      def paths(m, vs=list(), ks=list()):
        if ks and ks[0] < ks[-1]: yield (vs, ks)
        # can we extend the path?
        for (k, v) in m.items():
          if v == 'X': continue
          if ks and (k in ks or v == m[ks[-1]]): continue
          if k[0] == vs[-1]:
            yield from paths(m, vs + [k[1]], ks + [k])
          if k[1] == vs[-1]:
            yield from paths(m, vs + [k[0]], ks + [k])
      
      # find minimal graphs (by (number of nodes, number of edges))
      graphs = Accumulator(fn=min, collect=1)
      
      # consider increasing numbers of towns (at least 4)
      for n in irange(4, inf):
        # vertices (= towns)
        ns = list(irange(n))
        # edges (= routes)
        rs = list(subsets(ns, size=2))
      
        # assign colours to routes ('X' = no route)
        for ss in subsets("RBGX", size=len(rs) - 1, select='M', fn=list):
          # check minimum number of routes
          if ss.count('B') < 2: continue
          if ss.count('G') < 3: continue
          ss.insert(0, 'R')
      
          # make the map
          m = dict(zip(rs, ss))
          # check requirements
          if check(m, ns):
            # record (number of nodes, number of edges)
            graphs.accumulate_data((n, len(ss) - ss.count('X')), m)
      
        if graphs.data: break  # we only need the minimal number of vertices
      
      # collect answers
      ans = set()
      
      # process minimal graphs
      (n, e) = graphs.value
      ns = list(irange(n))
      printf("minimal graph has {n} vertices, {e} edges")
      
      for m in graphs.data:
        printf("-> {m}", m=map2str(m))
      
        # collect maximal paths on this map
        r = Accumulator(fn=max, collect=1)
        for v in ns:
          for (vs, ks) in paths(m, [v]):
            r.accumulate_data(len(ks), (vs, ks))
      
        printf("max path len = {r.value}")
        # output paths
        for (vs, ks) in r.data:
          ss = join(m[k] for k in ks)
          printf("-> {vs} {ss}")
          ans.add(ss)
        printf()
      
      # output solution
      printf("tour = {t}", t=join(ans, sep=", "))
      

      Solution: The companies used are: G, B, R, B, R, B, G.

      A minimal graph has 5 vertices and 9 edges:

      The program finds 12 graphs in total, but they are all isomorphic to the one above.

      We see that node 4 only has green edges (and all green edges lead to 4), so that can only act as the start/end of the path, and we can use a maximum of 7 of the 9 edges. Each path can be traversed in both directions, so there are essentially 3 different maximal paths that can be taken. And the fact that puzzle should have a unique solutions implies the answer should be palindromic (otherwise the reverse would also be a valid answer), even though the example given in the puzzle text is not.

      Liked by 1 person

      • Jim Randell's avatar

        Jim Randell 10:15 am on 27 January 2024 Permalink | Reply

        I augmented my code to detect isomorphic graphs [@replit], and there is essentially only one viable minimal layout.

        from enigma import (Accumulator, irange, inf, subsets, ordered, singleton, join, map2str, printf)
        
        # find routes between x and y via z
        def route(m, x, y, zs, ss):
          for z in zs:
            if z == x or z == y: continue
            if ordered(m.get(ordered(x, z), 'X'), m.get(ordered(y, z), 'X')) == ss:
              yield z
        
        # requirements: colour -> replacements
        reqs = {
          'R': ('B', 'B'),  # R can be replaced by B+B
          'B': ('G', 'G'),  # B can be replaced by G+G
          'G': ('G', 'R'),  # G can be replaced by G+R
        }
        
        # check the requirements on map <m> with nodes <ns>
        def check(m, ns):
          vs = set()
          for ((x, y), v) in m.items():
            if v != 'X': vs.update((x, y))
            ss = reqs.get(v)
            # look for unique replacement
            if ss and singleton(route(m, x, y, ns, ss)) is None: return False
          # check all nodes are non-trivial
          return vs.issuperset(ns)
        
        # find paths on map <m> without repeated edges, and different colours on adjacent edges
        # vs = visited vertices
        # ks = visited edges
        def paths(m, vs=list(), ks=list()):
          if ks and ks[0] < ks[-1]: yield (vs, ks)
          # can we extend the path?
          for (k, v) in m.items():
            if v == 'X': continue
            if ks and (k in ks or v == m[ks[-1]]): continue
            if k[0] == vs[-1]:
              yield from paths(m, vs + [k[1]], ks + [k])
            if k[1] == vs[-1]:
              yield from paths(m, vs + [k[0]], ks + [k])
        
        # check 2 graphs for isomorphism
        def is_isomorphic(vs, m1, m2):
          # choose a mapping of vs
          for ss in subsets(vs, size=len, select='P'):
            # remap m1
            m3 = dict((ordered(ss[x], ss[y]), v) for ((x, y), v) in m1.items())
            if m3 == m2: return True
          return False
        
        # find minimal graphs
        graphs = Accumulator(fn=min, collect=1)
        
        # consider increasing numbers of towns (at least 4)
        for n in irange(4, inf):
          # vertices (= towns)
          ns = list(irange(n))
          # edges (= routes)
          rs = list(subsets(ns, size=2))
        
          # assign colours to routes ('X' = no route)
          for ss in subsets("RBGX", size=len(rs) - 1, select='M', fn=list):
            # check minimum number of routes
            (nR, nB, nG) = (ss.count('R') + 1, ss.count('B'), ss.count('G'))
            if nB < 2 or nG < 3 or nB < nR or nG < nB: continue
            ss.insert(0, 'R')
        
            # make the map
            m = dict(zip(rs, ss))
            # check requirements
            if check(m, ns):
              # record (number of nodes, number of edges)
              graphs.accumulate_data((n, len(ss) - ss.count('X')), m)
        
          if graphs.data: break  # we only need the minimal number of vertices
        
        # collect answers
        ans = set()
        morphs = list()
        
        # process minimal graphs
        (n, e) = graphs.value
        ns = list(irange(n))
        printf("minimal graph has {n} vertices, {e} edges")
        
        for m in graphs.data:
          printf("-> {m}", m=map2str(m))
        
          # check morphs
          if any(is_isomorphic(ns, m, m1) for (j, m1) in enumerate(morphs)): continue
          printf("= isomorph {n}", n=len(morphs))
          morphs.append(m)
        
          # collect maximal paths on this map
          r = Accumulator(fn=max, collect=1)
          for v in ns:
            for (vs, ks) in paths(m, [v]):
              r.accumulate_data(len(ks), (vs, ks))
        
          printf("max path len = {r.value}")
          # output paths
          for (vs, ks) in r.data:
            ss = join(m[k] for k in ks)
            printf("-> {vs} {ss}")
            ans.add(ss)
          printf()
        
        # output solution
        printf("tour = {t}", t=join(ans, sep=", "))
        

        Like

    • Frits's avatar

      Frits 7:42 am on 27 January 2024 Permalink | Reply

      Similar, the tour also finishes where it started.
      The generated routes and/or tours may contain duplicates.

        
      from itertools import product
      
      # is route <k> doable by a unique combination via two routes of types <tps> 
      def check_alt(m, n, k, tps):
        rem_towns = [i for i in range(n) if i not in k]
        alt = [a for a in rem_towns 
               if {m[tuple(sorted([a, x]))] for x in k} == tps]
        return len(alt) == 1
      
      def check(m, n):
        d = dict(zip(["R", "B", "G"], [{"B"}, {"G"}, {"R", "G"}]))
       
        # check for unique alternative combinations
        for k, v in m.items():
          if v in "RBG":
            if not check_alt(m, n, k, d[v]): return False
        return True    
              
      # generate routes that meet the requirements
      def gen_routes(n):
        # routes
        rs = [(i, j) for i in range(n - 1) for j in range(i + 1, n)]
      
        # assign company to a route (or not), (blank = no route)
        for cs in product("RBG ", repeat = n * (n - 1) // 2 - 1):
          cs = ("R", ) + cs
          # R < B < G
          if (cntR := cs.count('R')) >= (cntB := cs.count('B')): continue
          if cntB >= (cntR := cs.count('G')): continue
          
          # check requirements
          if check(d := dict(zip(rs, cs)), n):
            yield d
      
      # add route to tour
      def add_route(m, ss=[]):
        if not ss: # start
          for k, v in m.items():
            yield from add_route(m, ss + [(k, v)])
        else:
          yield ss
          # find a new unused route for a different company from the last
          rt, tp = ss[-1]
          for k, v in m.items():
            if v == tp: continue 
            # already added?
            if (k, v) in ss or (k[::-1], v) in ss: continue
            if k[0] == rt[1]:
              yield from add_route(m, ss + [(k, v)]) 
            if k[1] == rt[1]:
              yield from add_route(m, ss + [(k[::-1], v)])    
      
      n = 4 # we need at least 6 routes (1 R, 2 B, 3 G)
      # increase number of towns until all alternative routes can be made
      while True:
        sols = set()
        mx = 0
        
        # generate valid routes
        for r in gen_routes(n):
          # find longest tour
          mx = 0
          for tour in add_route({k: v for k, v in r.items() if v in "RGB"}):
            lt = len(tour)
            if lt >= mx:
              if lt > mx:
                sols = set() 
                mx = lt
              # finish where we have started (is this a feature of a tour?)    
              if tour[0][0][0] != tour[-1][0][1]: continue    
              sols.add(''.join(t for _, t in tour))
            
        if mx:
          print(f"answer: {' or '.join(sols)}") 
          break 
        else:
          n += 1
      

      Like

      • Jim Randell's avatar

        Jim Randell 11:35 am on 31 January 2024 Permalink | Reply

        @Frits: I don’t think it is required for the tour to start and finish at the same town. (Certainly this is not explicitly stated).

        Also, how does your program ensure that the condition “this wouldn’t be possible with … fewer routes” is met?

        Like

    • Frits's avatar

      Frits 3:55 am on 1 February 2024 Permalink | Reply

      Without demanding for cyclic tours and with minimal routes.
      Program is also running a little bit faster (due to the extra “cntR” condition).

       
      from itertools import product
      
      # is route <k> feasible by a unique combination via two routes of types <tps> 
      def check_alt(m, n, k, tps):
        alt = [a for a in [i for i in range(n) if i not in k]
               if {m[(a, x) if a < x else (x, a)] for x in k} == tps]
        return len(alt) == 1
      
      # perform checks for all routes
      def check(m, n):
        d = dict(zip(["R", "B", "G"], [{"B", "B"}, {"G", "G"}, {"R", "G"}]))
       
        # check for unique alternative combinations
        for r, c in m.items():
          if c in "RBG":
            if not check_alt(m, n, r, d[c]): return False
        return True    
              
      # generate routes that meet the requirements
      def gen_routes(n):
        # routes
        rs = [(i, j) for i in range(n - 1) for j in range(i + 1, n)]
      
        # assign company to a route (or not), (blank = no route)
        for cs in product("RBG ", repeat = n * (n - 1) // 2 - 1):
          cs = ("R", ) + cs
          # nR < nB < nG
          # 3 * r + 3 <= n * (n - 1) // 2
          if not ((cntR := cs.count('R')) <= (n * (n - 1) - 6) // 6): continue
          
          if not (cntR < cs.count('B') < cs.count('G')): continue
          # check requirements
          if check(d := dict(zip(rs, cs)), n):
            yield d
      
      # generate all valid tours (credit: John Z)
      def tours(m, prevt, prevc="x", rts = []):
        
        # consider next town to visit
        for nt in set(range(n)) - {prevt}:
          
          route = (prevt, nt) if prevt < nt else (nt, prevt) 
          if route not in rts and m[route] not in {" ", prevc}:
            yield from tours(m, nt, m[route], rts + [route])
      
        # any route is linked to 2 other routes
        if len(rts) > 2:
         yield ''.join(m[x] for x in rts)
      
      n = 4 # we need at least 6 routes (1 R, 2 B, 3 G)
      # increase number of towns until all alternative routes can be made
      while True:
        mrts = dict()  
        # generate valid routes and calculate number of blank routes
        for r in gen_routes(n):
          mrts[brts] = mrts.get(brts := list(r.values()).count(" "), []) + [r]
          
        if mrts:
          trs = set()
          # get minimimal routes by maximising the number of blank routes
          for r in mrts[max(mrts.keys())]:
            # find all valid tours
            trs |= set(tr for twn in range(n) for tr in tours(r, twn))
      
          print("answer:", ' or '.join([tr for tr in trs 
                                        if len(tr) == len(max(trs, key=len))]))
          break 
        else:
          n += 1   
      

      Like

  • Unknown's avatar

    Jim Randell 11:11 am on 25 January 2024 Permalink | Reply
    Tags:   

    Teaser 2620: Stating the obvious 

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

    I have replaced the letters of the alphabet by the numbers 0 to 25 in some order, using different numbers for different letters. So, for example, a three-letter word could represent a number of three, four, five or six digits. With my particular values:

    ONE + ONE = TWO, and
    ONE is a perfect square.

    What number does TWO represent?

    [teaser2620]

     
    • Jim Randell's avatar

      Jim Randell 11:13 am on 25 January 2024 Permalink | Reply

      The value of a word is formed by concatenating the values (in base 10) of the digits, and then reading the result as a number.

      So, for example, if O = 10, N = 6, E = 25, then the value of ONE is 10625 (= 10:6:25).

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

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --base=26
      --code="word = lambda *vs: int(concat(vs))"
      --macro="@ONE = word(O, N, E)"
      --macro="@TWO = word(T, W, O)"
      --invalid="0,OT"
      
      # ONE is a perfect square
      "is_square(@ONE)"
      
      # ONE + ONE = TWO
      "2 * @ONE == @TWO"
      
      --answer="@TWO"
      

      Solution: TWO = 4232.

      There are two assignments that achieve this:

      E=6 N=11 O=2 T=4 W=23 → ONE=2116 TWO=4232
      E=16 N=1 O=2 T=4 W=23 → ONE=2116 TWO=4232

      And if we allow leading zeros (i.e. O or T is 0), we can also have:

      E=25 N=2 O=0 T=4 W=5 → ONE=225 TWO=450
      E=25 N=6 O=0 T=12 W=5 → ONE=625 TWO=1250
      E=25 N=12 O=0 T=24 W=5 → ONE=1225 TWO=2450

      So perhaps the puzzle would have been better if the values used were 1 to 26.

      Like

    • GeoffR's avatar

      GeoffR 3:00 pm on 25 January 2024 Permalink | Reply

      from math import isqrt
      def is_sq(x):
         return isqrt(x) ** 2 == x
      
      for O in range(1, 27):
          for N in range(26):
              if N == O:
                  continue
              for E in range(26):
                  if E in (O, N):
                      continue
                  #1. ONE is a perfect square.
                  ONE = int(str(O) + str(N) + str(E))
                  if is_sq(ONE):
                      for T in range(1, 27):
                          if T in (O, N, E):
                              continue
                          for W in range(26):
                              if W in (T, O, N, E):
                                  continue      
                              #2. ONE + ONE = TWO
                              TWO = int(str(T) + str(W) + str(O))
                              if ONE + ONE == TWO:
                                  print(f"ONE = {ONE} and TWO = {TWO}")
                                  print(f"O={O}, N={N}, E={E}, T={T}, W={W}")
                                  print()
                                                    
      # ONE = 2116 and TWO = 4232
      # O=2, N=1, E=16, T=4, W=23
      
      # ONE = 2116 and TWO = 4232
      # O=2, N=11, E=6, T=4, W=23
      
      

      Like

    • Frits's avatar

      Frits 4:50 am on 26 January 2024 Permalink | Reply

          
      from itertools import permutations
      from enigma import is_square
      
      sols = set()
      # O has to be even as 2 * ONE = TWO  
      for O in range(2, 26, 2):
        lnO = 1 + (O > 9)
        for N, E in permutations([n for n in range(26) if n != O], 2):
          if not is_square(ONE := int(str(O) + str(N) + str(E))): continue
          
          # parse TWO into TW and O_
          (sTW, sO_) = ((sTWO := str(ONE + ONE))[:-lnO], sTWO[-lnO:])
          if sO_ != str(O): continue
          
          # parse TW into T and W
          for t in range(1 + (len(sTW) == 4), 2 + (len(sTW) > 2)):
            (T, W) = (int(sTW[:t]), int(sTW[t:]))
            # different numbers
            if T == W or any(n in {O, N, E} or n > 25 for n in [T, W]): continue
            sols.add(sTWO)
      
      print(f"answer: {' or '.join(sols)}")      
      

      Like

  • Unknown's avatar

    Jim Randell 1:33 pm on 23 January 2024 Permalink | Reply
    Tags:   

    Teaser 2669: Extending the garden 

    From The Sunday Times, 17th November 2013 [link] [link]

    George and Martha used to have a square garden with sides of length 10 metres, but they have extended it by adding an acute-angled triangle on each side of the square. The result is that the new garden has eight straight sides, each of which is fenced. The fences are different whole number of metres long, with the shortest being one metre and the longest 13 metres. George remarked that the average length of the fences was also a whole number of metres.

    In increasing order, what were the lengths of the eight fences?

    [teaser2669]

     
    • Jim Randell's avatar

      Jim Randell 1:34 pm on 23 January 2024 Permalink | Reply

      If we consider the side of the 10 metre square to be the base of a triangle, then the vertex of the triangle formed by the new fences must lie above the interior of the base, and outside a semicircle whose diameter is the base.

      This Python program finds possible side configurations (for a shorter and longer site, chosen from 1..13), and then combines 4 of these configurations to find viable sets of triangles.

      It runs in 59ms. (Internal runtime is 3.5ms).

      Run: [ @replit ]

      from enigma import (irange, fdiv, sq, disjoint_union, printf)
      
      # generate possible (a, b) extensions
      def sides():
        # consider possible sides (from 1 - 13) to make an acute angled triangle
        # the shorter side
        for a in irange(1, 12):
          # the longer side
          for b in irange(max(a + 1, 11 - a), 13):
            # determine vertex points
            x = fdiv(sq(b) - sq(a), 20)
            # vertex must be above base
            if not (x < 5): continue
            x2 = sq(x)
            y2 = sq(b) - sq(x + 5)
            # vertex must lie outside a radius 5 semicircle
            if not (x2 + y2 > 25): continue
            # this is a viable configuration
            yield (a, b)
      
      # generate solutions
      def solve(sides, k, ts=list(), ss=set()):
        # are we done?
        if k == 0:
          # mean is an integer
          if sum(ss) % len(ss) != 0: return
          # smallest side is 1, largest is 13
          ss = sorted(ss)
          if not (ss[0] == 1 and ss[-1] == 13): return
          # viable solution
          yield (ts, ss)
        else:
          # add in another triangle
          for (i, t) in enumerate(sides):
            # all sides are different
            ss_ = disjoint_union([ss, t])
            if ss_:
              yield from solve(sides[i:], k - 1, ts + [t], ss_)
      
      # find solutions
      rs = set()
      for (ts, ss) in solve(list(sides()), 4):
        printf("{ts} -> {ss}")
        rs.add(tuple(ss))
      
      # output solutions
      for ss in rs:
        printf("sides = {ss}")
      

      Solution: The lengths of the fencing are (in metres): 1, 5, 7, 8, 9, 10, 11, 13.

      There are 2 configurations that use this set of fences:

      (1, 10) (5, 9) (7, 8) (11, 13)
      (1, 10) (5, 11) (7, 8) (9, 13)

      Here are examples of these configurations:

      I did consider adding code to ensure the fences could be arranged such that the two fences meeting at the corner were not co-linear, but from the example diagrams it is clear that this can be done in both cases.

      Like

  • Unknown's avatar

    Jim Randell 10:25 am on 21 January 2024 Permalink | Reply
    Tags: by: Chris Maslanka   

    Brainteaser 1389: A redistribution of wealth 

    From The Sunday Times, 23rd April 1989 [link]

    Etherdred the Every-ready had been unwise enough to promise to distribute among his four unworthy sons the contents of an old oak coffer which had subsequently turned out to contain the over-magnanimous sum of 405,405,135,015 talents and nothing else. As his own abacus had only eight significant but venerable beads, he summoned his astrologer, Easirede the Farsighted, for advice.

    “I feel that once my fee is determined, the rest of the problem will fall into place. Let us suppose His Majesty’s generosity extends to granting me X talents, where X is a whole number. I see that His Majesty has already divided all of the money into four unequal heaps, one for each son. Now, adding undoes subtraction. So let us take X talents from Aardman’s heap and put X on to Beergutt’s heap. Also, multiplying undoes division, so let us divide Ceedigrin’s heap by X and mutliply Dampfish’s by X. Each son will then have the same amount. After X talents has been paid to me from the remainder His Majesty can keep the rest”.

    The redistribution was made as the astrologer suggested and he left the castle only to be robbed of his X talents by a disgruntled Aardman a mile down the road.

    How many talents were received by each son under the new distribution?

    [teaser1389]

     
    • Jim Randell's avatar

      Jim Randell 10:26 am on 21 January 2024 Permalink | Reply

      If we start with T talents, and in the end distribute N to each of the 4 sons, and X to the astrologer, then:

      4N + X ≤ T

      And the initial piles were:

      A = N + X
      B = N − X
      C = NX
      D = N/X

      And these are all different positive integers, which sum to T.

      (N + X) + (N − X) + NX + N/X = T

      N = TX / (X + 1)^2

      Hence:

      D = N/X = T / (X + 1)^2

      So we can determine values for X by looking at squares that are divisors of T.

      Run: [ @replit ]

      from enigma import (divisors, div, printf)
      
      T = 405405135015
      
      # consider divisors of T
      for X in divisors(T):
        if X < 2: continue
      
        # is the square of X also a divisor?
        X2 = X * X
        if X2 > T: break
        d = div(T, X2)
        if d is None: continue
        X -= 1
      
        # calculate N
        N = X * d
      
        # and the initial piles
        ps = (N + X, N - X, N * X, div(N, X))
        if any(p is None or p < 1 for p in ps) or len(set(ps)) != 4: continue
        assert sum(ps) == T
      
        # output solution
        printf("X={X} -> N={N} ps={ps}")
      

      Solution: Each son received 135045000 talents.

      So each son receives 0.033% of the total.

      And the astrologer receives a modest 3000 talents, which is 0.00000074% of the total.

      And so Etherdred keeps 99.87% of the total.

      Although A mugged the astrologer, C has lost much more by his scheme.


      If we factorise T we get:

      T = 3 × 5 × 3001^3

      So we see that X = 3000 is the only viable candidate.

      Like

  • Unknown's avatar

    Jim Randell 4:31 pm on 19 January 2024 Permalink | Reply
    Tags:   

    Teaser 3200: Puzzling sum 

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

    Liam was given a set of addition sums as part of his homework. The teacher had cleverly chosen sums where the answer, together with the two numbers to be added, used all the digits from 1 to 9 just once (and there were no zeros). For instance, one of the sums was 193 + 275 = 468. I noticed that one of them was particularly interesting in that the two numbers to be added were perfect powers (squares, cubes etc).

    In the interesting sum, what are the two numbers that were to be added?

    [teaser3200]

     
    • Jim Randell's avatar

      Jim Randell 4:48 pm on 19 January 2024 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to consider all possible sums that the teacher could have set, and then looks for those with summands that are perfect powers.

      It runs in 87ms.

      Run: [ @replit ]

      from enigma import (
        SubstitutedExpression, decompose, irange, prime_factor,
        mgcd, unpack, sprintf as f
      )
      
      # symbols for the 9 different digits
      syms = "ABCDEFGHI"
      
      # check a number is a perfect power (> 1)
      def is_ipower(n, gcd=unpack(mgcd)):
        if n == 1: return True
        return gcd(e for (p, e) in prime_factor(n)) > 1
      
      # split the symbols into 2 addends and a result (x + y = z)
      for (a, b, c) in decompose(len(syms), 3, increasing=1, sep=0, min_v=1, max_v=4):
        # create the sum
        (x, y, z) = (syms[:a], syms[a:a + b], syms[a + b:])
        # create an alphametic puzzle
        exprs = [ f("{x} + {y} = {z}") ]
        if len(x) == len(y): exprs.append(f("{x0} < {y0}", x0=x[0], y0=y[0]))
        p = SubstitutedExpression(
          exprs,
          digits=irange(1, 9),
          answer=f("({x}, {y}, {z})"),
          verbose='',
        )
        # and solve it
        for (x, y, z) in p.answers():
          if is_ipower(x) and is_ipower(y):
            # output solution
            print(f("{x} + {y} = {z}"))
      

      Solution: The interesting sum is: 243 + 576 = 819.

      And:

      243 = 3^5
      576 = 24^2

      There are 168 possible sums using the digits 1-9, but only this one has summands that are perfect powers. And all of them are of the form: ABC + DEF = GHI.

      Like

      • Jim Randell's avatar

        Jim Randell 8:10 pm on 19 January 2024 Permalink | Reply

        An alternative approach is just to look for two powers that give a sum with the required properties. (The code to generate powers is borrowed from Teaser 3119).

        This Python program has an internal runtime of 1.2ms.

        Run: [ @replit ]

        from enigma import (Primes, nsplit, printf)
        
        # generate powers (x^y) in numerical order, x >= 0, y >= 2,
        def ipowers():
          # powers less than 2^2
          yield 0
          yield 1
          # powers from 2^2 upwards
          base = { 2: 2 }
          power = { 2: 4 }
          maxp = 2
          primes = Primes(35, expandable=1).generate(maxp + 1)
          while True:
            # find the next power
            n = min(power.values())
            yield n
            # what powers are involved
            ps = list(p for (p, v) in power.items() if v == n)
            # increase the powers
            for p in ps:
              base[p] += 1
              power[p] = pow(base[p], p)
            # do we need to add in a new power?
            if maxp in ps:
              maxp = next(primes)
              base[maxp] = 2
              power[maxp] = pow(2, maxp)
        
        # check for valid digits, must be all different and not include 0
        valid = lambda ds: 0 not in ds and len(set(ds)) == len(ds)
        
        # collect powers
        ps = list()
        for p in ipowers():
          ds = nsplit(p)
          if len(ds) > 4: break
          if not valid(ds): continue
        
          # look for a smaller power to pair with this one
          for (p1, ds1) in ps:
            t = p1 + p
            dst = nsplit(t)
            # check all 9 digits used
            ds9 = ds + ds1 + dst
            n = len(ds9)
            if n < 9: continue
            if n > 9: break
            if not valid(ds9): continue
            # output solution
            printf("{p1} + {p} = {t}")
        
          # record this power
          ps.append((p, ds))
        

        I will add the [[ ipowers() ]] function to the enigma.py library (from version 2024-01-19).

        Like

        • Frits's avatar

          Frits 1:53 am on 20 January 2024 Permalink | Reply

          @Jim, did you forget to check dst for zeroes?

          Like

        • Brian Gladman's avatar

          Brian Gladman 6:53 pm on 20 January 2024 Permalink | Reply

          @Jim. That is a very neat subroutine for generating integer powers in order.

          Like

        • Brian Gladman's avatar

          Brian Gladman 9:21 pm on 21 January 2024 Permalink | Reply

          @Jim, I looked at alternatives to your ipowers() routine and found this quite a bit faster:

          from heapq import heappush, heappop
          
          def ipowers(maths=True):
            if maths:
              yield 0
              yield 1
            heap, pv, m = [], None, 2
            heappush(heap, (2 ** 2, 2, 2))
            while True:
              m += 1
              v = 2 ** m
              while heap[0][0] <= v:
                (v1, n1, m1) = heappop(heap)
                if v1 != v and v1 != pv:
                  yield v1
                  pv = v1
                heappush(heap, ((n1 + 1) ** m1, n1 + 1, m1))
              heappush(heap, (2 ** m, 2, m))
          

          Like

          • Jim Randell's avatar

            Jim Randell 10:37 am on 22 January 2024 Permalink | Reply

            @Brian: Yes, using a heap is faster (especially in CPython which I think has heaps implemented via a C module).

            So, this is even faster:

            from heapq import (heapify, heappush, heappop)
            
            def ipowers():
              # powers less than 4
              yield 0
              yield 1
            
              # powers from 4 upwards
              hi = 1
              maxe = 2
              pows = [(4, 2, 2)]
              heapify(pows)
              exps = Primes(35, expandable=1).generate(3)
              while True:
                # find the next power
                (p, b, e) = heappop(pows)
                if p > hi:
                  yield p
                  hi = p
                heappush(pows, (pow(b + 1, e), b + 1, e))
                # do we need to add in a new exponent?
                if b == 2:
                  maxe = next(exps)
                  heappush(pows, (pow(2, maxe), 2, maxe))
            

            Liked by 1 person

            • Brian Gladman's avatar

              Brian Gladman 9:02 am on 23 January 2024 Permalink | Reply

              It is faster if you don’t count the cost of the extra import of Primes() which dwarfs the running cost for low power limits. Including import cost for powers below 1000 I measured 338 microseconds and 2.5 milliseconds on CPython. In my test, it overtakes the non-import version at a power limit of about 10^9.

              Like

            • Jim Randell's avatar

              Jim Randell 1:47 pm on 23 January 2024 Permalink | Reply

              I was more interested in algorithmic efficiency, so I was testing by generating the first 5 million perfect powers, and using primes for exponents was a clear winner on all the Python environments I tested.

              I generally have the enigma module loaded, so there is essentially no overhead in using the primes generator, but the candidate exponents can be any superset of the primes (as the algorithm will discard duplicates), so you can use something like [[ irange(3, inf, step=2) ]] if you don’t want to use primes, and still get a performance improvement.

              Like

    • GeoffR's avatar

      GeoffR 6:55 pm on 19 January 2024 Permalink | Reply

      from enigma import nsplit
      
      # A manual search for all three digit powers gave the following list
      d3_powers = [125, 128, 216, 243, 256, 512, 529, 576, 625, 729, 784, 961]
      
      for r in range(123, 987):  # result of the sum
        g, h, i = nsplit(r)
        if len(set((g, h, i))) != 3:continue
        if r in d3_powers:continue
        
        # find the two summands, which must both be powers
        for n in d3_powers:
          for m in d3_powers:
            if n == m:continue
            a, b, c = nsplit(n)
            d, e, f = nsplit(m)
            if len(set((a, b, c, d, e, f))) == 6:
              if m + n == r:
                if m > n and len(set((a,b,c,d,e,f,g,h,i))) == 9:
                  print(f"The sum is {m} + {n} = {r}.")
      
      

      Like

    • GeoffR's avatar

      GeoffR 8:27 pm on 19 January 2024 Permalink | Reply

      Seems faster to use the remaining digits, after the two squares, for the addition result.

      
      from enigma import nsplit
      
      # A manual search for all three digit powers gave the following list
      d3_powers = [125, 128, 216, 243, 256, 512, 529, 576, 625, 729, 784, 961]
      
      # find the two summands, which must both be powers
      for n in d3_powers: 
          for m in d3_powers:
            if n == m:continue
            a, b, c = nsplit(n)
            d, e, f = nsplit(m)
            if len(set((a, b, c, d, e, f))) == 6:
              
              # find result of additon from remaining digits
              g, h, i = set(range(1,10)).difference((set((a, b, c, d, e, f))))
              if len(set((g, h, i))) != 3:continue
              r = 100*g + 10*h + i
              if r in d3_powers:continue
              if m + n == r:
                if m > n and len(set((a, b, c, d, e, f, g, h, i))) == 9:
                  print(f"The sum is {m} + {n} = {r}.")
      

      Like

    • GeoffR's avatar

      GeoffR 2:50 pm on 22 January 2024 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 1..9:B; var 1..9:C;
      var 1..9:D; var 1..9:E; var 1..9:F;
      var 1..9:G; var 1..9:H; var 1..9:I;
      
      var 100..999:ABC == 100*A + 10*B + C;
      var 100..999:DEF == 100*D + 10*E + F;
      var 100..999:GHI == 100*G + 10*H + I;
      
      constraint all_different ([A,B,C,D,E,F,G,H,I]);
      
      constraint ABC + DEF == GHI;
      
      set of int: powers == {125,128,216,243,256,512,529,576,625,729,784,961};  
      
      constraint ABC > DEF /\ ABC in powers /\ DEF in powers;
      
      solve satisfy;
      output ["Sum is " ++ show(ABC) ++ " + " ++ show(DEF) ++ " = " ++ show(GHI)];
      
      

      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