Recent Updates Page 17 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 10:03 am on 24 March 2024 Permalink | Reply
    Tags:   

    Brain-Teaser 351: Birthday party 

    From The Sunday Times, 28th January 1968 [link]

    Some years ago the Bell family were holding their usual annual special birthday party. Four members of the family, of four different generations, had birthdays on the same day of the year. They were old Adam, his son Enoch, Enoch’s son Joseph and Joseph’s son David. On this occasion David remarked that the sum of any three of their four ages was a perfect square.

    Some years later old Adam died on his birthday, but it so happened that on the very same day David’s son Samuel was born, and the annual party was continued in subsequent years.

    In 1967 at the usual party Samuel made exactly the same remark that David had made, on the previous occasion.

    In what year did Adam die and how old was he then?

    (Perhaps I should mention that no one survived to be 100!).

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

    [teaser351]

     
    • Jim Randell's avatar

      Jim Randell 10:03 am on 24 March 2024 Permalink | Reply

      This Python program considers possible potential gaps between generations, and then looks for ages that makes any subset of size 3 sum to a square. Once we have found potential candidates we look for a pair with suitable overlapping gaps.

      It runs in 221ms. (Internal runtime is 144ms).

      # generate possible (<gaps>, <ages>)
      def generate():
        # suppose the gaps between generations are in the range [15, 40]
        # which means the sum of the 3 gaps lies between 45 and 97
        for g in irange(45, 97):
          for (p, q, r) in decompose(g, 3, increasing=0, sep=0, min_v=15, max_v=40):
            # consider lowest age
            for a in irange(1, 98 - g):
              b = a + p
              c = b + q
              d = c + r
              t = a + b + c + d
              if not all(is_square(t - x) for x in (a, b, c, d)): continue
              printf("[({r}, {q}, {p}) -> ({d}, {c}, {b}, {a})]")
              yield ((r, q, p), (d, c, b, a))
      
      # look for gaps (p, q, r) -> (q, r, s)
      for (((p, q, r), (A1, E1, J1, D1)), ((q_, r_, s), (E2, J2, D2, S2))) in subsets(generate(), size=2, select='P'):
        if not (q_ == q and r_ == r and E2 > E1): continue
        # event 2 is in 1967
        y2 = 1967
        y1 = y2 - (E2 - E1)
        printf("{y1} (A={A1} E={E1} J={J1} D={D1}) -> {y2} (E={E2} J={J2} D={D2} S={S2})")
        b = y1 - A1  # A's birth year
        d = y2 - S2  # A's death year = S's birth year
        a = d - b # A's age at death
        printf("-> A born {b}; died {d}, aged {a}")
      

      Solution: Adam died in 1953, on his 96th birthday.


      There are only three viable candidate lists:

      (58, 41, 22, 1) with gaps of (17, 19, 21)
      (78, 57, 34, 9) with gaps of (21, 23, 25)
      (89, 66, 41, 14) with gaps of (23, 25, 27)

      An age of 1 might be a little young to be making observations, but in any case only the final two candidates have suitable overlapping gaps.

      So, at the first event we have:

      Adam = 78; Enoch = 57; Joseph = 34; David = 9

      And at the second event:

      Enoch = 89; Joseph = 66; David = 41; Samuel = 14

      This is 32 years later than the first event, and as it happened in 1967 the first event was in 1935.

      So Adam was born in 1857, and Samuel was born in 1953, the same year Adam died. So Adam died on his 96th birthday.

      Like

    • Frits's avatar

      Frits 12:40 pm on 24 March 2024 Permalink | Reply

      Faster and probably easier to understand.
      I have also not put in an age restriction for the youngest family member to make observations.

       
      from enigma import SubstitutedExpression, subsets
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 100):
        vs = set() 
        if d > 69: vs.update('A')
        # not excluding young fathers (with such names)
        for i in range(4):
          if not (10 * i <= d < 10 * (i + 7)): vs.update('ABCD'[i])
        d2i[d] = vs  
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # increasing ages A, B, C and D
          "B > (A + 10)",
          "C > (B + 10)", 
          "is_square(A + B + C)",
          "D > (C + 10)", 
          # the sum of any three of their four ages was a perfect square
          "all(is_square(x + y + z) for x, y, z in subsets(@ages, 3))"
        ],
        base=100,
        macro=dict([("ages", "[D, C, B, A]")]),
        answer="@ages",
        d2i=d2i,
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      # a,     e,     j,     d
      # e + n, j + n, d + n, s 
      for (a, e, j, d), (e2, j2, d2, s)  in subsets(p.answers(), 2, select="P"):
        # same age increase for Enoch, Joseph and David
        if len(set([e2 - e, j2 - j, d2 - d])) != 1: continue
        n = e2 - e
        # some years later old Adam died on his birthday
        if s >= n - 2: continue
        print(f"answer: Adam died in {1967 - s} at the age of {a + n - s}")    
      

      Like

      • Jim Randell's avatar

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

        @Frits: Yes, it a good idea to check three of the values sum to a square before moving on to the fourth.

        This Python program runs in 70ms. (Internal runtime is 11ms).

        from enigma import (irange, powers, decompose, subsets, is_square, singleton, printf)
        
        # generate possible (<gaps>, <ages>)
        def generate():
          # choose possible squares for b + c + d
          for ta in powers(10, 15):
            for (b, c, d) in decompose(ta, 3, increasing=1, sep=15, min_v=16, max_v=99):
              # find values for a
              for a in irange(1, b - 15):
                t = ta + a
                if not all(is_square(t - x) for x in (b, c, d)): continue
                printf("[ages = ({d}, {c}, {b}, {a})]")
                yield (d, c, b, a)
        
        # look for gaps (p, q, r) -> (q, r, s)
        for ((A1, E1, J1, D1), (E2, J2, D2, S2)) in subsets(generate(), size=2, select='P'):
          g = singleton({E2 - E1, J2 - J1, D2 - D1})
          if g is None or not (g > 0): continue
          # event 2 is in 1967
          y2 = 1967
          y1 = y2 - g
          printf("{y1} (A={A1} E={E1} J={J1} D={D1}) -> {y2} (E={E2} J={J2} D={D2} S={S2})")
          b = y1 - A1  # A's birth year
          d = y2 - S2  # A's death year = S's birth year
          a = d - b # A's age at death
          if not (d > y1 and a < 100): continue
          printf("-> A born {b}; died {d}, aged {a}")
        

        Like

      • Frits's avatar

        Frits 2:21 pm on 24 March 2024 Permalink | Reply

        It is getting more and more annoying to have to do an import or specify a function for a normal operation like ceil.

        This program performs the same as Jim’s 2nd program and seems to have more requirements checks than Jim.

        @Jim, it is not immediately clear to me from your code that Adam didn’t die as a centenarian or that Samuels’ age isn’t too high.

        from itertools import permutations
        from math import ceil
        
        # minimal difference between generations
        diff = 15
        mxA = 98   # if Adam dies one year after the party he still is only 99
        
        d6 = 6 * diff
        sqs = set(i * i for i in range(ceil(d6**.5), int((4 * mxA - d6)**.5) + 1))
        
        sols = []
        for A in range(1, mxA - 3 * diff + 1):
          for B in range(A + diff, mxA - 2 * diff + 1):
            for C in range(B + diff, mxA - diff + 1):
              if (A + B + C) not in sqs: continue
              for D in range(C + diff, mxA + 1):
                # borrowed from Jim
                if any((A + B + C + D - x) not in sqs for x in [D, C, B, A]): 
                  continue
                sols.append([D, C, B, A])
        
        # a,     e,     j,     d
        # e + n, j + n, d + n, s 
        for (a, e, j, d), (e2, j2, d2, s) in permutations(sols, 2):
          # same age increase for Enoch, Joseph and David
          if len(set([e2 - e, j2 - j, d2 - d])) != 1: continue
          n = e2 - e
          # some (1 or more) years later Adam died on his birthday and 
          # no one (read Adam) survived to be 100
          if s >= n or a + n - s > 99: continue
          print(f"answer: Adam died in {1967 - s} at the age of {a + n - s}")     
        

        Like

        • Jim Randell's avatar

          Jim Randell 2:59 pm on 24 March 2024 Permalink | Reply

          @Frits: Yes, for completeness we can add a check to ensure a < 100 and d > y1.

          Fortunately it doesn’t remove the single candidate solution.

          Like

      • Frits's avatar

        Frits 8:29 pm on 24 March 2024 Permalink | Reply

        Inspired by Brian. Fastest approach sofar (5ms with PyPy).

             
        from itertools import combinations
        
        # minimal difference between generations
        diff = 15
        
        sqs = [sq for n in range(1, 20) 
               if 3 * (diff + 1) < (sq := n * n) <= 3 * (99 - diff)]
        
        # find sets of four different values, all less
        # than 100, for which any three add to a square
        quads = []
        # pick four squares (a + b + c, a + b + d, a + c + d, b + c + d)
        for sq1, sq2, sq3, sq4 in combinations(sqs, 4):
          a, r = divmod(sq1 + sq2 + sq3 - 2 * sq4, 3)
          if r or a < 1: continue
          
          if (d := sq4 - sq1 + a) > 99: continue
          b, c = sq2 - a - d, sq3 - a - d
        
          # check difference between generations
          if any(y - x < diff for x, y in zip((a, b, c), (b, c, d))): continue
           
          quads.append((a, b, c, d))
        
        # consider the two events mentioned
        for p in quads:
          for q in quads:
            if p == q:
              continue
            #               ages in 1967 
            (d, j, e, a), (s, d_, j_, e_) = p, q
            # the age gap between the two events must 
            # be the same for David, Joseph and Enoch
            if len({d_ - d, j_ - j, e_ - e}) == 1:
              gap = e_ - e
              if s < gap and (ad := a + gap - s) < 100:
                print(f"Adam died in {1967 - s} at age {ad}.")   
        

        Like

        • Jim Randell's avatar

          Jim Randell 10:01 pm on 24 March 2024 Permalink | Reply

          Starting from the squares is an even better idea.

          If we have four ages (a, b, c, d) (in ascending order), and each 3-subset sums to a square number, we have:

          a + b + c = u²
          a + b + d = v²
          a + c + d = w²
          b + c + d = x²

          The squares also appear in ascending order, and the difference between consecutive squares is the difference between consecutive ages.

          Then by summing the equations we get:

          t = a + b + c + d = (u² + v² + w² + x²) / 3

          And given the squares we can recover the ages:

          a = t − x²
          b = t − w²
          c = t − v²
          d = t − u²

          Here’s my version. It has an internal runtime of 349µs.

          from enigma import (powers, sqrtc, sqrtf, subsets, div, singleton, printf)
          
          # generate possible ages
          def generate():
            g = 15  # min generation gap
            # find possible sets of 4 squares
            m = 3 + 3*g
            for (u2, v2, w2, x2) in subsets(powers(sqrtc(m), sqrtf(300 - m)), size=4):
              # check generation gaps
              if v2 - u2 < g or w2 - v2 < g or x2 - w2 < g: continue
              # 3(a + b + c + d) = u^2 + v^2 + w^2 + x^2
              t = div(u2 + v2 + w2 + x2, 3)
              if t is None: continue
              # calculate (a, b, c, d)
              (a, b, c, d) = (t - x2, t - w2, t - v2, t - u2)
              if not (0 < a < b < c < d < 100): continue
              printf("[ages = ({d}, {c}, {b}, {a})]")
              yield (d, c, b, a)
          
          # look for ages at the 2 events
          for ((A1, E1, J1, D1), (E2, J2, D2, S2)) in subsets(generate(), size=2, select='P'):
            # gap between events
            g = singleton({E2 - E1, J2 - J1, D2 - D1})
            if g is None or not (g > 0): continue
            # event 2 is in 1967
            y2 = 1967
            y1 = y2 - g
            printf("{y1} (A={A1} E={E1} J={J1} D={D1}) -> {y2} (E={E2} J={J2} D={D2} S={S2})")
            b = y1 - A1  # A's birth year
            d = y2 - S2  # A's death year = S's birth year
            # check timeline
            if not (y1 + 1 < d < 100 + b): continue
            # output solution
            printf("-> A born {b}; died {d}, aged {a}", a=d - b)
          

          Like

  • Unknown's avatar

    Jim Randell 4:39 pm on 22 March 2024 Permalink | Reply
    Tags:   

    Teaser 3209: All in order 

    From The Sunday Times, 24th March 2024 [link] [link]

    Audley’s age is a two-figure number. He has that number of cards and on them he has spelt out the consecutive numbers from one up to and including his age, (“one”, “two”, etc) with one number on each card.

    Then he has arranged the cards in a row in alphabetical order. It turns out that two of the numbers are in the “correct” place (i.e. in the same place as if he had arranged the cards in numerical order).

    If he had done all this a year ago, or if he repeated this whole exercise in a year’s time, there would be no card in the correct place.

    How old is he?

    [teaser3209]

     
    • Jim Randell's avatar

      Jim Randell 4:51 pm on 22 March 2024 Permalink | Reply

      This Python program generates (age, correct) pairs for increasing ages, and then looks for a sequence of (0, 2, 0) occurring in the correct values, to find the required age.

      It runs in 58ms. (Internal runtime is 585µs).

      from bisect import insort
      from enigma import (irange, inf, int2words, tuples, unzip, printf)
      
      # generate (<age>, <correct>) values for increasing ages
      def generate(m=inf):
        # numbers in order (numerically and alphabetically)
        (num, lex) = (list(), list())
        for n in irange(1, m):
          # insert the next number
          w = int2words(n)
          num.append(w)
          insort(lex, w)
          # how many are in the correct position?
          p = sum(x == y for (x, y) in zip(lex, num))
          yield (n, p)
      
      # find triples of (<age>, <correct>) with <correct> = 0, 2, 0
      for ts in tuples(generate(100), 3):
        (ages, ps) = unzip(ts)
        if ps == (0, 2, 0) and 9 < ages[1] < 100:
          printf("{ages} -> {ps}")
      

      Solution: Audley is 87.

      The two numbers in the correct position are “eleven” and “sixty two”.

      The next time it would happen is when Audley is 172.

      Like

      • Jim Randell's avatar

        Jim Randell 8:39 am on 24 March 2024 Permalink | Reply

        Or a version that considers ages in decreasing order:

        It runs in 63ms. (Internal runtime is 517µs).

        from enigma import (irange, int2words, tuples, unzip, printf)
        
        # generate (<age>, <correct>) values for decreasing ages
        def generate(n):
          # numbers in alphabetical order
          lex = sorted(irange(1, n), key=int2words)
          # consider decreasing ages
          while lex:
            # how many in the correct position
            k = sum(x == i for (i, x) in enumerate(lex, start=1))
            yield (n, k)
            lex.remove(n)
            n -= 1
        
        # find triples of (<age>, <correct>) with <correct> = 0, 2, 0
        for ts in tuples(generate(100), 3):
          (ages, ps) = unzip(ts)
          if ages[1] < 10: break
          if ps == (0, 2, 0):
            printf("{ages} -> {ps}")
        

        Like

    • Frits's avatar

      Frits 5:25 pm on 22 March 2024 Permalink | Reply

      Used some code from [ https://s2t2.home.blog/2021/04/15/teaser-2786-out-of-order/ ]

       
      from bisect import insort
      
      upto19 = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven',
                'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen',
                'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen',
                'nineteen']
      asof20 = ['twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy',
                'eighty', 'ninety']
      
      # convert integer to word
      i2w = lambda i: "one hundred" if i == 100 else upto19[i] if i < 20 else \
                      asof20[(i - 20) // 10] + " " + upto19[(i - 20) % 10]
                     
      n_order = [i2w(n) for n in range(1, 9)]
      a_order = sorted(n_order)
      last3 =[-1, -1, -1]
      
      # also consider two ages outside Audley's age boundaries
      for a in range(9, 101):
        # age as a word
        w = i2w(a)
        # cards in numerical order
        n_order.append(w)
        # insert into a list in alphabetical order
        insort(a_order, w)
      
        # numbers in the "correct" place
        n_correct = sum(x == y for x, y in zip(n_order, a_order))
        if (last3 := last3[1:] + [n_correct]) == [0, 2, 0]:
          print(f"answer: {a - 1}")     
      

      Like

      • Frits's avatar

        Frits 11:59 am on 23 March 2024 Permalink | Reply

        First item of ‘upto19’ can also be set to ‘\b’ (backspace) to remove the trailing blank for numbers 20, 30, 40, …

        Like

  • Unknown's avatar

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

    Brainteaser 1647: Happy Easter 

    From The Sunday Times, 3rd April 1994 [link]

    Here is a sum with an odd total in which digits have been consistently replaced by letters, different letters being used for different digits:

    Find the value of EGGS.

    [teaser1647]

     
    • Jim Randell's avatar

      Jim Randell 10:27 am on 21 March 2024 Permalink | Reply

      Using the [[ SubstitutedExpression ]] solver from the enigma.py library.

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      # the alphametic sum
      "DEAR + READER + HAPPY = EASTER"
      
      # the total is odd
      --extra="R % 2 = 1"
      
      --answer="EGGS"
      

      Solution: EGGS = 4882.

      The sum is: 1403 + 340143 + 60997 = 402543.

      Which assigns 9 of the ten digits, leaving G = 8.

      Like

    • GeoffR's avatar

      GeoffR 7:03 pm on 21 March 2024 Permalink | Reply

      from itertools import permutations
      digits = set('0123456789')
      
      for R in (1,3,5,7,9):
          r = str(R)
          q1 = digits - {r}
          for p1 in permutations(q1, 3):
              d, e, a = p1
              dear = int(d + e + a + r)
              reader = int(r + e + a + d + e + r)
              q2 = q1 - set(p1)
              for p2 in permutations(q2, 3):
                  h, p, y = p2
                  happy = int(h + a+ p + p + y)
                  EASTER = dear + reader + happy
                  easter = str(EASTER)
                  if easter[0] == e and easter[4] == e:
                      if easter[1] == a and easter[-1] == r:
                         s, t = easter[2], easter[3]
                         used = set((e, a, s, t, r, d, h, p, y))
                         if len(used) == 9:
                             g = digits - used
                             G = int(g.pop())
                             E, S = int(e), int(s)
                             EGGS = 1000*E + 110*G +  S
                             print(f"EGGS = {EGGS}")
                             print(f"Sum: {dear} + {reader} + {happy} = {EASTER}")
      # EGGS = 4882           
      # Sum: 1403 + 340143 + 60997 = 402543
      

      Like

    • Frits's avatar

      Frits 11:11 am on 22 March 2024 Permalink | Reply

      from functools import reduce
      
      # convert digits sequence to number
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
      
      dgts = set(range(10))
      
      # R is odd as total is odd but not 5 (Y becomes 5) or 9 (E becomes 10)
      for R in [1, 3, 7]:
        if len(q1 := dgts - {R, E := R + 1, Y := 10 - R}) != 7: continue
        for A in q1:
          # carry + E + H = 10 + A --> A < carry + E
          if not (A < 2 + E): continue
         
          if len(q2 := q1 - {A, P := 9 - A}) != 5: continue
          APPY = d2n([A, P, P, Y])
          for D in q2:
            if D == 0: continue
            DEAR = d2n([D, E, A, R])
            READER = d2n([R, E, A, D, E, R])
         
            # sum last 4 columns
            carry, tot = divmod(DEAR + READER % 10000 + APPY, 10000)
            # carry + E + H = 10 + A
            H = 10 + A - carry - E
            if H in {0, D} or H not in q2: continue
           
            # tot must be equal to STER
            S, T = divmod(tot // 100, 10)
         
            if len(q3 := q2 - {D, H, S, T}) != 1: continue
           
            HAPPY = 10000 * H + APPY
            EASTER = d2n([E, A, S, T, E, R])
            if DEAR + READER + HAPPY != EASTER: continue
      
            print(f"answer: EGGS = {d2n([E, (G := q3.pop()), G, S])}")
            '''
            print()
            print(f"{DEAR:>6}")
            print(f"{READER:>6}")
            print(f"{HAPPY:>6}")
            print("------")
            print(f"{EASTER:>6}")
            '''
      

      Like

    • Ruud's avatar

      Ruud 6:42 am on 21 April 2024 Permalink | Reply

      Brute force, extremely simple, solution. Runs within 30 seconds …

      import itertools
      from istr import istr
      
      for d, e, a, r, h, p, y, s, t,g in istr(itertools.permutations("0123456789", 10)):
          if r.is_odd() and (d | e | a | r) + (r | e | a | d | e | r) + (h | a | p | p | y) == (e | a | s | t | e | r):
              print('eggs = ', e|g|g|s)
      

      Like

      • Frits's avatar

        Frits 10:55 am on 22 April 2024 Permalink | Reply

        Hi Ruud, with CPython your version runs for 140seconds on my PC. A similar program without the istr package runs for 3 seconds with CPython (PyPy is faster).

        from itertools import permutations
         
        for d, e, a, r, h, p, y, s, t, g in permutations("0123456789"):
          if (r in "13579" and "0" not in (d + r + h + e) and
              int(d+e+a+r) + int(r+e+a+d+e+r) + int(h+a+p+p+y) == int(e+a+s+t+e+r)):
            print('eggs = ', e+g+g+s)     
        

        Like

    • GeoffR's avatar

      GeoffR 11:10 am on 21 April 2024 Permalink | Reply

      Using primary school addition method of columns and carry digits.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:D; var 1..9:E; var 0..9:A; var 1..9:R;
      var 0..9:S; var 0..9:T; var 0..9:G; var 1..9:H;
      var 0..9:P; var 0..9:Y;
      var 1000..9999:EGGS = 1000*E + 110*G + S;
      
      constraint R in {1,3,5,7,9};
      
      % Column carry digits from right hand end
      var 0..2:c1; var 0..2:c2; var 0..2:c3; var 0..2:c4; var 0..2:c5; 
      
      constraint all_different ([D, E, A, R, S, T, G, H, P, Y]);
      
      % Adding columns from the right hand end
      constraint (R + R + Y) mod 10 == R /\ c1 == (R + R + Y) div 10;
      constraint (c1 + A + E + P) mod 10 == E /\ c2 == (c1 + A + E + P) div 10;
      constraint (c2 + E + D + P) mod 10 == T /\ c3 ==  (c2 + E + D + P) div 10;
      constraint (c3 + D + A + A) mod 10 == S /\ c4 ==  (c3 + D + A + A) div 10;
      constraint (c4 + E + H) mod 10 == A /\ c5 == (c4 + E + H) div 10;
      constraint E == R + c5;
      
      solve satisfy;
      output ["EGGS = " ++ show(EGGS) ++ "\n" 
      ++ "([D, E, A, R, S, T, G, H, P, Y] = "  ++ show([D, E, A, R, S, T, G, H, P, Y]  )];
       
      % EGGS = 4882
      % ([D, E, A, R, S, T, G, H, P, Y] = 
      %  [1, 4, 0, 3, 2, 5, 8, 6, 9, 7]
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 8:54 am on 19 March 2024 Permalink | Reply
    Tags:   

    Teaser 2673: Footprints 

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

    A cubical dice, with faces labelled as usual, is placed in one of the nine squares of a three-by-three grid, where it fits exactly. It is then rotated about one of its edges into an adjacent square and this is done a total of eight times so that the dice visits each square once. The “footprint” of the route is the total of the nine faces that are in contact with the grid.

    (a) What is the lowest footprint possible?
    (b) What is the highest footprint possible?
    (c) Which whole numbers between those two values cannot be footprints?

    [teaser2673]

     
    • Jim Randell's avatar

      Jim Randell 8:55 am on 19 March 2024 Permalink | Reply

      I used the [[ Cube() ]] class to deal with rotations of the standard die (see: Teaser 2835).

      This Python program considers possible representative starting squares on the grid (a corner, an edge, the centre square) using a standard right-handed die, and then positioning it in all possible orientations it looks for possible footprint values as it moves around the grid. (We get the same results with the remaining squares on the grid, and also if we were to use a left-handed die).

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

      from enigma import (irange, append, diff, printf)
      from cube import (Cube, U, D, L, R, F, B)
      
      # possible moves:
      #
      #  1 2 3
      #  4 5 6
      #  7 8 9
      #
      move = {
        1: { F: 2, L: 4 },
        2: { B: 1, F: 3, L: 5 },
        3: { B: 2, L: 6 },
        4: { R: 1, F: 5, L: 7 },
        5: { R: 2, B: 4, F: 6, L: 8 },
        6: { R: 3, B: 5, L: 9 },
        7: { R: 4, F: 8 },
        8: { R: 5, B: 7, F: 9 },
        9: { R: 6, B: 8 },
      }
      
      # calculate footprint totals
      # d = current die orientation
      # i = current die position
      # t = accumulated footprint total
      # vs = visited positions
      def footprints(d, i, t=0, vs=set()):
        # add in the value on the D face
        t += d.faces[D]
        vs = append(vs, i)
        # have we visited each square in the grid?
        if len(vs) == 9:
          yield t
        else:
          # make a move
          for (r, j) in move[i].items():
            if j not in vs:
              yield from footprints(d.rotate([r]), j, t, vs)
      
      # a standard die (U, D, L, R, F, B)
      die = (1, 6, 2, 5, 3, 4)
      
      # accumulate footprint totals
      ts = set()
      # consider possible starting squares: corner = 1, edge = 2, centre = 5
      for i in [1, 2, 5]:
        # consider possible initial orientations for the die
        for d in Cube(faces=die).rotations():
          # calculate footprint totals
          ts.update(footprints(d, i))
      
      # calculate min/max footprints
      (a, b) = (min(ts), max(ts))
      # output solution
      printf("min = {a}; max = {b}; missing = {ms}", ms=diff(irange(a, b), ts))
      

      Solution: (a) The minimum possible footprint is 21; (b) The maximum possible footprint is 42; (c) 29 and 34 cannot be footprints.

      The following paths starting in the centre square (5) and visiting (5, 6, 3, 2, 1, 4, 7, 8, 9) (i.e. spiralling out from the centre) give the minimum and maximum:

      (1); F → (2); R → (4); B → (1); B → (3); L → (2); L → (4); F → (1); F → (3) = footprint 21
      (6); F → (5); R → (4); B → (6); B → (3); L → (5); L → (4); F → (6); F → (3) = footprint 42


      With a bit of analysis we can get a faster program:

      There are only 3 essentially different paths (every other path is a reflection/rotation/reversal of one of these):

      If we start at the beginning of one of these paths with a die showing (x, y, z) around one corner, and opposite faces (X, Y, Z) (so x + X = y + Y = z + Z = 7), then we get the following footprints:

      X + z + x + y + z + Y + X + z + x = 21 + 3z
      X + z + x + y + X + z + x + y + z = 14 + 2y + 3z
      X + z + y + X + Z + y + z + X + Z = 14 + 2y + 3X

      (y, z) = (2, 1) gives a minimum of 21 in second equation, and (y, z) = (5, 6) gives a maximum of 42.

      We can examine the full range of footprints by considering the value of 1 face in the first equation, and 2 adjacent faces in the second (or third) equation.

      This Python program examines all possible footprints, and has an internal runtime of 63µs.

      from enigma import (irange, diff, printf)
      
      # calculate footprint totals
      scores = [1, 2, 3, 4, 5, 6]
      ts = set()
      for x in scores:
        ts.add(21 + 3*x)
        for y in scores:
          if x == y or x + y == 7: continue
          ts.add(14 + 2*x + 3*y)
      
      # calculate min/max footprints
      (a, b) = (min(ts), max(ts))
      # output solution
      printf("min = {a}; max = {b}; missing = {ms}", ms=diff(irange(a, b), ts))
      

      Like

  • Unknown's avatar

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

    Brain-Teaser 948: Journey north-east 

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

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

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

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

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

    On each stretch Charles averages 40 miles per hour.

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

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

    [teaser948]

     
    • Jim Randell's avatar

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

      See: Teaser 3140, where we determined:

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

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

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

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

      Solution: The total time taken is 93 minutes.

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

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

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

      So each arrives at the destination after 93 minutes.

      Like

    • Lise Andreasen's avatar

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

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

      Like

  • Unknown's avatar

    Jim Randell 4:45 pm on 15 March 2024 Permalink | Reply
    Tags:   

    Teaser 3208: The scores are all square 

    From The Sunday Times, 17th March 2024 [link] [link]

    For Skaredahora’s quartet four players read the same musical score, but from different compass directions. There are symbols of three types, indicating different whole number beat durations, on a square 17×17 grid. Each player reads the beat position in their left-to-right direction, and pitch in their bottom-to-top.

    Each player plays four notes; South reads a plus at (beat,pitch) position (3,12), a circle at (14,1), a cross at (16,3), and a plus at beat 9. For example, if a cross indicates three beats, South plays a note of pitch 3 at beat 16, which is still sounding at beats 17 and 18, while East plays a note of pitch 2 sounding at beats 3, 4 and 5.

    No player sounds more than one note at the same time. All possible pitch differences between notes sounding simultaneously from different players occur, except zero and exactly one other value.

    Which non-zero pitch difference never occurs? What pitch does South play at beat 9?

    [teaser3208]

     
    • Jim Randell's avatar

      Jim Randell 6:10 pm on 15 March 2024 Permalink | Reply

      This program considers possible positions for the unspecified “+” note, and then constructs the notes heard at each beat for each player and checks the remaining constraints.

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

      from enigma import (irange, inf, tuples, cproduct, subsets, diff, singleton, printf)
      
      # fill out beats for notes in <X>, durations <dd>
      def beats(X, dd):
        bs = [0] * 22
        for (b, p, d) in X:
          for i in irange(dd[d]):
            i += b
            if bs[i] != 0: return
            bs[i] = p
        return bs
      
      # check a set of beats
      def check(bs):
        if None in bs: return
        # find differences between non-zero pitches
        ds = set()
        for ps in zip(*bs):
          ds.update(abs(x - y) for (x, y) in subsets(filter(None, ps), size=2))
        # differences exclude 0 and exactly one other value
        if 0 in ds: return
        return singleton(diff(irange(1, 16), ds))
      
      # duration symbols
      ks = "ox+"
      
      # suppose the unspecified '+' is at pitch <x> for S
      for x in irange(1, 17):
        # construct the notes (<beat>, <pitch>, <duration>) for each player
        S = [(3, 12, '+'), (9, x, '+'), (14, 1, 'o'), (16, 3, 'x')]
        N = list((18 - b, 18 - p, d) for (b, p, d) in S)
        W = list((18 - p, b, d) for (b, p, d) in S)
        E = list((p, 18 - b, d) for (b, p, d) in S)
      
        # determine maximum possible note duration
        dm = dict((k, inf) for k in ks)
        for X in [S, N, W, E]:
          for ((a, _, k), (b, _, _)) in tuples(sorted(X), 2):
            dm[k] = min(dm[k], b - a)
        if 0 in dm.values(): continue
      
        # choose durations
        for ds in cproduct(irange(1, dm[k]) for k in ks):
          if len(set(ds)) != 3: continue
          dd = dict(zip(ks, ds))
      
          # record pitches on each beat
          (bS, bN, bW, bE) = (beats(X, dd) for X in [S, N, W, E])
          k = check((bS, bN, bW, bE))
          if not k: continue
      
          # output solution
          printf("S={bS}", bS=bS[1:])
          printf("N={bN}", bN=bN[1:])
          printf("W={bW}", bW=bW[1:])
          printf("E={bE}", bE=bE[1:])
          printf("x={x} dd={dd} -> k={k} S[9]={S9}", S9=bS[9])
          printf()
      

      Solution: A pitch difference of 4 does not occur. South plays pitch 17 at beat 9.

      The note durations are:

      o = 1 beat
      x = 2 beats
      + = 5 beats

      Here is a diagram of the notes played:

      Like

      • Frits's avatar

        Frits 3:08 pm on 22 March 2024 Permalink | Reply

        @Jim, only thing to improve for a general solution is getting rid of the hardcoded 22 in line 5.

        Like

    • Frits's avatar

      Frits 5:23 pm on 16 March 2024 Permalink | Reply

      Based on part of Jim’s first published program.

       
      from enigma import SubstitutedExpression, subsets
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        if d == 0: vs.update('KLM')
        if d > 1: vs.update('U')
        if d > 2: vs.update('KL') # by manual inspection of S resp. N
        if d > 5: vs.update('M')  # by manual inspection of S
        d2i[d] = vs  
      
      # check for 15 different pitch differences (without zero)
      def check(K, L, M, UV):
        S = sorted([(3,      12, M), (9,  UV, M), (14,      1, K), (16,  3, L)])
        E = sorted([(UV,      9, M), (1,   4, K), (3,       2, L), (12, 15, M)])
        N = sorted([(2,      15, L), (4,  17, K), (9, 18 - UV, M), (15,  6, M)])
        W = sorted([(18 - UV, 9, M), (6,   3, M), (15,     16, L), (17, 14, K)])
        d = dict()
        for pl in [S, E, N, W]:
          done = set()  
          for (b, p, s) in pl:
            for j in range(s):
              # no simultaneous notes by the same player
              if (k := b + j - 1) in done: return False
              d[k] = d.get(k, []) + [p]
              done.add(k)
              
        # pitch differences
        diffs = set(abs(p2 - p1) for vs in d.values() for p1, p2 in subsets(vs, 2))
        if 0 in diffs or len(diffs) != 15: return False
        
        return set(range(1, 17)).difference(diffs).pop()
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # duration 'o': K, duration 'x': L, duration '+': M
          # the unspecified pitch
          "0 < UV < 18",
          # main checks
          "(missing := check(K, L, M, UV)) != 0",
        ],
        answer="(K, L, M, UV, missing)",
        d2i=d2i,
        # different whole number beat durations
        distinct=("KLM"),
        env=dict(check=check),
        verbose=0,    # use 256 to see the generated code
      ) 
      
      # print answers
      for ans in p.answers():
        print(f"{ans}")
        print("answer: pitch difference that never occurs:", ans[4])
        print("        South plays pitch", ans[3], "at beat 9")    
      

      Like

  • Unknown's avatar

    Jim Randell 11:52 am on 12 March 2024 Permalink | Reply
    Tags:   

    Teaser 2646: Monday birthdays 

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

    In one auspicious month last year our family had two great celebrations: Harry, the oldest member of the family, had his 100th birthday and, in the same month, his great-grandson Peter was born.

    It turns out that they were both born on the same day of the week. Of course, Harry has celebrated several of his 100 birthdays on a Monday, as will Peter. However, even if Peter lives that long, the number of his first 100 birthdays that fall on a Monday will be two fewer than Harry’s.

    On which day of the week were they born?

    [teaser2646]

     
    • Jim Randell's avatar

      Jim Randell 11:52 am on 12 March 2024 Permalink | Reply

      This puzzle was set in 2013, so Harry had his 100th birthday in 2012, and so was born in 1912.

      Peter was born in the same month in 2012 as Harry’s 100th birthday, so Peter’s 100th birthday will be in 2112.

      And we are interested in counting birthdays that fall on a Monday.

      This Python program collects candidate birthdays >(month, day) for each of them and groups them by the number of Monday birthdays in the first 100. We then look for situations where Harry has 2 more Monday birthdays that Peter, and then try to find common (weekday, month) combinations.

      It runs in 63ms. (Internal runtime is 3.1ms).

      from datetime import (date, timedelta)
      from enigma import (defaultdict, group, intersect, printf)
      
      # collect birthdays between y1 and y2 that fall on a Monday
      def collect(y1, y2):
        # find the first Monday (weekday = 0) in the range
        inc = timedelta(days=1)
        d = date(y1, 1, 1)
        while d.weekday() != 0: d += inc
        # and then skip forward by weeks
        inc = timedelta(days=7)
        # count the number of Monday birthdays by date
        r = defaultdict(int)
        while True:
          r[(d.month, d.day)] += 1
          d += inc
          if d.year > y2: break
        # group dates by Monday count
        return group(r.keys(), by=r.get)
      
      # count birthdays on a Monday for Harry from 1913 - 2012
      gH = collect(1913, 2012)
      # count birthdays on a Monday to Peter from 2013 - 2112
      gP = collect(2013, 2112)
      
      # collect results
      rs = set()
      
      # consider possible counts for H
      for (kH, vH) in gH.items():
        # P has a count that is 2 less
        kP = kH - 2
        vP = gP.get(kP)
        if vP is None: continue
      
        # collect possible (weekday, month) combinations for Harry and Peter
        fn = lambda d: (d.weekday(), d.month)
        dH = group((date(1912, m, d) for (m, d) in vH), by=fn)
        dP = group((date(2012, m, d) for (m, d) in vP), by=fn)
      
        # look for (weekday, month) combinations common to both
        for (w, m) in intersect([dH.keys(), dP.keys()]):
          rs.add(w)
      
      # find possible days of the week
      days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      for w in rs:
        printf("weekday = {w}", w=days[w])
      

      Solution: Harry and Peter were both born on a Tuesday.

      And they could be both born on Tuesdays in any month from March to December.

      For example:

      Harry was born on Tuesday, 5th March 1912, and has had 15 birthdays on a Monday up to 2012 (in years: 1917, 1923, 1928, 1934, 1945, 1951, 1956, 1962, 1973, 1979, 1984, 1990, 2001, 2007, 2012).

      Peter was born on Tuesday, 6th March 2012, and will have 13 birthdays on a Monday up to 2112 (in years: 2017, 2023, 2028, 2034, 2045, 2051, 2056, 2062, 2073, 2079, 2084, 2090, 2102).

      The sequences differ because 2000 was a leap year, but 2100 will not be.

      Like

  • Unknown's avatar

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

    Brain teaser 1023: A mixed maths class 

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

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

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

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

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

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

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

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

    [teaser1023]

     
    • Jim Randell's avatar

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

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

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

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

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

      The only scenario is:

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

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

      Which gives the following sequence of scores:

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

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

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

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

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

      Like

    • GeoffR's avatar

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

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

      Like

  • Unknown's avatar

    Jim Randell 4:42 pm on 8 March 2024 Permalink | Reply
    Tags:   

    Teaser 3207: Dodecahedra 

    From The Sunday Times, 10th March 2024 [link] [link]

    Fabulé’s next creation will be a set of equal-sized silver regular dodecahedra, but some of the faces will be gold-plated. He is undecided whether to go ahead with either a “Charm” set or a “Partial” set.

    “Charm” is composed of dodecahedra with at least one gold-plated face but with no gold-plated face having a common side with more than one other gold-plated face. “Partial” is composed of dodecahedra with exactly six gold-plated faces. All the items in each set are distinguishable.

    What is the maximum number of dodecahedra possible in (a) “Charm”, (b) “Partial”?

    [teaser3207]

     
    • Jim Randell's avatar

      Jim Randell 5:58 pm on 8 March 2024 Permalink | Reply

      See also: Teaser 2538.

      This Python program constructs the 60 rotations of the faces of a regular dodecahedron, and then uses them to count the number of distinguishable dodecahedra in each set.

      It runs in 140ms. (Internal runtime is 64ms).

      from enigma import (rotate, flatten, irange, subsets, printf)
      
      # arrangement of adjacent faces
      adj = {
        0: [1, 2, 3, 4, 5],
        1: [0, 5, 8, 7, 2],
        2: [0, 1, 7, 6, 3],
        3: [0, 2, 6, 10, 4],
        4: [0, 3, 10, 9, 5],
        5: [0, 4, 9, 8, 1],
      }
      # add in the opposite faces
      opp = lambda f: 11 - f  # opposite face
      adj.update((opp(k), list(opp(v) for v in reversed(vs))) for (k, vs) in list(adj.items()))
      
      # make a set of rotations with upper face U, adjacent faces fs
      def make_rots(U, fs):
        for _ in fs:
          r = [U] + fs
          r.extend(reversed(list(opp(f) for f in r)))
          yield r
          fs = rotate(fs, 1)
      
      # construct the 60 rotations of the dodecahedron
      rots = flatten(make_rots(k, vs) for (k, vs) in adj.items())
      
      # apply a rotation to a set of faces
      rot = lambda r, fs: tuple(sorted(r[f] for f in fs))
      
      # canonical colouring for a set of faces
      canonical = lambda fs: min(rot(r, fs) for r in rots)
      
      # "Charm" = colour some faces; no more than 2 adjacent
      rs = set()
      # choose coloured faces
      for fs in subsets(irange(12), min_size=1, max_size=4, fn=set):
        # check no coloured face has more than 1 coloured neighbour
        if not any(len(fs.intersection(adj[f])) > 1 for f in fs):
          rs.add(canonical(fs))
      printf("Charm = {n} dodecahedra", n=len(rs))
      
      # "Partial" = colour 6 of the faces
      rs = set(canonical(fs) for fs in subsets(irange(12), size=6))
      printf("Partial = {n} dodecahedra", n=len(rs))
      

      Solution: (a) Charm has a maximum of 11 dodecahedra; (b) Partial has a maximum of 24 dodecahedra.

      Here is a diagram of the 11 possible arrangements for “Charm”:

      There is 1 colouring with 1 gold face; 3 colourings with 2 gold faces; 3 colourings with 3 gold faces; 4 colourings with 4 gold faces.

      The first 4 colourings in the diagram have no shared edges between gold faces, the next 4 have one pair of faces that share an edge, and the final 3 have two separate pairs of faces that share an edge.

      And here are the 24 possible arrangements for “Partial” (6 coloured faces):

      Like

    • Hugo's avatar

      Hugo 9:08 am on 18 March 2024 Permalink | Reply

      I had a think about other Platonic solids.
      For the tetrahedron there is one way to have no gilt faces, one way to gild one face, one way to gild two faces (because any two faces share an edge), one way to gild three faces (because that leaves just one face silver so is just the inverse of one gilt face), and one way to gild all four faces.
      For the cube there is one way to have no gilt faces or all six faces gilt, one way to gild one face or five faces, two ways to gild two faces (either adjacent or opposite) and therefore two ways to gild four faces, two ways to gild three faces (all sharing a vertex, or two opposite faces and one between them).

      The octahedron exceeds my power of visualizing in three dimensions. Anyone have any ideas?

      Like

      • Jim Randell's avatar

        Jim Randell 9:41 am on 18 March 2024 Permalink | Reply

        In Teaser 2538 we looked at colouring some of the the faces of an octahedron.

        The code for calculating the “Partial” collection can be used to find the colourings of the dodcahedron:

        0 (or 12) → 1
        1 (or 11) → 1
        2 (or 10) → 3
        3 (or 9) → 5
        4 (or 8) → 12
        5 (or 7) → 14
        6 → 24
        total = 96

        OEIS A098112 gives the total number of colourings of the icosahedron as 17824.

        Like

    • Hugo's avatar

      Hugo 10:35 am on 18 March 2024 Permalink | Reply

      Thank you, Jim. I’d forgotten that.

      Like

  • Unknown's avatar

    Jim Randell 8:57 am on 5 March 2024 Permalink | Reply
    Tags:   

    Teaser 2649: Right to left 

    From The Sunday Times, 30th June 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. In fact the word CORRECT represents a nine-figure number. It turns out that:

    TO
    REFLECT
    RIGHTTOLEFT

    are three even palindromes.

    What is the CORRECT number?

    [teaser2649]

     
    • Jim Randell's avatar

      Jim Randell 8:58 am on 5 March 2024 Permalink | Reply

      See also: Teaser 2859.

      We can use the [[ SubstitutedExpression ]] solver to solve this puzzle.

      Encoding the conditions directly gives us a run file that executes is 1.4s, but we can speed thing up a bit by checking the starts and ends of partial palindromes to make sure they match. By considering (R, T), (RE, CT) and (RI, FT) we bring the runtime down to 95ms (and the internal runtime of the generated program down to 18ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # use base 26 for values 0-25
      --base=26
      
      --invalid="0,CRT" # no leading zeros in words
      --invalid="0|10|20,OT" # no trailing zeros in palindromes
      --invalid="1|3|5|7|9|11|13|15|17|19|21|23|25,OT" # O and T must end in an even digit
      --invalid="1|3|5|7|9|11-19,RT" # R and T must start with an even digit
      
      # check digits form palindromic numbers
      --code="pal = lambda ds: is_palindrome(join(ds))"
      "pal([T, O])"
      "pal([R, E, F, L, E, C, T])"
      "pal([R, I, G, H, T, T, O, L, E, F, T])"
      
      # check digits have n characters
      --code="length = lambda ds: len(join(ds))"
      "length([C, O, R, R, E, C, T]) == 9"
      
      # [optional] speed things up by checking for partial palindromes
      --code="check = lambda xs, ys: zip_eq(join(xs), reverse(join(ys)))"
      "check([R], [T])"
      "check([R, E], [C, T])"
      "check([R, I], [F, T])"
      
      --answer="(C, O, R, R, E, C, T)"
      --template=""
      

      Solution: CORRECT = 124421124.

      We get the following assignments:

      C=1, E=21, F=12, G=11, I=22, O=2, R=4, T=24
      C=1, E=21, F=12, G=11, I=22, O=2, R=4, T=24
      C=1, E=21, F=12, G=11, I=22, O=2, R=4, T=24

      which gives CORRECT (= 1:2:4:4:21:1:24).

      And one of:

      H=20, L=0
      H=23, L=3
      H=25, L=5

      Which give the palindromes:

      TO = 24:2
      REFLECT = 4:21:12:x:21:1:24
      RIGHTTOLEFT = 4:22:11:2x:24:24:2:x:21:12:24
      where x = 0, 3, 5

      Liked by 1 person

    • Frits's avatar

      Frits 6:27 am on 6 March 2024 Permalink | Reply

      Reusing part of Jim’s code.

       
      n2d     = lambda n: [n] if n < 10 else n2d(n // 10) + [n % 10]
      jn      = lambda s, fn = lambda x: x: [fn(y) for x in s for y in n2d(x)]
      is_pal  = lambda *s: (j := jn(s)) == j[::-1]
      check   = lambda xs, ys: all(x == y for x, y in zip(jn(xs), jn(ys)[::-1]))
      
      palins = ["TO", "REFLECT", "RIGHTTOLEFT"]
      letters = {y for x in palins for y in x}
      B = 26
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(B):
        vs = set()
        if d == 0: vs.update('CORT')     # no leading zeros in words
        if (d % 10) in {10, 20}: 
          vs.update('OT')                # no trailing zeros in palindromes
        if d % 2: vs.update('OT')        # O and T must end in an even digit
        if (int(str(d)[::-1])) % 2: 
          vs.update('RT')                # R and T must start with an even digit
      
        d2i[d] = vs 
      
      # valid digits
      d2v = {lt: {k for k, vs in d2i.items() if lt not in vs} for lt in letters}
      
      if 1:
        print("domain:")
        for k, vs in d2v.items():
          print(f"{k}: {','.join(str(v) for v in vs)}")
      
      # return a valid loop structure string, indent after every "for" statement
      def indent(s):
        res, ind = "", 0
        for ln in s:
          res += " " * ind + ln + "\n"
          if len(ln) > 2 and ln[:3] == "for":
            ind += 2
        return res   
      
      # generate expression for unused letters in palindrome <p>     
      def add_palin_expressions(p, extras=[], exprs=[], done=set()):
        s, i, part = p, 0, dict()
        
        # loop over all letters in <s>
        while s:
          if (lt := s[0]) not in done:
            exp = f"for {lt} in {d2v[lt]}.difference([{','.join(done)}]):"
            exprs.append(exp.replace(", ", ","))
          
          done.add(lt)
          # fill left/right parts  
          part[i % 2] = part.get(i % 2, []) + [lt]
          i += 1
          if i > 1: # we have more than one letter
            if len(s) > 1:  # we'll use is_pal for the last one
              exprs.append(f"if not check([{', '.join(part[0])}], [{', '.join(part[1][::-1])}]): continue")
            # add extra condition if all variables are present  
            for extra in extras : 
              if len(done | extra[0]) == len(done):
                exprs.append(f"if not ({extra[1]}): continue")
                extras.remove(extra) 
          # remove precessed letter and start from the other side        
          s = s[1:][::-1]
      
        exprs.append(f"# final palindrome check for {p}")  
        exprs.append(f"if not is_pal({', '.join(p)}): continue")  
        
        return extras, exprs, done
      
      # extra conditions besides palindromes  
      conds = [(set("CORRECT"), "len(jn([C, O, R, R, E, C, T])) == 9")]
      es, dn = [], set()
      # main loop
      for pal in palins:
        es.append(f"# -----  process palindrome {pal}  -----")
        conds, es, dn = add_palin_expressions(pal, conds, es, dn)
        es.append(" ")
      
      # add final print statement for the answer
      es.append("print('answer: CORRECT =', ''.join(jn([C, O, R, R, E, C, T], fn=str)), end =', ')")
      ltrs = "{" + '=}, {'.join(letters) + "=}"
      es.append(f"print(f'{ltrs}')")
      
      exprs = indent(es)
      
      print(exprs)    
      exec(exprs)   
      

      Like

  • 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.

        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).

      #! 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).

      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).

      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).

      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.

      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).

      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).

      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:

        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).

      #! 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).

      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 [@github].

      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

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

Why are you reporting this comment?

Report type
Design a site like this with WordPress.com
Get started