Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 9:54 am on 6 July 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 803: Three perfect squares 

    From The Sunday Times, 5th December 1976 [link]

    To code his message, the Agent began by writing three words — each word being a number, e.g. FIVE. In doing this, he did not write any single letter of the alphabet more than once.

    For each of the letters thus written, he then substituted a digit and, in doing so, he used each of the digits 0 to 9 inclusive once (and once only).

    He now had three numbers (all in figures) each of which was a perfect square. By adding these three numbers together, he obtained a total running to five figures.

    In place of the digits in this total he now wrote the letters for which each of them had originally been substituted. This gave the letters NONSO.

    What were the three perfect squares (in figures)?

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

    [teaser803]

     
    • Jim Randell's avatar

      Jim Randell 9:54 am on 6 July 2021 Permalink | Reply

      The three numbers used (when considered as words) must use 10 different letters between them, and the result is 5 letters long. So each of the numbers has at most 5 letters. But if one were to have 5 letters, that would only leave 5 letters for the remaining 2 numbers, and there are no numbers with 2 (or fewer) letters. So the numbers must have (4, 3, 3) letters.

      This Python program collects numbers with fewer than 5 letters, and finds 3 of them that between them use 10 different letters (including the letters of NONSO). We then use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve the alphametic sum.

      It runs in 64ms.

      Run: [ @replit ]

      from enigma import irange, int2words, subsets, union, SubstitutedExpression, sprintf, printf
      
      # collect numbers (as words) with fewer than 5 letters
      words = list(x.upper() for x in map(int2words, irange(0, 10)) if len(x) < 5 and x.isalpha())
      
      # choose three words
      for ws in subsets(words, size=3):
        # check there are 10 different symbols (including NOS)
        ss = union(ws)
        if not(len(ss) == 10 and ss.issuperset('NOS')): continue
      
        # solve the alphametic puzzle
        exprs = list(sprintf("is_square({w})") for w in ws)
        t = sprintf("{ws[0]} + {ws[1]} + {ws[2]} = NONSO")
        exprs.append(t)
        p = SubstitutedExpression(exprs, verbose=0)
        for s in p.solve():
          printf("{t} | {s}", s=p.substitute(s, t))
      

      Solution: The three squares are: 9025, 784, 361.

      These correspond to:

      FOUR = 9025 (= 95²)
      SIX = 784 (= 28²)
      TEN = 361 (= 19²)

      Like

    • GeoffR's avatar

      GeoffR 7:28 pm on 6 July 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % sets of three and four digit squares
      set of int: sq = {n*n | n in 10..99};
      
      % he used each of the digits 0 to 9 inclusive once only
      var 1..9:A; var 0..9:B; var 0..9:C; 
      var 1..9:D; var 0..9:E; var 0..9:F; 
      var 1..9:G; var 0..9:H; var 0..9:I; var 0..9:J; 
      
      constraint all_different([A, B, C, D, E, F, G, H, I, J]);
      
      % 2 no. 3-digit and 1 no. 4-digit squares - ex Jim's analysis
      var 100..999:ABC = 100*A + 10*B + C;
      var 100..999:DEF = 100*D + 10*E + F;
      var 1000..9999:GHIJ = 1000*G + 100*H + 10*I + J;
      
      var 10000..99999:NONSO; % the sum of the three squares
      
      constraint ABC in sq /\ DEF in sq /\ GHIJ in sq;
      
      constraint NONSO ==  ABC + DEF + GHIJ;
      
      % check digit pattern of NONSO for 'N' and 'O'
      constraint NONSO div 10000 == NONSO div 100 mod 10
      /\ NONSO div 1000 mod 10 == NONSO mod 10;
      
      solve satisfy;
      
      output ["Three squares are " ++ show(ABC) ++ ", " ++ show(DEF) 
      ++ ",  " ++ show(GHIJ) ++ "\n" ++ 
      "NONSO, the 5-digit sum = " ++ show(NONSO)];
      
      % Three squares are 361, 784,  9025
      % NONSO, the 5-digit sum = 10170
      % ----------
      % Three squares are 784, 361,  9025
      % NONSO, the 5-digit sum = 10170
      % ----------
      % ==========
      
      
      
      

      Like

      • Frits's avatar

        Frits 6:08 pm on 7 July 2021 Permalink | Reply

        @GeoffR,

        It is not directly stated but I think N, O and S are meant to be different digits. It doesn’t matter for the solution.

        Like

    • John Crabtree's avatar

      John Crabtree 10:31 am on 9 July 2021 Permalink | Reply

      FIVE and NINE only allow one 3-digit number (TWO), and so FOUR is the four-digit number and must contain 0, and, as F = 8 or 9, so must be 9025, 9604 or 9801.
      The sum of the digits of NONSO = 0 mod 9. N = 1. If O = 8, S = 0 which is invalid. If O = 6, S = 4, which is invalid. And so FOUR = 9025, S = 7, ie SIX = 784, which leaves TEN = 361.

      Like

    • GeoffR's avatar

      GeoffR 12:22 pm on 9 July 2021 Permalink | Reply

      # three/four digit squares list
      sq = [x * x for x in range(10, 100)]
      
      digits = set('1234567890')
      
      from itertools import permutations
      
      # 1st stage permutation
      for p1 in permutations('1234567890', 3):
        A, B, C = p1
        if A == '0':
          continue
        ABC = int(A + B + C)
        if ABC not in sq:
          continue
        
        # 2nd stage permutation
        q1 = digits.difference(p1)
        for p2 in permutations(q1, 3):
          X, Y, Z = p2
          if X == '0':
            continue
          XYZ = int(X + Y + Z)
          if XYZ not in sq:
            continue
      
          # 3rd stage permutation
          q2 = q1.difference(p2)
          for p3 in permutations(q2):
            P, Q, R, S = p3
            if P == '0':
              continue
            PQRS = int(P + Q + R + S)
            if PQRS not in sq:
              continue
      
            # form sum of three squares
            NONSO = ABC + XYZ + PQRS
            # check digit pattern of NONSO
            if 10000 < NONSO < 100000:
              if NONSO // 1000 % 10 == NONSO % 10:  # 'O'
                if NONSO // 10000 == NONSO // 100 % 10: # 'N' 
                  print(f"Three Squares are {ABC}, {XYZ}, {PQRS}")
                
      # Three Squares are 361, 784, 9025
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:22 am on 4 July 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 38: Digital computer 

    From The Sunday Times, 10th December 1961 [link]

    My nephew Gregory is keen on calendars — an odd interest for a bank clerk.

    “On my next birthday”, he told me yesterday, “I shall take the day off work to celebrate”.

    “To celebrate what?” I asked.

    “To celebrate the fact that it will be the same day of the week as that on which I was born. Moreover, the sum of the digits in the date (e.g. 26/11/1961 = 27) will equal my age, as would also have been the case if had been born just four weeks later than I was”.

    What was Gregory’s date of birth?

    [teaser38]

     
    • Jim Randell's avatar

      Jim Randell 11:23 am on 4 July 2021 Permalink | Reply

      This Python program runs in 54ms.

      Run: [ @replit ]

      from datetime import date, timedelta
      from enigma import dsum, printf
      
      # digit sum of a date
      ddsum = lambda d: dsum(d.day) + dsum(d.month) + dsum(d.year)
      
      # generate dates in a specific range
      inc1 = timedelta(days=1)
      def drange(a, b, step=inc1):
        while not(a > b):
          yield a
          a += step
      
      # consider date of next birthday (within 1 year of the puzzle date)
      for d in drange(date(1961, 12, 10), date(1962, 12, 9)):
        w = d.weekday()
        # must be a workday
        if w == 6: continue
        
        # calculate digit sum (= age)
        n = ddsum(d)
      
        # and calculate the date n years ago
        b = date(d.year - n, d.month, d.day)
      
        # do days of the week match?
        if b.weekday() == w:
      
          # if the birthday was 28 days later ...
          b_ = b + 28 * inc1
          # the digit sum of the nth birthday is also n
          if ddsum(date(b_.year + n, b_.month, b_.day)) == n:
            printf("{d} ({n}) -> born = {b} (+4w = {b_})")
      

      Solution: Gregory was born on Friday, 2nd February 1940.

      So:

      His next birthday, on Friday, 2nd February 1962, is his 22nd.
      And 2+2+1+9+6+2 = 22.

      If he had been born exactly 4 weeks later, it would have been, Friday, 1st March 1940.
      His next birthday, on Thursday, 1st March 1962, is his 22nd.
      And 1+3+1+9+6+2 = 22.

      There is a second candidate solution:

      Gregory was born on Sunday, 11th February 1940.
      His next birthday, on Sunday, 11th February 1962, is his 22nd.
      And 1+1+2+1+9+6+2 = 22.

      If he had been born exactly 4 weeks later, it would have been, Sunday, 10th March 1940.
      His next birthday, on Saturday, 10th March 1962, is his 22nd.
      And 1+0+3+1+9+6+2 = 22.

      But this is eliminated as his next birthday is on a Sunday, so he wouldn’t have to take the day off work.

      Like

    • Jim Olson's avatar

      Jim Olson 9:41 pm on 6 July 2021 Permalink | Reply

      Four weeks after 2/22/1940 would have been 3/21/1940.

      Like

      • Jim Randell's avatar

        Jim Randell 9:51 pm on 6 July 2021 Permalink | Reply

        But if he was born on 22nd February 1940 his next birthday in 1962 would be his 22nd, but the sum of the digits in the date (22/2/1962) would be 24, so it doesn’t work.

        Like

    • Jim Olson's avatar

      Jim Olson 10:06 pm on 6 July 2021 Permalink | Reply

      I think the wording of this Teaser through me off. I see that the 3/21/1962 digits don’t total 22 and that was my problem. 3/1/1940 is not four weeks after his birth. So I didn’t see how this satisfied the requirements of the teaser.

      Like

      • Jim Randell's avatar

        Jim Randell 10:40 pm on 6 July 2021 Permalink | Reply

        1st March 1940 is exactly 4 weeks after 2nd February 1962 because 1940 was a leap year. So the month number goes up 1 and the day number goes down 1, keeping the sum of the digits the same. (Of course the fact that 1962 is not a leap year means that if he had been born 4 weeks later, his next birthday would not be on the same day of the week as the day he was born. But the sum of the digits in the date would be the same as his age, and that is what is important).

        Like

    • Jim Olson's avatar

      Jim Olson 11:50 pm on 6 July 2021 Permalink | Reply

      Apologies I must of misread the posting of the answer as 2/22/1940.

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 2 July 2021 Permalink | Reply
    Tags:   

    Teaser 3067: Reciprocal arrangement 

    From The Sunday Times, 4th July 2021 [link] [link]

    Twelve men from our football squad had turned up for training, and I’d promised them a game of six-a-side at the end of the session; so while they were off on a gentle three-mile run I worked out what the two teams would be. They were wearing their squad numbers, which had one or two digits: 2, 3, 4, 5, 6, 7, 8, 9, 15, and three others. It appealed to me when I found that I could divide them into two teams of six, such that the sum of the reciprocals of the squad numbers in each team equalled one exactly.

    What were the squad numbers in the team containing number 2?

    [teaser3067]

     
    • Jim Randell's avatar

      Jim Randell 4:49 pm on 2 July 2021 Permalink | Reply

      (See also: Enigma 348, Enigma 1087, Enigma 1062, Enigma 501).

      I used the [[ reciprocals() ]] function from the enigma.py library (originally written for Enigma 348) to generate the sums of reciprocals. And this lets us keep things entirely in the integer domain.

      This Python program runs in 83ms. (Internal runtime is 21.5ms).

      Run: [ @replit ]

      from enigma import (reciprocals, subsets, union, printf)
      
      # numbers we are given
      numbers = {2, 3, 4, 5, 6, 7, 8, 9, 15}
      
      # look for 6 (different) reciprocals that sum to 1
      ss = reciprocals(6, M=99, g=1)
      
      # choose 2 disjoint sets of numbers
      for (t1, t2) in subsets(ss, size=2):
        ns = union([t1, t2])
        if len(ns) == 12 and ns.issuperset(numbers):
          printf("teams = {t1} vs {t2}")
      

      Solution: The six members of the team containing 2 were: 2, 6, 7, 9, 18, 42.

      And the six members of the other team were: 3, 4, 5, 8, 15, 40.

      Like

      • Jim Randell's avatar

        Jim Randell 5:32 pm on 2 July 2021 Permalink | Reply

        Here’s a slightly longer, but more efficient program.

        It finds three additional shirt numbers that bring the total reciprocal sum for all 12 numbers to 2. And then looks for 5 numbers with a reciprocal sum of 1/2 to go with 2 to make one of the teams.

        Using the [[ rsum() ]] function from the enigma.py library means we don’t have to use rationals.

        It runs in 54ms. (Internal runtime is 815µs).

        from enigma import (rsum, reciprocals, subsets, printf)
        
        # numbers we are given
        numbers = {2, 3, 4, 5, 6, 7, 8, 9, 15}
        
        # find the sum of the reciprocals
        (p, q) = rsum(numbers)
        
        # find 3 more numbers that bring the total reciprocal sum to 2
        for xs in reciprocals(3, q, 2 * q - p, m=10, M=99, g=1):
          ns = numbers.union(xs)
          if len(ns) != 12: continue
        
          # choose 5 numbers to go with 2
          for ns1 in subsets(ns.difference([2]), size=5, fn=list):
            if rsum(ns1) == (1, 2):
              t1 = sorted([2] + ns1)
              t2 = sorted(ns.difference(t1))
              printf("teams = {t1} vs {t2}")
        

        Like

    • Frits's avatar

      Frits 1:56 pm on 3 July 2021 Permalink | Reply

      Based on Jim’s second approach (avoiding rational number logic).

      from enigma import lcm, divisors
      
      # pick <k> elements from list <li> that sum to <s>
      def pickElements(k, li, s, ns=[]):
        if k == 1:
          r = s - sum(ns)
          if r in li and r not in ns:
            yield [L // x for x in ns + [r]]
        else:
          for i in range(len(li)):
            yield from pickElements(k - 1, li[i + 1:], s, ns + [li[i]])
      
      # numbers we are given
      numbers = {2, 3, 4, 5, 6, 7, 8, 9, 15}
      
      L = lcm(*numbers)  # Least Common Multiple
      
      # sum(i=1..12, 1 / x) = 2 then also sum(i=1..12, L / x) = 2 * L
      rest = 2 * L - sum(L // x for x in numbers)
      
      # determine candidates for three remaining squad numbers
      cands = [L // x for x in divisors(L) if 9 < x < 100 and x not in numbers]
      
      last2 = cands[-1] + cands[-2]
      # remove big values from list
      cands = [x for x in cands if x + last2 <= rest]
      
      # pick three remaining squad numbers
      for p in pickElements(3, cands, rest):
        twelve = numbers | set(p)
        (s1, s2) = (twelve.difference([7]), [7])
      
        # in the group containing 7 there must be a multiple of 7 as well
        p7 = [x for x in p if x % 7 == 0]
        if len(p7) == 1:
          s1 = s1.difference([p7[0]])
          s2.append(p7[0])
      
        # determine candidates for the group with 7
        cands2 = [L // x for x in twelve if x not in s2]
        rest2 = L - sum(L // x for x in s2)
       
        # pick squad numbers to go with 7 (and multiple of 7)
        for p2 in pickElements(6 - len(s2), cands2, rest2):
          t1 = p2 + s2
          if 2 in t1:
            print("answer:", sorted(t1))
          else:
            print("answer:", sorted(twelve.difference(t1)))
      

      Like

    • GeoffR's avatar

      GeoffR 7:08 pm on 16 September 2021 Permalink | Reply

      Probably not the best idea to mix floating point and integer numbers generally, but with very small difference tests, it gave the correct solution.

      from itertools import combinations
      
      NL = []   # list for squad numbers of two teams
      
      # set of given squad numbers
      NS = {2, 3, 4, 5, 6, 7, 8, 9, 15}
      
      for p1 in combinations(range(10, 100), 3):
        # find three other squad numbers
        a, b, c = p1
        if any(x in NS for x in (a, b, c)):
          continue
        s12num = 1 / 2 + 1 / 3 + 1 / 4 + 1 / 5 + 1 / 6 + 1 / 7 + \
               1 / 8 + 1 / 9 + 1 / 15 + 1 / a + 1 / b + 1 / c
        if abs(s12num - 2) >= 1e-9:
          continue
        NS = NS.union([a, b, c])  # missing squad numbers
        
        # find numbers in 1st squad
        for p2 in combinations(NS, 6):
          d, e, f, g, h, i = p2
          # find reciprocal sum of 1st squad numbers
          t1 = 1 / d + 1 / e + 1 / f + 1 / g + 1 / h + 1 / i
          if abs(t1) - 1 >= 1e-9:
            continue
          Q = NS.difference([d, e, f, g, h, i])
      
          # find numbers in 2nd squad
          for p3 in combinations(Q, 6):
            j, k, m, n, p, q = p3
            # find reciprocal sum of 2nd squad numbers
            t2 = 1 / j + 1 / k + 1 / m + 1 / n + 1 / p + 1 / q
            if abs(t2) - 1 >= 1e-9:
              continue
            if p2 not in NL: NL.append(p2)
            if p3 not in NL: NL.append(p3)
            
      # find team numbers of two teams
      if len(NL) == 2:
        print(f" 1st Team Numbers are {sorted(NL[0])}")
        print(f" 2nd Team Numbers are {sorted(NL[1])}")
          
      # 1st Team Numbers are [2, 6, 7, 9, 18, 42] <<< Ans
      # 2nd Team Numbers are [3, 4, 5, 8, 15, 40]
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:10 am on 1 July 2021 Permalink | Reply
    Tags: ,   

    Teaser 2799: Desert patrol 

    From The Sunday Times, 15th May 2016 [link] [link]

    Pat is at Base Camp in the desert. There is enough fuel at the base for his vehicle to travel 420 miles, but the vehicle can hold (in its tank and in cans) at most enough fuel for 105 miles. On any journey he can leave some fuel in cans at any point and then pick up some or all of it whenever he passes in future. He has worked out that there is just enough fuel for him to reach the Main Camp.

    How far is it between the two camps?

    [teaser2799]

     
    • Jim Randell's avatar

      Jim Randell 10:11 am on 1 July 2021 Permalink | Reply

      This is a similar idea to Enigma 1732, except that this is for a 1 way journey.

      See: Jeep Problem [ @wikipedia ].

      This Python program runs in 54ms.

      Run: [ @replit ]

      from enigma import (Rational, irange, inf, printf)
      
      Q = Rational()  # choose an implementation of rationals
      
      T = 420  # total amount of fuel available
      R = 105  # range
      
      # consider amount of fuel required for a maximal distance in k trips
      f = d = 0
      for k in irange(1, inf):
        f += R
        d += R * Q(1, 2 * k - 1)
        printf("{k} trips, fuel = {f}, distance = {x:.2f} ({d})", x=float(d))
        # stop when the fuel required reaches (or exceeds) the available fuel
        if not (f < T): break
      

      Solution: The distance between the camps is 176 miles.

      The journey will require 4 trips:

      Trip 1:
      With a full load of fuel (105 miles), drive 15 miles (= 105 / 7).
      Make a fuel dump with 75 miles of fuel, and use the remaining 15 miles to drive back.
      There is 315 miles of fuel remaining at base camp.

      Trip 2:
      With a full load (105 miles), drive 15 miles, and fill up at dump 1 (leaving 60 miles at dump 1).
      Drive a further 21 miles (= 105 / 5), and make a fuel dump with 63 miles of fuel.
      Drive back to dump 1 using the remaining 21 miles of fuel, and refuel for the last 15 miles (leaving 45 miles at dump 1).
      There is 210 miles of fuel remaining at base camp.

      Trip 3:
      With a full load (105 miles), drive 15 miles to dump 1 and refuel (leaving 30 miles at dump 1).
      Drive 21 miles to dump 2 and refuel (leaving 42 miles of fuel at dump 2).
      Drive 35 miles (= 105 / 3) to dump 3 and leave 35 miles of fuel.
      Drive back to dump 2 using the remaining 35 miles of fuel, and refuel for the 21 miles to dump 1 (leaving 21 miles at dump 2).
      Drive back to dump 1 and refuel for the 15 miles to base camp (leaving 15 miles at dump 1).
      There is 105 miles of fuel remaining at base camp.

      Trip 4:
      Fill up with the remaining fuel (105 miles), drive 15 miles to dump 1 and refuel (exhausting dump 1).
      Drive 21 miles to dump 2 and refuel (exhausting dump 2).
      Drive 35 miles to dump 3 and refuel (exhausting dump 3).
      Drive 105 miles (= 105 / 1) and arrive at main camp.
      Total distance = 15 + 21 + 35 + 105 = 176 miles.

      Like

    • Frits's avatar

      Frits 1:44 pm on 8 July 2021 Permalink | Reply

      Similar (next one R=315, T = 5*R)

      T = 420       # total amount of fuel available
      R = 105       # range
      
      k = T // R    # number of trips
      
      # number of times traject <n-1>th depot to the <n>th depot is travelled 
      V = lambda k, n : 2 * (k - n) + 1
      
      # T / R = k = 4 so 4 trips can be made setting up 3 depots.
      # The traject <n-1>th depot to the <n>th depot (n = 1..3) is travelled 
      # V(n) = 2 * (k - n) + 1 times (where 0th depot is the base camp) so we need
      # to split R into V(n) equal parts and store V(n) - 2 parts at the depot 
      # leaving 2 parts to reach and return from the <n>th depot
      # this means the <n>th depot is sum (i=1..n, R / V(n)) miles from base camp
       
      # consider amount of fuel required for a maximal distance in k trips
      print("answer:", R + sum(R // V(k, n) for n in range(1, k)), "miles")
      

      Like

      • Frits's avatar

        Frits 3:39 pm on 8 July 2021 Permalink | Reply

        or

        from math import log
        
        def H(n):
            """Returns an approximate value of n-th harmonic number.
        
               http://en.wikipedia.org/wiki/Harmonic_number
            """
            # Euler-Mascheroni constant
            gamma = 0.57721566490153286060651209008240243104215933593992
            return gamma + log(n) + 0.5 / n - 1 / (12 * n**2) + 1 / (120 * n**4)
            
        R = 105    
        n = 4
        print("answer:", round((H(2 * n - 1) - 0.5 * H(n - 1)) * R), "miles")
        

        Like

    • Frits's avatar

      Frits 1:43 pm on 17 July 2021 Permalink | Reply

      Different order:

      # make 3 return trips to 1st depot (15 miles from Base Camp) and 
      #      each time drop 75 miles of fuel at 1st depot
      # make 4th trip to 1st depot and fill up (210 miles of fuel left at 1st depot)
      # make 2 return trips from 1st to 2nd depot (21 miles from 1st depot) and
      #      each time drop 63 miles of fuel at 2nd depot
      # make 3th trip from 1st to 2nd depot and fill up 
      #    (0 / 105 miles of fuel left at 1st/2nd depot)
      # make a return trip from 2nd to 3rd depot (35 miles from 2nd depot) and
      #      drop 35 miles of fuel at 3rd depot
      # make 2nd trip from 2nd to 3rd depot and fill up
      #    (0 / 0 / 0 miles of fuel left at 1st/2nd/3rd depot)
      # drive 150 miles to Main Camp
      
      # Total miles: 15 + 21 + 35 + 150 = 176 miles
      

      Like

  • Unknown's avatar

    Jim Randell 9:55 am on 29 June 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 790: Multi-value coil 

    From The Sunday Times, 5th September 1976 [link]

    Post Office multi-value coil machines are designed to vend strips of five stamps. Soon after the introduction of the 8½p and 6½p first and second class postal rates the machines sold for 10p a strip of stamps values 6p, 1p, ½p, 2p and ½p respectively (as always with these machines the last stamp being the lowest in case of tearing). While catering for both rates it was impossible to make up either 6½p or 8½p with a joined strip of stamps.

    However an efficiency expert worked out a possible 10p strip which afforded the following:

    (i) from one strip either first or second rate in a joined strip;
    (ii) from two joined strips, three second class rates, each in a joined strip;
    (iii) from three joined strips, two first class rates plus two second class rates,  each in a joined strip.

    Of course the “expert” assumed that all ½p steps from ½p to 10p were available in stamps.

    What was his suggested strip?

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

    [teaser790]

     
    • Jim Randell's avatar

      Jim Randell 9:56 am on 29 June 2021 Permalink | Reply

      Working in units of half-pennies we can keep amounts in integers.

      The value of one complete strip is 20 (= 10p), the first class rate is 17 (= 8.5p), and the second class rate is 13 (= 6.5p).

      This Python 3 program runs in 82ms.

      Run: [ @replit ]

      from enigma import irange, subsets, printf
      
      # decompose total <t> into <k> positive integers
      def decompose(t, k, ns=[]):
        if k == 1:
          yield ns + [t]
        else:
          k -= 1
          for n in irange(1, t - k):
            yield from decompose(t - n, k, ns + [n])
      
      # can values in <vs> be made from segments of <ss>
      def _connected(vs, ss):
        # are we done?
        if not vs:
          return True
        else:
          v = vs[0]
          ss = list(x for x in ss if x)  # drop empty lists
          # consider segments
          for (i, ns) in enumerate(ss):
            if sum(ns) < v: continue  # shortcut
            # and sub-segments
            for (j, k) in subsets(irange(0, len(ns)), size=2):
              if sum(ns[j:k]) == v:
                if _connected(vs[1:], ss[:i] + ss[i + 1:] + [ns[:j], ns[k:]]):
                  return True
          return False
      
      connected = lambda vs, ns: _connected(vs, [ns])
      
      # check a sequence
      def check(ns):
        # (i) 13 and 17 can be made from 1 strip
        if not(connected([13], ns) and connected([17], ns)): return False
        # (ii) three lots of 13 from 2 joined strips
        if not(connected([13] * 3, ns * 2)): return False
        # (iii) two lots of 13 _and_ two lots of 17 from 3 joined strips
        if not(connected([13, 17] * 2, ns * 3)): return False
        # passed
        return True
      
      # look for viable strips
      for ns in decompose(20, 5):
        # lowest value stamp comes last
        if ns[-1] == min(ns) and check(ns):
          # output solution
          printf("strip = {ns}")
      

      Solution: The stamps on the suggested strip were: 1½p, 1½p, 3½p, 3p, ½p.

      Like

  • Unknown's avatar

    Jim Randell 10:28 am on 27 June 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 37: Which page? 

    From The Sunday Times, 3rd December 1961 [link]

    A student desiring to find a reference in a book, could not remember the actual page number, but only the three figures comprising it.

    Before beginning his search, he wrote down in ascending order the six possible variants of the three figures. In writing the second variant his pencil broke on
    a figure which thereafter was illegible. With a fresh pencil he completed the list of variants and then found his reference, the page number being the second variant.

    Some time later he found in his pocket the scrap of paper bearing the six numbers. Idly adding them he obtained a total of 4,796, having unwittingly misread the illegible figure.

    On what page was the reference?

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

    This puzzle was originally published with no title.

    [teaser37]

     
    • Jim Randell's avatar

      Jim Randell 10:29 am on 27 June 2021 Permalink | Reply

      If he had written out each of the 6 numbers correctly (the digits being a, b, c), then each digit would appear twice in each position so the total sum of the six numbers would be:

      T = 222×(a + b + c)

      If the misinterpreted digit is off by x then the total sum is off by 100x if the first digit of the number is misinterpreted, 10x if its the second digit, and x if its the final digit.

      This Python program runs in 49ms.

      Run: [ @replit ]

      from enigma import (irange, subsets, div, nconcat, printf)
      
      M = 4796  # mistaken total
      
      # check to see if "abc" can be misread by d
      def check(a, b, c, d):
        # error in hundreds position? (a)
        x = div(abs(d), 100)
        if x:
          return 0 < (a + x if d > 0 else a - x) < 10
        # error in tens position? (b)
        x = div(abs(d), 10)
        if x:
          return 0 < (b + x if d > 0 else b - x) < 10
        # error in units position? (c)
        return 0 < (c + d) < 10
      
      # consider possible digits in the number
      for (a, b, c) in subsets(irange(1, 9), size=3):
        # the total sum should be...
        T = 222 * (a + b + c)
        # and the difference from the calculated sum
        d = M - T
        # digits of the 2nd number in the list are a, c, b
        if d != 0 and check(a, c, b, d):
          n = nconcat(a, c, b)
          printf("reference = {n} [read as {m}]", m=n + d)
      

      Solution: The reference was on page 198.

      So the sum of the numbers should have been 3996, but the second number was read as 998, adding 800 to the sum.

      Like

      • Frits's avatar

        Frits 10:22 am on 12 August 2025 Permalink | Reply

        from enigma import SubstitutedExpression  
        
        M = 4796  # mistaken total
        vars = "ABC"
        
        # i * j = difference with respect to the correct number 
        for i in range(-9, 10):
          if not i: continue
          for k, j in enumerate([100, 10, 1]):
            t, r = divmod(M - i * j, 222)
            if r: continue
            sum_ABC = str((M - i * j) // 222)
            # illegible figure may also have been a zero if idly adding them
            extra = "0 <= " + vars[k] + " + " + str(i) + " < 10"
            # digits of the 2nd number in the list are a, c, b
            p1 = SubstitutedExpression(["C < B", sum_ABC + " - B - C = A", 
                                        extra, "A < C"],
                                       answer="ABC", 
                                       digits=range(1, 10),
                                       reorder=0, 
                                       verbose=0)
            
            # solve the first alphametic
            for ans in p1.answers():
              print("answer:", ans)
        

        Like

  • Unknown's avatar

    Jim Randell 4:46 pm on 25 June 2021 Permalink | Reply
    Tags:   

    Teaser 3066: Leaning tower of pesos 

    From The Sunday Times, 27th June 2021 [link] [link]

    I have six old coins worth an odd number of pesos, comprising a mixture of one- and two-peso coins. Both denominations are of the same diameter and made of the same metal, but the two-peso coins are twice the thickness of the one-peso coins.

    After making a vertical stack of the coins I then slid each of the top five coins as far as possible to the right, to make the pile lean as much as possible in that direction, without toppling. I managed to get the rightmost edge of the top coin a distance of one and a quarter times its diameter further right than the rightmost edge of the bottom coin.

    Starting from the top, what is the value of each of the six coins?

    [teaser3066]

     
    • Jim Randell's avatar

      Jim Randell 6:04 pm on 25 June 2021 Permalink | Reply

      This is a block-stacking problem [@wikipedia] (or book-stacking, or coin-stacking, …).

      This paper [link] gives a good explanation of the basic idea. (See also this paper [link] for more complicated stackings).

      The lowest coin acts as the table, and we need to balance 5 coins on top of it.

      The following Python program runs in 53ms.

      Run: [ @replit ]

      from enigma import Rational, subsets, printf
      
      Q = Rational()
      
      # stack a collection of equal length blocks, with masses <ms>
      # return  (M, D) = (total mass, total displacement)
      def stack(ms):
        (M, D) = (0, 0)
        for m in ms:
          # considering moments: M' = M + m; M'D' = m(D + 1/2) + MD
          M += m
          D += Q(m, 2 * M)
        return (M, D)
      
      # consider the distribution of the top 5 coins
      for vs in subsets((1, 2), size=5, select="M", fn=list):
        # construct the stack
        (M, D) = stack(vs)
        # the required displacement is 5/4
        if D == Q(5, 4):
          # the bottom coin makes the total value odd
          vs.append((M % 2) + 1)
          # check each denomination is used
          if set(vs) == {1, 2}:
            printf("coins = {vs} [top to bottom]")
      

      Solution: The values of the coins are: 1, 2, 1, 2, 2, 1.

      Here is a diagram of the layout:

      The top coin is displaced from the bottom coin by 1.25 diameters.

      But this is not the maximal possible displacement, the following stacks (top to bottom) give greater displacement:

      (1, 1, 1, 2, 2, x) → 1.260 (529/420)
      (1, 2, 2, 2, 2, x) → 1.287 (811/630)
      (1, 1, 2, 2, 2, x) → 1.292 (31/24)

      Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 11:57 am on 26 June 2021 Permalink | Reply

      I think I have followed a similar approach working from first principles.

      
      #Since the coins have the same diameter we can work in units of one coin radius. The thickness of the coins is irrelevant but we know that their mass is proportionate to their value
      
      #Find the horizontal position of the centre of mass for a list of coins
      #Vector A contains the positions of the centres of the coins and defines the list
      #Vector B contains the values of the coins
      def CoM(A,B):
        C=[a*b for a,b in zip(A[:len(B)],B)]
        return sum(C)/sum(A[:len(B)])
      
      #Loop over possible vectors of values for the top 5 coins (5 binaries = 32)
      for m in range(32):
        vals=[1+int(i) for i in bin(m)[2:].zfill(5)]
        dists=[2.5]#Given the position of the topmost coin...
        for d in range(5):#...find the maximum distance of the rest from the origin
          dists.append(CoM(vals,dists[:d+1])-1)
          #The edge of each coin must be immediately below the centre of mass of those above it
      
        if dists[5] == 0:#The centre of the bottom coin must be at the origin
          vals+=[1-sum(vals)%2]#To make the total value odd
          
          print("The values in order, starting from the top, were:")
          print(vals)
      
      
      

      Like

    • Hugh Casement's avatar

      Hugh Casement 10:04 am on 4 July 2021 Permalink | Reply

      Given that the total value is odd, there must be an odd number of each denomination.
      Admittedly the value of the bottom coin is immaterial.

      I tested it with some parquet-floor slabs. The theoretical overhangs are not achievable in practice, as the stack starts to teeter. But one can get close.

      Like

  • Unknown's avatar

    Jim Randell 12:26 pm on 24 June 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 787: Puzzlers’ office party 

    From The Sunday Times, 15th August 1976 [link]

    Brainbenders and Co., the noted games and puzzles manufacturers, held their office party recently.

    The eight from the planning department shared one round table. Pat Robinson and Ann arrived together. Miss Davis looked sensational in her new dress and Mr Armstrong’s suit was fresh from the cleaners.

    Red-haired John sat on Miss Jones’s right while Mary sat next-but-one to both Miss Brown and Miss Stevens. Joan only had eyes for Mr Evans, opposite her, while her neighbour, Edna, was interested in Fred, who was sitting beside Bill and next-but-one to Miss Stevens.

    Mr Smith was the only man between two girls and Miss Brown the only girl between two fellows. However, only two people sat opposite a person of their own sex.

    What was the seating plan?

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

    [teaser787]

     
    • Jim Randell's avatar

      Jim Randell 12:27 pm on 24 June 2021 Permalink | Reply

      I used the [[ SubstitutedExpression ]] solver from the enigma.py library to find viable assignments of first/last names to positions 0 to 7 around the table.

      The following run file finds the assignments of names to positions:

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # we have 8 first names:
      #  Pat, Ann, John, Mary, Joan, Edna, Fred, Bill
      #  A    B    C     D     E     F     G     H
      #
      # and 8 last names:
      #  Robinson, Jones, Davis, Armstrong, Brown, Stevens, Evans, Smith
      #  S         T      U      V          W      X        Y      Z
      --distinct="ABCDEFGH,STUVWXYZ"
      
      # and we need to assign each to a position 0-7
      --base=8
      
      # start with Pat Robinson in position 0
      --assign="A,0"
      --assign="S,0"
      
      # John sat on Jones' right
      "(C + 1) % 8 = T"
      
      # Mary sat next but 1 to Brown and Stevens
      "abs(D - W) in {2, 6}"
      "abs(D - X) in {2, 6}"
      
      # Joan was opposite Evans
      "(E + 4) % 8 = Y"
      
      # Joan was next to Edna
      "abs(E - F) in {1, 7}"
      
      # Fred was next to Bill, and next but 1 to Stevens
      "abs(G - H) in {1, 7}"
      "abs(G - X) in {2, 6}"
      
      # return the positions for each first name and last name
      --answer="((A, B, C, D, E, F, G, H), (S, T, U, V, W, X, Y, Z))"
      

      We are given the genders associated with 7 of the 8 surnames, so only Robinson remains undecided.

      The following Python program takes the output of the above run file, and performs the gender checks. It runs in 59ms.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, irange, tuples, printf)
      
      # load the solver for the names
      p = SubstitutedExpression.from_file("teaser787.run")
      
      # perform the gender checks
      def check(gs, Smith, Brown):
      
        # only 2 people sat opposite a person of the same gender
        # (i.e. there is exactly 1 pair)
        if not(sum(gs[i] == gs[i + 4] for i in irange(0, 3)) == 1): return False
      
        # find positions of gender X between gender Y
        def between(X, Y):
          for (j, i, k) in tuples(irange(0, 7), 3, circular=1):
            if gs[i] == X and gs[j] == gs[k] == Y:
              yield i
      
        # Smith is the only male between 2 females
        if not(set(between(0, 1)) == {Smith}): return False
        # Brown is the only female between 2 males
        if not(set(between(1, 0)) == {Brown}): return False
      
        # looks OK
        return True
      
      # find solutions for first/last names
      for (s, (fps, lps)) in p.solve(verbose=0):
      
        # assign the genders we know:
        # m = Mr Armstrong, Mr Evans, Mr Smith
        # f = Miss Davis, Miss Jones, Miss Brown, Miss Stevens
        # and choose the remaining value (Robinson)
        for g in (0, 1):
          # map gs: pos -> gender (0=male, 1=female)
          gs = dict(zip(lps, [g, 1, 1, 0, 1, 1, 0, 0]))
          if not check(gs, s['Z'], s['W']): continue
      
          # output solution
          last = dict(zip(lps, ["Robinson", "Jones", "Davis", "Armstrong", "Brown", "Stevens", "Evans", "Smith"]))
          first = dict(zip(fps, ["Pat", "Ann", "John", "Mary", "Joan", "Edna", "Fred", "Bill"]))
          for i in irange(0, 7):
            printf("{i}: ({x}) {first} {last}", i=i + 1, x="mf"[gs[i]], first=first[i], last=last[i])
          printf()
      

      Solution: The seating plan is (clockwise from Pat Robinson):

      1: Pat Robinson (f)
      2: Fred Armstrong (m)
      3: Bill Evans (m)
      4: Ann Brown (f)
      5: John Smith (m)
      6: Mary Jones (f)
      7: Joan Davis (f)
      8: Edna Stevens (f)

      Like

      • Jim Randell's avatar

        Jim Randell 4:03 pm on 24 June 2021 Permalink | Reply

        Or with all the code in a single file:

        from enigma import SubstitutedExpression, irange, tuples, printf
        
        # perform the gender checks
        def check(ns, gs, Smith, Brown):
          # map d: pos -> gender (0=male, 1=female)
          d = dict(zip(ns, gs))
        
          # only 2 people sat opposite a person of the same gender
          # (i.e. there is exactly 1 pair)
          if not(sum(d[i] == d[i + 4] for i in irange(0, 3)) == 1): return False
        
          # find positions of gender X between gender Y
          def between(X, Y):
            for (j, i, k) in tuples(irange(0, 7), 3, circular=1):
              if d[i] == X and d[j] == d[k] == Y:
                yield i
        
          # Smith is the only male between 2 females
          if not(set(between(0, 1)) == {Smith}): return False
          # Brown is the only female between 2 males
          if not(set(between(1, 0)) == {Brown}): return False
        
          # looks OK
          return True
        
        # we have 8 first names:
        #  Pat, Anne, John, Mary, Joan, Edna, Fred, Bill
        #  A    B     C     D     E     F     G     H
        #
        # and 8 last names:
        #  Robinson, Jones, Davis, Armstrong, Brown, Stevens, Evans, Smith
        #  S         T      U      V          W      X        Y      Z
        p = SubstitutedExpression([    
            "(C + 1) % 8 = T",  # John sat on Jones' right
            "abs(D - W) in {2, 6}",  # Mary sat next but 1 to Brown ...
            "abs(D - X) in {2, 6}",  # ... and Stevens
            "(E + 4) % 8 = Y",  # Joan was opposite Evans ...
            "abs(E - F) in {1, 7}",  # ... and next to Edna
            "abs(G - H) in {1, 7}",  # Fred was next to Bill ...
            "abs(G - X) in {2, 6}",  # ... and next but 1 to Stevens
            "check([S, T, U, V, W, X, Y, Z], [N, 1, 1, 0, 1, 1, 0, 0], Z, W)", # gender checks
          ],
          distinct=("ABCDEFGH", "STUVWXYZ"),
          base=8,  # assign each name a position 0-7
          s2d=dict(A=0, S=0),  # start with Pat Robinson in position 0
          d2i=dict((d, 'N') for d in irange(2, 7)),
          env=dict(check=check),
          answer="((A, B, C, D, E, F, G, H), (S, T, U, V, W, X, Y, Z), N)",
          verbose=0
        )
        
        # find solutions for first/last names, and Robinson's gender
        for (s, (fps, lps, g)) in p.solve():
          # output solution
          gs = dict(zip(lps, [g, 1, 1, 0, 1, 1, 0, 0]))
          last = dict(zip(lps, ["Robinson", "Jones", "Davis", "Armstrong", "Brown", "Stevens", "Evans", "Smith"]))
          first = dict(zip(fps, ["Pat", "Anne", "John", "Mary", "Joan", "Edna", "Fred", "Bill"]))
          for i in irange(0, 7):
            printf("{i}: ({x}) {first} {last}", i=i + 1, x="mf"[gs[i]], first=first[i], last=last[i])
          printf()
        

        Like

      • Frits's avatar

        Frits 11:30 am on 1 July 2021 Permalink | Reply

        Adding the following to the run member limits the possibilities to one.

        # Smith is the only male between 2 females
        # this must be John as Fred was next to Bill
        "C == Z",
        
        # Brown is the only female between 2 males
        # so Brown must be next to John
        "abs(C - W) in {1, 7} and (abs(G - W) in {1, 7} or abs(H - W) in {1, 7})"
        
        # only 2 people sat opposite a person of the same gender
        # so all males didn't sit opposite each other
        "4 not in [abs(C - G), abs(C - H)]"
        
        # male surnames are Armstrong, Evans and Smith
        "sorted([C, G, H]) == sorted([V, Y, Z])"
        

        Like

        • Jim Randell's avatar

          Jim Randell 2:36 pm on 1 July 2021 Permalink | Reply

          I wasn’t sure if the setter intended us to infer gender from first names (although clearly “Pat” is deliberately ambiguous).

          But it turns out that it is not necessary to do so to solve the puzzle, so I didn’t.

          Like

  • Unknown's avatar

    Jim Randell 3:24 pm on 22 June 2021 Permalink | Reply
    Tags:   

    Teaser 2798: Housey-housey 

    From The Sunday Times, 8th May 2016 [link] [link]

    Philip, Daphne and I live in different houses on Teaser Drive. Philip lives at number 1 and the houses are then numbered consecutively along one side of the road. The sum of the house numbers strictly between Philip’s house and mine is equal to the sum of the house numbers strictly between my house and Daphne’s. Furthermore, the digits of Daphne’s house number add up to the same total as the digits of my house number.

    What is my house number and what is Daphne’s?

    [teaser2798]

     
    • Jim Randell's avatar

      Jim Randell 3:24 pm on 22 June 2021 Permalink | Reply

      We can calculate the sum of the house numbers between two houses (non-inclusive):

      S(a, b) = (a + 1) + … + (b − 1)
      S(a, b) = T(b − 1) − T(a)

      This program finds the first viable solution. It runs in 51ms.

      Run: [ @replit ]

      from enigma import T, irange, inf, dsum, printf
      
      # calculate the "inter-sum" of a and b
      S = lambda a, b: T(b - 1) - T(a)
      
      # generate solutions
      def solve(P):
        # consider values for setters house number (R)
        for R in irange(P + 1, inf):
          s = S(P, R)
          # look for D with an equal value, and equal digit sum
          for D in irange(R + 1, inf):
            s_ = S(R, D)
            if s_ > s: break
            if s_ == s and dsum(D) == dsum(R):
              yield (R, D)
      
      # look for the first solution
      for (R, D) in solve(1):
        printf("R={R}, D={D}")
        break
      

      Solution: Your house number is 64. Daphne’s house number is 91.

      Like

      • Jim Randell's avatar

        Jim Randell 3:27 pm on 22 June 2021 Permalink | Reply

        The program above finds the smallest solution (which is the required answer), but there are larger solutions. The next one is:

        R=85225144, D=120526555

        The numbers involved are too large to be house numbers, but we can write a more efficient program to investigate larger solutions.

        Ignoring the digit sum condition, we can look for (R, D) values that give equal inter-sums:

        S(1, R) = S(R, D)
        2R² = D² − D + 2
        R² − 1 = D(D − 1)/2

        So we can look for triangular numbers that are one less than a square, and this lets us find the larger solution in a few seconds:

        from enigma import (is_square, dsum, printf)
        
        # generate solutions
        def solve():
          t = 0
          D = 1
          # look for triangular numbers that are 1 less than a square
          while True:
            t += D
            R = is_square(t + 1)
            D += 1
            if R is not None and dsum(D) == dsum(R):
              yield (R, D)
        
        # look for the first solution
        for (R, D) in solve():
          printf("R={R} D={D}")
          break
        

        To get a faster program we can look at the sequence of (R, D) values with equal “inter-sums”:

        R = 1, 1, 2, 4, 11, 23, 64, … [= OEIS A006452]
        D = 0, 1, 3, 6, 16, 33, 91, … [= OEIS A006451 + 1]

        After the first 4 values these sequences can be generated by the following recurrence relations:

        R[n] = 6 R[n − 2] − R[n − 4]
        D[n] = 6 D[n − 2] − D[n − 4] − 2

        We can then test the generated (R, D) values to look for pairs with equal digit sums:

        from enigma import (dsum, first, arg, printf)
        
        # find (R, D) pairs
        def solve():
          # first 4 (R, D) pairs
          q = [(1, 0), (1, 1), (2, 3), (4, 6)]
          
          while True:
            # compute the next pair
            (R2, D2) = q[2]
            (R0, D0) = q[0]
            R = 6 * R2 - R0
            D = 6 * D2 - D0 - 2
            if dsum(R) == dsum(D): yield (R, D)
        
            q.pop(0)
            q.append((R, D))
        
        # find the first N solutions
        N = arg(8, 0, int)
        for (i, (R, D)) in enumerate(first(solve(), N, fn=iter), start=1):
          printf("{i}: R={R}, D={D}")
        

        Here are the first 8 solutions:

        % python3 teaser2798g.py 8
        1: R=64, D=91
        2: R=85225144, D=120526555
        3: R=98349742324, D=139087539451
        4: R=151143569481047247784, D=213749285825577237211
        5: R=1844521723207478395861, D=2608547637051808009585
        6: R=2128576470207852702322273, D=3010261712716195592552833
        7: R=2834655085444781980043036964601, D=4008807666485875255750052272225
        8: R=268047403891416806462822495283352, D=379076273942140382330486943077083
        

        The (R, D) values can also be generated using Pell’s Equation ((2D − 1)² − 8R² = −7), which gives the following recurrence relation (as noted by Brian Gladman [link]):

        R = 1, 1, …
        D = 0, 1, …
        R[n] = 2 D[n − 2] + 3 R[n − 2] − 1
        D[n] = 3 D[n − 2] + 4 R[n − 2] − 1

        Like

    • GeoffR's avatar

      GeoffR 8:01 pm on 22 June 2021 Permalink | Reply

      I tested for a solution for 2-digit house numbers up to 100.

      This solution tests for equality of the sum of two arithmetic series of house numbers:

      1) Between Philip and Me (P and M)
      2) Between Me and Daphne (M and D)

      # P..................M..............D  house numbers
      # no. of houses between M and D is (D - M - 1)
      
      P = 1
      
      for M in range(10, 100):
        for D in range(M+1, 100):
          # no. of houses for series between P -> M
          nh1 = M - 2
          # no. of houses for series between M -> D
          nh2 = D - M - 1
          # S1 = arithmetic sum of numbers between P and M
          S1 = nh1 // 2 * (2 * (P + 1) + (nh1 - 1) * 1)
          # S2 = arithmetic sum of numbers between M and D
          S2 = nh2 // 2 * (2 * (M + 1) + (nh2 - 1) * 1)
          if S1 == S2:
            # digits in house numbers M and D
            d1, d2 = M // 10, M % 10
            d3, d4 = D // 10, D % 10
            # the digits of Daphne’s house number add up to
            # ..the same total as the digits of my house number
            if d1 + d2 == d3 + d4:
              print(f"My house number is {M}.")
              print(f"Daphne's house number is {D}.")
      
      # My house number is 64.
      # Daphne's house number is 91.
      

      Like

    • John Crabtree's avatar

      John Crabtree 4:45 pm on 25 June 2021 Permalink | Reply

      Brian Gladman expresses the Pell equation as
      (2D- 1)^2 – 8R^2 = – 7
      Let E = 2D – 1 and then E^2 -8R^2 = -7
      The Wolfram web page on the Pell equation suggests that there may be multiple series derived from multiple basic solutions. That is the case here. One sequence is
      R = 1, 4, 23, …..
      E = 1, 11, 65, …
      The other sequence is:
      R = 1, 2, 11, 64, …..
      E = -1, 5, 31, 181, ….
      In each case R(n) = 6 * R(n-1) – R(n-2) and E(n) = 6 * E(n-1) – E(n-2)
      This makes it easier to solve the difference equations.
      Or R(n) = 3 * R(n – 1) + E(n – 1) and E(n) = 3 * E(n – 1) + 8(n-1)

      One can show that the fact that the digits in D’s and R’s numbers add to the same number means that R = D = 1 mod 3. The reduces the work in a manual (calculator) search.

      Like

    • GeoffR's avatar

      GeoffR 6:46 pm on 26 June 2021 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 10..99:M;  % My house number
      var 10..99:D;  % Daphne's house number
      
      % house number digits for Me and Daphne
      var 1..9:d1; var 1..9:d2; var 1..9:d3; var 1..9:d4;
      
      % Using Brian's equation: (2.D - 1)^2 - 2(2.M)^2 = -7
      constraint (2*D - 1) * (2*D - 1) - 2 * (2 * M)*(2 * M) == -7;
      
      % Equate sum of digits of M and D
      constraint d1 == M div 10 /\ d2 == M mod 10 /\ d3 == D div 10 
      /\ d4 == D mod 10 /\ (d1 + d2) == (d3 + d4);
      
      solve satisfy;
      
      output[ "My house = " ++ show(M) ++ " and Daphne's house = " ++ show(D) ];
      
      % My house = 64 and Daphne's house = 91
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:18 am on 20 June 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 36: [Telephone numbers] 

    From The Sunday Times, 26th November 1961 [link]

    On one of June’s birthdays her father telephoned some of her friends inviting them to June’s party. He noticed that Joan’s telephone number consisted of the four digits in his own, but in reverse order, and that one phone number divided by the other gave June’s age.

    On a subsequent birthday June rang up Jennifer from a call box and she also noticed that Jennifer’s number and that of the call box were four digit numbers, each the reverse of the other. Dividing these two numbers gave June’s latest age.

    How old was June when the two phone calls were made?

    This puzzle was originally published with no title.

    [teaser36]

     
    • Jim Randell's avatar

      Jim Randell 9:19 am on 20 June 2021 Permalink | Reply

      The phone numbers must be different, so the ages are more than 1 (but less than 10).

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to find candidate phone numbers and ages.

      The following run file executes in 78ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --distinct=""
      
      "div(ABCD, DCBA) > 1"
      
      --answer="(ABCD, DCBA, div(ABCD, DCBA))"
      

      Solution: The calls were made when June was 4 years old and 9 years old.

      The two sets of numbers are:

      8712 / 2178 = 4
      9801 / 1089 = 9

      Like

    • Hugh Casement's avatar

      Hugh Casement 2:33 pm on 20 June 2021 Permalink | Reply

      If they had been five-digit numbers we would have had
      87912 / 21978 = 4
      98901 / 10989 = 9
      It seems we can insert a string of as many 9s in the middle of each number as we like.

      Like

      • Hugh Casement's avatar

        Hugh Casement 3:08 pm on 20 June 2021 Permalink | Reply

        Over the years this has proved surprisingly popular, with slight variants.
        See Enigmas 792 and 1535, Teasers 847, 1663, and 1964.

        Like

    • GeoffR's avatar

      GeoffR 9:01 pm on 21 June 2021 Permalink | Reply

      
      from itertools import permutations
      
      ages = []  # list for June's ages
      
      for p1 in permutations(range(10),4):
        a, b, c, d = p1
        if 0 in (a, d): continue
        abcd = 1000*a + 100*b + 10*c + d
        dcba = 1000*d + 100*c + 10*b + a
        q, r = divmod(abcd, dcba)
        if r == 0:
          ages.append(q)
      
      # two phone calls were made
      if len(ages) == 2:
        print(f"June's ages were {ages[0]} and {ages[1]} years.")
      
      # June's ages were 4 and 9 years.
      
      

      Like

  • Unknown's avatar

    Jim Randell 2:08 pm on 18 June 2021 Permalink | Reply
    Tags:   

    Teaser 3065: Members of the jury 

    From The Sunday Times, 20th June 2021 [link] [link]

    A jury had twelve members, all with different ages (at least 20 but not more than 65), except that two were twins with a common age over 40. The average age was a prime number. A counsel objected to one of the members and he was replaced by another with the result that the average age was reduced to another prime number. Between the two juries, there were twelve different ages and they formed a progression with a common difference (e.g.: 1, 4, 7, 10, 13, etc.; or: 6, 13, 20, 27, 34, etc.). None of the individuals had a perfect square age, and the replacement jury still included both twins.

    How old were the twins?

    [teaser3065]

     
    • Jim Randell's avatar

      Jim Randell 2:38 pm on 18 June 2021 Permalink | Reply

      This Python program runs in 61ms. (Internal runtime is 765µs).

      from enigma import (irange, inf, sq, delete, div, is_prime, seq_items, printf)
      
      # disallowed square ages
      squares = set(sq(i) for i in irange(5, 8))
      
      # consider possible start ages
      for a in irange(20, 54):
        # and possible common differences
        for d in irange(1, inf):
          # the progression consisting of 12 ages
          ns = list(irange(a, a + 11 * d, step=d))
          if ns[-1] > 65: break
          # there are no squares
          if not squares.isdisjoint(ns): continue
      
          # one of the 12 ages is the replacement age (y)
          t = sum(ns)
          for (i, y) in enumerate(ns):
            # the ages of the original jury
            js = delete(ns, [i])
            # duplicate one of them
            for z in reversed(js):
              # duplicate age is over 40
              if z < 41: break
              # and gives a prime mean age
              m1 = div(t - y + z, 12)
              if m1 is None or not is_prime(m1): continue
      
              # replace one of the original ages x with y
              for (_, x) in seq_items(js, i):
                if x == z: continue
                # mean age of the new jury is a lower prime
                m2 = div(t - x + z, 12)
                if m2 is None or not is_prime(m2): continue
      
                # output solution
                printf("twins = {z} [{js} + {z}; avg={m1}; {x} -> {y}; avg={m2}]", js=tuple(js))
      

      Solution: The twins are 41.

      The sequence of ages for both juries are all those ages between 26 and 59 in steps of 3.

      The ages of the original jury are:

      26, 29, 32, 38, 41, 41, 44, 47, 50, 53, 56, 59; mean = 43

      The 59 year old is then replaced by a 35 year old:

      26, 29, 32, 35, 38, 41, 41, 44, 47, 50, 53, 56; mean = 41


      One way to test your program is to remove the constraint that there are no squares. Without this condition you should find 10 different answers for the age of the twins.

      Liked by 1 person

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 12:05 pm on 19 June 2021 Permalink | Reply

      Similar routine but with a bit less trial and error, so marginally quicker

      
      primes=list(SieveOfEratosthenes(65))#function left out for ease of reading
      squares=[x**2 for x in range(4,9)]
      
      for delta in range(1,5): #possible common differences
        for a_min in range(20,66-11*delta): #consequential possible start ages
          ages=[a_min+delta*i for i in range(12)] #sequence of 12 ages
          if set(ages).intersection(squares):continue #no square ages
      
          twins=[t for t in ages if t>40]#subset of possible ages of the twins
          a_ave=a_min+11*delta/2#average of the 12 ages
          min_ave = a_ave+(40-max(ages))/12#lowest possible average age of a jury
          max_ave = a_ave+(max(ages)-min(ages))/12#highest possible average age of a jury
          poss_aves = [p for p in primes if p>=min_ave and p<max_ave]
          if len(poss_aves)<2:continue
          #possible prime average ages for a jury, must be (at least) 2 of them
      
          for twin in twins: #find twins' age by trial and error
            rejects=[]
            for poss_ave in poss_aves:
              swapped=twin+(a_ave-poss_ave)*12#age of swapped juror relative to twins
              if swapped>a_min+11*delta:break
              if swapped >=20:rejects.append(swapped)
              #reject impossible juror ages
      
            if len(rejects)>1: #solution contains at least two swapped juror ages
              print("The ages of the twins was",twin)
            #happily there is one solution and it has exactly two swapped jurors
          
      
      
      
      

      Liked by 1 person

      • Jim Randell's avatar

        Jim Randell 10:20 am on 20 June 2021 Permalink | Reply

        The [[ Primes ]] class in the enigma.py library implements a prime sieve, so you can use the following to avoid having to include the code in your program, but still posting runnable code.

        from enigma import primes
        primes.expand(66)
        

        And it might be worth placing longer comments on the line above the code, as horizontal space is limited in replies.

        Like

        • Jim Randell's avatar

          Jim Randell 10:57 am on 20 June 2021 Permalink | Reply

          @Tony: It’s an interesting approach.

          Here’s a version using a similar idea, but calculates the separation of the replacement/replaced pair in the progression from the difference between the prime means, and then looks for suitable candidates, from which the repeated age can be determined:

          from enigma import (primes, subsets, irange, inf, sq, div, divc, divf, printf)
          
          primes.expand(60)
          
          # disallowed square ages
          squares = set(sq(i) for i in irange(5, 8))
          
          # consider possible start ages
          for a in irange(20, 54):
            # and possible common differences
            for d in irange(1, inf):
              # the sequence of 12 ages
              ns = list(irange(a, a + 11 * d, step=d))
              if ns[-1] > 65: break
              # there are no squares
              if not squares.isdisjoint(ns): continue
              t = sum(ns)
          
              # consider possible mean ages (m2, m1)
              m_min = divc(t + 41 - ns[-1], 12)
              m_max = divf(t + ns[-1] - ns[0], 12)
              for (m2, m1) in subsets(primes.between(m_min, m_max), size=2):
                # calculate separation between replaced and replacement
                k = div(12 * (m1 - m2), d)
                if k is None: continue
          
                # consider possible replacement pairs (x = ns[j - k] replaces y = ns[j])
                for j in irange(11, k, step=-1):
                  # calculate repeated age (z)
                  z = 12 * m2 + ns[j] - t
                  if z < 41: break
                  if z in ns and z != ns[j] and z != ns[j - k]:
                    # output solution
                    printf("twins = {z} [{ns}; {x} replaces {y}; avg: {m1} -> {m2}]", ns=tuple(ns), x=ns[j - k], y=ns[j])
          

          (The internal runtime is reduced to 247µs).

          There is only one candidate pair of prime ages, and only one of the potential replacement/replaced pairings gives a duplicate age greater than 40.

          (And a bit more analysis fixes the values of the common difference, the mean ages, and the difference between the replaced and replacement ages, which allows us to bring the internal runtime down to just 48µs).

          Like

      • Brian Gladman's avatar

        Brian Gladman 10:27 am on 20 June 2021 Permalink | Reply

        A nice approach, Tony, which I ‘stole’ here. For those who want to run your code, here is a one line replacement for line one:

        primes = {x for x in range(23, 63, 2) if all(x % p for p in {2, 3, 5, 7})}
        

        Like

    • GeoffR's avatar

      GeoffR 1:27 pm on 20 June 2021 Permalink | Reply

      Hi Tony,

      I have re-formatted your code to PEP8, which can easily be done under the Wingware IDE for Python,
      using Source/Reformatting from the main menu – hope you don’t mind, but it does make code much
      easier to read and appreciate by others. Have also replaced end of line comments as separate comments, as suggested by Jim. An interesting solution.

      https://wingware.com/downloads

      #primes = list(SieveOfEratosthenes(65))
      # using Brian's replacement for line above
      primes = {x for x in range(23, 63, 2)
                if all(x % p for p in {2, 3, 5, 7})}
      squares = [x**2 for x in range(4, 9)]
      
      # possible common differences
      for delta in range(1, 5):
        # consequential possible start ages
        for a_min in range(20, 66 - 11 * delta):
          # sequence of 12 ages
          ages = [a_min + delta * i for i in range(12)]
          # no square ages
          if set(ages).intersection(squares):
            continue
      
          # subset of possible ages of the twins
          twins = [t for t in ages if t > 40]
      
          # average of the 12 ages
          a_ave = a_min + 11 * delta / 2
          # lowest possible average age of a jury
          min_ave = a_ave + (40 - max(ages)) / 12
          # highest possible average age of a jury
          max_ave = a_ave + (max(ages) - min(ages)) / 12
          poss_aves = [p for p in primes
                       if p >= min_ave and p < max_ave]
      
          # possible prime average ages for a jury,
          # ...must be (at least) 2 of them
          if len(poss_aves) < 2:
            continue
      
          # find twins' age by trial and error
          for twin in twins:
            rejects = []
            for poss_ave in poss_aves:
              # age of swapped juror relative to twins
              swapped = twin + (a_ave - poss_ave) * 12
              if swapped > a_min + 11 * delta:
                break
              # reject impossible juror ages
              if swapped >= 20:
                rejects.append(swapped)
      
            # solution contains at least two swapped juror ages
            if len(rejects) > 1:
              # happily there is one solution
              #.. and it has exactly two swapped jurors
              print("The ages of the twins was", twin)
      
      
      
      

      Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 12:04 pm on 26 June 2021 Permalink | Reply

      Thanks all for your comments. I apologise for my continued struggles making my code readable in these replies. I’ll have a look at the Wingware IDE.

      Like

    • Hugh Casement's avatar

      Hugh Casement 11:34 am on 27 June 2021 Permalink | Reply

      With the same sequence the twins could have been aged 32, 35, or 38 (had those been allowed), still with the same two average ages. I found no other sequences that work, assuming no square ages .

      Like

    • Frits's avatar

      Frits 4:56 pm on 14 August 2025 Permalink | Reply

      Fast under CPython.

      The program has gotten a bit complex as I tried to also allow the program run for different numbers (m, M, N and minT).

      from math import ceil
      from enigma import primes
      
      # min/max, number of jury members, minimum twin age
      m, M, N, minT = 20, 65, 12, 41
      tri = (N * (N - 1)) // 2
      
      # progression of N ages: total sum(i=0..N-1) (y + i.k) = (N - 1).y + tri.k
      # age p was replaced by q with p > q
      # N.y + tri.k - q + t = N.p1
      # N.y + tri.k - p + t = N.p2   so p - q = N.(p1 - p2) is even
      
      sqs = {i * i for i in range(ceil(m**.5), int(M**.5) + 1)}
      primes = set(primes.irange(m, M))
      
      # possible differences between primes p1 and p2
      for d in range(2, (M - m) // N, 2):
        # valid primes for lower prime (p2) so that p1 is also prime
        primes2 = {p2 for p2 in primes if (p2 + d) in primes}
        # determine possible common differences (m + (N - 1).k <= M)
        ks = [k for k in range(1, (M - m) // (N - 1) + 1) if (N - 1) * k >= d * N]
        # determine (y, k) combinations that avoid a square age
        yks = [(y, k, N * y + tri * k) for k in ks
               for y in range(m, M - (N - 1) * k + 1) 
               if all((y + k * i) not in sqs for i in range(N))]
        for y, k, tot in yks:
          ages = range(y, y + N * k, k)
          # ages for the replaced jury member
          p_ages = [a for a in ages if a >= m + d * N]
          # reduce possible twin ages
          twin_ages = [t for t in ages if t >= minT for p in p_ages 
                       if t not in {p, p + d} and
                       not (dm := divmod(tot - p + t, N))[1] and dm[0] in primes2]
          
          if twin_ages: 
            print("answer:", ' or '.join(str(t) for t in twin_ages))
      

      Like

  • Unknown's avatar

    Jim Randell 9:22 am on 17 June 2021 Permalink | Reply
    Tags:   

    Teaser 2795: No mean feat 

    From The Sunday Times, 17th April 2016 [link] [link]

    I bought a run of consecutively-numbered raffle tickets, some of which were red and the rest were blue. Amongst my numbers no red number was the average of two other reds, and no blue number was the average of two other blues. I actually won the raffle. My two-figure winning red number was in fact the geometric mean of two other red numbers of mine. [The geometric mean of M and N is the square root of (M×N)].

    What were my blue numbers?

    [teaser2795]

     
    • Jim Randell's avatar

      Jim Randell 9:23 am on 17 June 2021 Permalink | Reply

      The winning ticket w is the geometric mean of two of the other tickets, say a and b:

      w = √(a⋅b)
      w² = a⋅b

      So, the range of tickets must include the interval [a, …, b]. And if this interval can be successfully coloured (with a, w, b red) then we have a solution.

      This Python program runs in 530ms.

      Run: [ @replit ]

      from enigma import (irange, divisors_pairs, empty, peek, filter2, mod, subsets, diff, printf)
      
      # check no number in <ns> is the mean of two other numbers in <ns>
      def check(ns):
        for ps in filter2(mod(2), ns):
          if any((x + y) // 2 in ns for (x, y) in subsets(ps, 2)): return False
        return True
      
      # colour tickets in <ts> to give reds <rs> and blues <bs>
      def colour(ts, rs, bs):
        if not ts:
          yield (rs, bs)
        else:
          (t, ts_) = (ts[0], ts[1:])
          # try red
          rs_ = rs.union([t])
          if check(rs_): yield from colour(ts_, rs_, bs)
          # try blue
          bs_ = bs.union([t])
          if check(bs_): yield from colour(ts_, rs, bs_)
      
      # consider possible 2-digit winning tickets
      for w in irange(10, 99):
        # consider possible other ticket whose geometric mean is w
        for (a, b) in divisors_pairs(w, 2):
          if not (a < b): break
      
          # can we colour the tickets?
          rs = {a, w, b}
          if not check(rs): continue
          ts = diff(irange(a, b), rs)
          try:
            (reds, blues) = peek(colour(ts, rs, empty))
          except ValueError:
            continue
      
          printf("red = {reds}, blue = {blues} [w={w}; a={a} b={b}]", reds=sorted(reds), blues=sorted(blues))
      

      Solution: The blue tickets were numbered: 10, 11, 14, 15.

      And the red tickets were: 9, 12, 13, 16.

      i.e. the sequence is: (R9, B10, B11, R12, R13, B14, B15, R16).

      The winning ticket is 12, which is the geometric mean of 9 and 16.

      And we can check that the sequence cannot be extended (with 8 or 17):

      >>> check([8, 9, 12, 13, 16])
      False
      >>> check([8, 10, 11, 14, 15])
      False
      >>> check([9, 12, 13, 16, 17])
      False
      >>> check([10, 11, 14, 15, 17])
      False
      

      For a faster execution time we can note that if we have a sequence of numbers such that no number is the mean of any other two.

      a[0], a[1], …, a[n]

      Then the sequence:

      a[0] + k, a[1] + k, …, a[n] + k

      also has this property.

      So if we find a pattern of red/blue colourings of consecutive numbers, we can also form another valid pattern by adding a constant to each of the numbers (or by swapping the colours).

      The following program finds all possible patterns (of length 4 or more):

      from enigma import printf
      
      # generate colourings, with at least k elements
      def patterns(s, n, choice=(0, 1)):
        j = len(s)
        if not (j < n): yield s
        for x in choice:
          for (i, y) in enumerate(s):
            if y == x:
              (d, r) = divmod(i + j, 2)
              if r == 0 and s[d] == x: break
          else:
            yield from patterns(s + [x], n, choice)
      
      # output patterns
      for ps in patterns([0], 4):
        printf("{ps}")
      

      And we could modify the original program to check that the sequence of tickets matches one of these patterns, although we can cut the run time of the program down to 51ms by simply rejecting sequences that are longer than the maximum pattern length (which is 8).

      Replacing line 26 with the following does the trick:

      if not (0 < b - a < 8): continue
      

      There are 3 viable patterns of length 8, which we can identify with the following sequences:

      (0, 0, 1, 1, 0, 0, 1, 1)
      (0, 1, 0, 1, 1, 0, 1, 0)
      (0, 1, 1, 0, 0, 1, 1, 0)

      Giving a total of 6 possible red/blue colourings.

      The solution sequence (R9, B10, B11, R12, R13, B14, B15, R16) matches the last of these patterns.

      Like

      • Jim Randell's avatar

        Jim Randell 9:26 am on 17 June 2021 Permalink | Reply

        The following approach is based on an observation by Brian Gladman [link].

        Suppose the winning number (at index w in the sequence), is between the numbers at indices a and b that are the components of the geometric mean.

        Then the sequence is:

        n, …, n + a, …, n + w, …, n + b, …
        (n + a)(n + b) = (n + w)²
        n = (w² − ab) / (a + b − 2w)

        So, given the values of the indices within the sequence a, w, b we can calculate the starting value of the sequence.

        The following Python program generates possible patterns, and then looks for indices that produce a viable value for n. The values of the blue tickets can then be determined.

        It runs in 48ms (but the internal runtime is only 243µs compared to 1.26ms for my previous program (with modification)).

        from enigma import (irange, subsets, div, group, printf)
        
        # generate colourings, with at least k elements
        def patterns(s, n, choice=(0, 1)):
          j = len(s)
          if not (j < n): yield s
          for x in choice:
            for (i, y) in enumerate(s):
              if y == x:
                (d, r) = divmod(i + j, 2)
                if r == 0 and s[d] == x: break
            else:
              yield from patterns(s + [x], n, choice)
        
        # collect patterns by length
        pats = group(patterns([0], 4), by=len)
        m = max(pats.keys())
        
        # consider values for a, w, b
        for (a, w, b) in subsets(irange(0, m - 1), size=3):
          # calculate n
          n = div(w * w - a * b, a + b - 2 * w)
          if n is None or n + w < 10 or n + w > 99: continue
          # look for patterns with length at least b + 1
          for (k, vs) in pats.items():
            if not (k > b): continue
            for ps in vs:
              # look for patterns where a, w, b are red
              red = ps[a]
              if not (red == ps[w] == ps[b]): continue
              # output solution
              blues = sorted(n + j for (j, x) in enumerate(ps) if x != red)
              printf("blues = {blues} [ps={ps} n={n} red={red} w={w}]")
        

        It turns out there is only one possible value for (a, w, b) = (0, 3, 7), which gives n = 9.

        So the sequence is maximal, and of the three candidates only (0, 1, 1, 0, 0, 1, 1, 0) has elements 0, 3, 7 the same. They are all 0, so the 1’s indicate the blue tickets.

        Like

  • Unknown's avatar

    Jim Randell 7:16 am on 15 June 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 776: Christmas shopping 

    From The Sunday Times, 30th May 1976 [link]

    My three nieces Anne, Betty and Carole came to stay with me just before Christmas. When they arrived they handed over their “present” money, and as I wrote down the amounts (in pence) I noticed that they were 3-digit numbers  using all nine digits (zero excluded) and that Anne had more than Betty and Carole had as much as the other two together.

    I drove the girls into town to shop and as they entered the car to return home they again asked me to look after their money. As I jotted down the three  amounts I noticed that they were 3-digit numbers using all nine digits as before.

    Before I drove off they wanted to know how much each had spent. As I told them these amounts I was struck by the fact that they were 3-digit numbers again using all nine digits. I also added that Carole had spent exactly three-fifths of her money while Anne had spent a little more than three-fifths of hers.

    How much did the three girls have for the rest of their stay?

    This puzzle was included in the book The Sunday Times Book of Brain-Teasers: Book 1 (1980, edited by Victor Bryant and Ronald Postill). The puzzle text above is taken from the book.

    [teaser776]

     
    • Jim Randell's avatar

      Jim Randell 7:19 am on 15 June 2021 Permalink | Reply

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

      The following run file executes in 85ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --digits="1-9"
      --distinct="ABCDEFGHI,JKLMNPQRS,TUVWXYZab"
      
      # initial amounts: Anne = ABC; Betty = DEF; Carole = GHI
      "{A} > {D}"  # ABC > DEF
      "{GHI} - {ABC} = {DEF}"  # GHI = ABC + DEF
      
      # final amounts: Anne = JKL; Betty = MNP; Carole = QRS
      # amounts spent: Anne = TUV; Betty = WXY; Carole = Zab
      "{ABC} - {TUV} = {JKL}"
      "{DEF} - {WXY} = {MNP}"
      "{GHI} - {Zab} = {QRS}"
      
      # Carole had spent 3/5 of her money
      "div(3 * {GHI}, 5) = {Zab}"
      
      # Anne had spent a little more than 3/5 of hers (say: > 60%, < 65%)
      "0.60 < fdiv({TUV}, {ABC}) < 0.65"
      
      # the answer is the final amounts
      --answer="({JKL}, {MNP}, {QRS})"
      

      Solution: The remaining amounts are: Anne = 245p; Betty = 169p; Carole = 378p.

      The initial amounts are:

      Anne: 627p
      Betty: 318p
      Carole: 945p

      And the amounts spent are:

      Anne: 382p (60.9%)
      Betty: 149p (46.9%)
      Carole: 567p (60.0%)

      Like

    • GeoffR's avatar

      GeoffR 10:27 am on 15 June 2021 Permalink | Reply

      
      % A Solution in MiniZinc 
      include "globals.mzn";
      
      % using same notations as Jim's solution 
      % starting amounts - Anne, Betty and Carole
      var 1..9:A; var 1..9:B; var 1..9:C;  
      var 1..9:D; var 1..9:E; var 1..9:F;
      var 1..9:G; var 1..9:H; var 1..9:I;
      var 100..999:ABC = 100*A + 10*B + C; % Anne
      var 100..999:DEF = 100*D + 10*E + F; % Betty
      var 100..999:GHI = 100*G + 10*H + I; % Carole
      
      var set of int: start = {A,B,C,D,E,F,G,H,I};
      constraint start == {1,2,3,4,5,6,7,8,9};
      
      % Anne had more than Betty and 
      % Carole had as much as the other two together.
      constraint ABC > DEF /\ GHI == ABC + DEF;
      
      % final amounts - Anne, Betty and Carole
      var 1..9:J; var 1..9:K; var 1..9:L;
      var 1..9:M; var 1..9:N; var 1..9:P;
      var 1..9:Q; var 1..9:R; var 1..9:S;
      var 100..999:JKL = 100*J + 10*K + L; % Anne
      var 100..999:MNP = 100*M + 10*N + P; % Betty
      var 100..999:QRS = 100*Q + 10*R + S; % Carole
      
      var set of int: final = {J,K,L,M,N,P,Q,R,S};
      constraint final == {1,2,3,4,5,6,7,8,9};
      
      % amounts spent - Anne, Betty and Carole
      var 1..9:T; var 1..9:U; var 1..9:V;
      var 1..9:W; var 1..9:X; var 1..9:Y;
      var 1..9:Z; var 1..9:a; var 1..9:b;
      var 100..999:TUV = 100*T + 10*U + V; % Anne
      var 100..999:WXY = 100*W + 10*X + Y; % Betty
      var 100..999:Zab = 100*Z + 10*a + b; % Carole
      
      var set of int: spent ={T,U,V,W,X,Y,Z,a,b};
      constraint spent == {1,2,3,4,5,6,7,8,9};
      
      % for Anne, Betty and Carole, amount_left = starting_amount - amount_spent
      constraint ABC - TUV == JKL
      /\ DEF - WXY == MNP /\ GHI - Zab == QRS;
      
      % Carole had spent 3/5 of her money
      constraint 3 * GHI == 5 * Zab;
      
      % Anne had spent a little more than 3/5 of her money (say less than 4/5)
      constraint 5 * TUV > 3 * ABC /\ 5 * TUV < 4 * ABC;
      
      solve satisfy;
      
      output ["Starting amounts(p) for Anne, Betty & Carole: " 
      ++ show(ABC) ++ ", " ++ show (DEF) ++ ", " ++ show(GHI)
      ++ "\nFinal amounts(p) for Anne, Betty & Carole: " 
      ++ show(JKL) ++ ", " ++ show(MNP) ++ ", " ++ show(QRS)
      ++ "\nAmounts spent(p) for Anne, Betty & Carole: " 
      ++ show(TUV) ++ ", " ++ show(WXY) ++ ", " ++ show(Zab)];
      
      % Starting amounts(p) for Anne, Betty & Carole: 627, 318, 945
      % Final amounts(p) for Anne, Betty & Carole: 245, 169, 378
      % Amounts spent(p) for Anne, Betty & Carole: 382, 149, 567
      % time elapsed: 0.38 s
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:31 pm on 13 June 2021 Permalink | Reply
    Tags: ,   

    Brain-Teaser 35: After-lunch golf 

    From The Sunday Times, 19th November 1961 [link]

    Gee and Jay play — for £1 the match, 5s. a hole, and 1s. a stroke — a golf match of nine holes which is decided on the ninth green. Neither takes less than four or more than nine strokes at any hole and the
    results vary for every hole.

    Gee wins five holes and the match, and takes 22s. off his opponent, but had the result at the last hole been reversed he would have lost 10s.

    At the eighth hole all the strokes travel an exact number of yards straight towards the pin, but whilst each of Gee’s travels one half the distance of his previous stroke, each of Jay’s goes one quarter the distance of its predecessor.

    (i) In how many strokes did Gee complete the round?
    (ii) What is the length of the eighth hole?

    [teaser35]

     
    • Jim Randell's avatar

      Jim Randell 4:31 pm on 13 June 2021 Permalink | Reply

      I struggled to find a unique solution for this puzzle, and had to make several assumptions along the way. Maybe I just don’t know enough about golf.

      Let’s start by looking at the 8th hole:

      If G makes n strokes, and the distance of the final one is x, then the total distance is:

      x + 2x + 4x + … + (2^n)x = (2^n − 1)x

      And if J makes m strokes, and the distance of the final one is y, then the total distance is:

      y + 4y + 16y + … + (4^m)y = (4^m − 1)y / 3

      So the overall distance of the hole must be some multiple of: lcm(2^n − 1, (4^m − 1) / 3)

      The following values work (for holes with a distance of less than 1000 yards):

      n=4 m=4: dist = 255k; Gs=(136, 68, 34, 17)k, Js=(192, 48, 12, 3)k
      n=5 m=5; dist = 341k; Gs=(176, 88, 44, 22, 11)k, Js=(256, 64, 16, 4, 1)k
      n=8 m=4; dist = 255k; Gs=(128, 64, 32, 16, 8, 4, 2, 1)k, Js=(192, 48, 12, 3)k

      If we limit the length of a hole we can reduce the possibilities, but even the smallest possible distance (255 yards) has 2 options for the score.

      As far as the actual match goes I’m assuming the winner of the match is the player who won the greatest number of individual holes, and the loser pays the winner £1 (= 20s), and for each individual hole the loser of each hole pays the winner 5s, plus 1s per stroke for the difference in strokes.

      I further assumed that “the results vary for each hole” mean that no score for any hole is repeated.

      We know that G wins 5 holes (including the final hole), and hole 8 is either drawn, or G loses it by 4 strokes.

      If the 8th hole is drawn there are 2 possibilities (scores from G’s perspective):

      8th hole = 4-4; Gs wins = 4-5, 5-6, 6-7, 7-8, 8-9; other holes = 8-4, 9-5, 9-4
      8th hole = 5-5; Gs wins = 4-5, 5-6, 6-7, 7-8, 8-9; other holes = 8-4, 9-5, 9-4

      The first gives G a total of 60 strokes, and J a total of 52.

      G wins 20 + (5 − 3)×5 + (52 − 60) = 22 shillings.

      And if the result of one of G’s wins (on the last hole) was reversed, there would be no winner on holes (each won 4, and 1 was drawn), so the match is drawn, and only money for the number of strokes changes hands. G has a total of 61 strokes, and J a total of 51 strokes, so G loses 10s. (Although I think J could argue that in this case he won the match).

      The second gives G a total of 61 strokes, and J a total of 53.

      Again, if the result of one of G’s wins is reversed, G’s total goes down by 1 stroke and J’s goes up by 1, so G loses 10s.

      So, if we limit the maximum possible distance of a hole to 340 yards, then only the first of these options remains, and gives the solution:

      Solution: (i) Gee completed the round in 60 strokes. (ii) The 8th hole was 255 yards long.

      Which is the published solution.

      Like

      • Jim Randell's avatar

        Jim Randell 10:35 am on 15 June 2021 Permalink | Reply

        Here is a Python program that solves the puzzles according to the assumptions described in my previous comment.

        It runs in 86ms.

        Run: [ @replit ]

        from enigma import irange, subsets, update, lcm, group, unpack, multiset, join, printf
        
        # possible strokes per hole
        strokes = irange(4, 9)
        
        # consider the number of strokes on the 8th hole (G=x, J=y)
        def hole8():
          for (x, y) in subsets(strokes, size=2, select="M"):
            # calculate minimal distance
            tx = sum(pow(2, n) for n in irange(0, x - 1))
            ty = sum(pow(4, n) for n in irange(0, y - 1))
            d = lcm(tx, ty)
            if d < 1000:
              printf("[hole8: G={x} J={y}; dist = {d}k; Gs={Gs}k Js={Js}k]",
                Gs=tuple(pow(2, n) * d // tx for n in irange(0, x - 1))[::-1],
                Js=tuple(pow(4, n) * d // ty for n in irange(0, y - 1))[::-1],
              )
              yield (x, y)
        # collect hole 8 scores by score
        score = unpack(lambda x, y: y - x)
        h8s = group(hole8(), by=score)
        
        # collect possible wins/losses
        scores = group(subsets(strokes, size=2, select="M"), by=score)
        
        # calculate the gain for a sequence of holes
        def gain(hs):
          # total gain (shillings), difference (in holes)
          T = d = 0
          for x in hs:
            if x < 0:
              # G wins the hole
              T += 5 - x
              d += 1
            elif x > 0:
              # G loses the hole
              T -= 5 + x
              d -= 1
          if d > 0:
            # G wins the match (on holes, not strokes)
            T += 20
          elif d < 0:
            # G loses the match
            T -= 20
          return T
        
        # find scores with stroke differences in ds
        def complete(ds, ss):
          if not ds:
            if len(set(ss)) == len(ss):
              yield ss
          else:
            (k, v) = ds[0]
            for xs in subsets(scores[k], size=v, fn=list):
              yield from complete(ds[1:], ss + xs)
        
        # output holes and total strokes
        def output(hs, ss):
          holes = list()
          G = J = 0
          for (h, (x, y)) in zip(hs, ss):
            if h > 0: (x, y) = (y, x)
            holes.append((x, y))
            G += x
            J += y
          printf("{holes} -> G={G} J={J}", holes=join((f"{x}-{y}" for (x, y) in holes), sep=", ", enc="()"))
        
        # G wins 5 holes (including the final one)
        for Gw in subsets([-1, -2, -3, -4, -5], size=5, select="R"):
          # G does not win any of the other 4 holes
          for Gl in subsets([0, 1, 2, 3, 4, 5], size=4, select="R"):
            # calculate G's winnings
            hs = list(Gw + Gl)
            # hole 8 is 0 or -4
            if not any(k in hs for k in h8s.keys()): continue
            w = gain(hs)
            if w != 22: continue
        
            # look for a 9th hole, which if swapped would give -10s
            for x in set(Gw):
              hs_ = update(hs, [hs.index(x)], [-x])
              w_ = gain(hs_)
              if w_ != -10: continue
        
              # count the scores, and reject any collection that would require a duplicate score
              s = multiset.from_seq((abs(x) for x in hs))
              if any(s.count(k) > len(scores[k]) for k in s.keys()): continue
        
              # choose a hole 8
              for (i, h8) in enumerate(hs):
                for s8 in h8s.get(h8, []):
                  # count the remaining scores
                  for ss in complete(list(s.difference([abs(h8)]).items()), [s8]):
                    # output solution (hole 8 first)
                    output([h8] + hs[:i] + hs[i + 1:], ss)
        

        The distance for the 8th hole is limited to below 1000 yards, so the program produces two possible answers.

        In the output the holes are not listed in play order, rather hole 8 comes first, then the remaining holes, starting with those that G won.

        Like

      • Jim Randell's avatar

        Jim Randell 5:49 pm on 19 June 2021 Permalink | Reply

        With Teaser 36 the following was published:

        In Brain-Teaser No. 35 (Sunday, November 19) owing to the omission of the result of the eighth hole (halved in four) several alternative solutions became possible. No reader who submitted any of these was excluded from the prize.

        Which I think means we are take it that the result of the eighth hole was a draw, each player taking 4 strokes.

        With this additional information we can reduce the number of possible solutions (although we still need to limit the distance of the eighth hole, or the individual strokes, to deduce a unique distance).

        Like

    • Jim Olson's avatar

      Jim Olson 9:14 pm on 13 June 2021 Permalink | Reply

      I’m not following the total score for each. If Gee wins the match then he has the lowest total strokes. In the analysis presented Jay should have won the match. It is not determined by the number of holes won. I understand the assumption you made but that is not golf rules.

      Like

      • Jim Randell's avatar

        Jim Randell 11:02 pm on 13 June 2021 Permalink | Reply

        I tried various things to try and get a unique solution, and this was the only way I found. Looking on Wikipedia [link] it seemed to be allowed (“match play” vs “stroke play” – I did start off using the latter, but didn’t get anywhere).

        I don’t think a modern Teaser puzzle would be published that didn’t have at least a brief description of the scoring system that is to be used. But this one is from 60 years ago.

        Like

    • Jim Olson's avatar

      Jim Olson 2:18 am on 14 June 2021 Permalink | Reply

      I agree your interpretation is what the setter had in mind. However, after many years of playing golf In tournaments and at golf clubs the tournament or match was won by the golfer with the lowest number of total strokes. The setter should have made it clear how the scoring for the match winner was to be computed. It would have saved quite a bit of time.

      Like

  • Unknown's avatar

    Jim Randell 4:36 pm on 11 June 2021 Permalink | Reply
    Tags:   

    Teaser 3064: Turnip prize 

    From The Sunday Times, 13th June 2021 [link] [link]

    The Turnip Prize is awarded to the best piece of work by an artist under fifty. This year’s winning entry consisted of a mobile made up of many different plain white rectangular or square tiles hanging from the ceiling. The sides of the tiles were all whole numbers of centimetres up to and including the artist’s age, and there was precisely one tile of each such possible size (where, for example, a 3-by-2 rectangle would be the same as a 2-by-3 rectangle). Last week one of the tiles fell and smashed and then yesterday another tile fell and smashed. However, the average area of the hanging tiles remained the same throughout.

    How old is the artist?

    There are now 500 Teaser puzzles available on the site (approximately 10 years worth).

    And I still have 52 puzzles from 2016-2017 that I solved at the time to post, which at the current rate will take me another year.

    [teaser3064]

     
    • Jim Randell's avatar

      Jim Randell 4:50 pm on 11 June 2021 Permalink | Reply

      The following Python program runs in 52ms.

      Run: [ @replit ]

      from enigma import irange, tri, div, printf
      
      # collect rectangles with sides up to x, and total area
      (rs, t) = (list(), 0)
      for x in irange(1, 49):
        rs.extend((a, x) for a in irange(1, x))
        t += x * tri(x)
      
        # calculate the number of tiles and the total area
        n = len(rs)
        # look for an integer mean
        m = div(t, n)
        if m is None: continue
      
        # look for tiles with the mean area
        ts = list((a, b) for (a, b) in rs if a * b == m)
        if len(ts) > 1:
          printf("x={x} [m={m}; ts={ts}]")
      

      or (shorter, but doesn’t track tile dimensions):

      from enigma import multiset, irange, div, printf
      
      # collect rectangles with sides up to x
      rs = multiset()
      for x in irange(1, 49):
        rs.update(a * x for a in irange(1, x))
        # calculate integer mean tile area
        m = div(rs.sum(), rs.size())
        # look for tiles with the mean area
        if rs.count(m) > 1:
          printf("x={x} [m={m}]")
      

      Solution: The artist is 37.

      So, there were originally 703 tiles with a total area of 255892 cm², given a mean area of 364 cm².

      There are 2 tiles with this area (= 14×26, 13×28), and the removal of either (or both) will leave the mean area unchanged.

      Like

      • Jim Randell's avatar

        Jim Randell 8:47 am on 12 June 2021 Permalink | Reply

        And here’s a version that doesn’t collect the rectangles at all (and does report tile dimensions):

        The number of tiles is given by the triangular numbers:

        T(n) = n (n + 1) / 2

        And the total area is given by the 4-dimensional pyramidal numbers (see: OEIS A001296):

        A(n) = n (n + 1) (n + 2) (3n + 1) / 24

        And hence the mean area of a tile is:

        M(n) = A(n) / T(n) = (n + 2) (3n + 1) / 12

        So we can look for an integer mean, that has (at least) 2 divisor pairs in the required range:

        from enigma import irange, div, divisors_pairs, printf
        
        # consider ages
        for x in irange(1, 49):
          # calculate integer mean
          m = div((x + 2) * (3 * x + 1), 12)
          if m is None: continue
          # find tiles with mean area
          ts = list((a, b) for (a, b) in divisors_pairs(m) if b <= x)
          if len(ts) > 1:
            printf("x={x} [m={m}; ts={ts}]")
        

        Like

        • Frits's avatar

          Frits 10:05 am on 12 June 2021 Permalink | Reply

          Nice.

          Proof:

          see [ https://www.wolframalpha.com/input/?i=%28sum+x*x*%28x%2B1%29%2F2%2C+x%3D1+to+n%29+ ]

          total area = sum(y=1..x) 1/2 y y (y + 1) = 1/24 x (x + 1) (x + 2) (3x + 1)
          number of tiles = x (x+1) / 2
          integer mean = (x + 2) * (3 * x + 1) / 12

          Like

        • Tony Brooke-Taylor's avatar

          Tony Brooke-Taylor 8:07 am on 14 June 2021 Permalink | Reply

          To get an integer mean, x must satisfy the requirement that (x+2).(3x+1) mod 12 = 0. This requires that x be equal to either 1+12i or 10+12i (i=0,1,…).

          This plot gives a visual representation of that requirement.

          
          import numpy as np
          import matplotlib.pyplot as plt
          
          x = np.asarray([a for a in range(1,51)])
          y = np.asarray([(a+2)*(3*a+1)%12 for a in range(1,51)])
          
          plt.scatter(x, y)
          plt.title('y=(x + 2).(3x + 1) mod 12')
          plt.xlabel('x')
          plt.ylabel('y')
          plt.show()
          
          

          Like

  • Unknown's avatar

    Jim Randell 8:00 am on 10 June 2021 Permalink | Reply
    Tags:   

    Teaser 2794: D-day 

    From The Sunday Times, 10th April 2016 [link] [link]

    I have a particular digit in mind and I shall call it D. I have written down a number consisting of D digits in which the penultimate digit is D itself. If I move that penultimate digit to the left so that it becomes the first digit, then I get a new D-digit number that turns out to be D times the number I started with.

    What is the D-digit number I started with?

    [teaser2794]

     
    • Jim Randell's avatar

      Jim Randell 8:00 am on 10 June 2021 Permalink | Reply

      Using : for concatenation, we have:

      D:abc:z = D × abc:D:z
      ⇒ 10^(D − 1) D + 10 abc + z = D (100 abc + 10 D + z)
      ⇒ abc = (10 D (10^(D − 2) − D) − (D − 1) z) / (100 D − 10)

      where abc represents a (D − 2) digit number).

      This Python program runs in 50ms.

      Run: [ @replit ]

      from enigma import irange, div, ndigits, printf
      
      # choose D
      for D in irange(3, 9):
        x = 10 * D * (10 ** (D - 2) - D)
        y = 100 * D - 10
        # possible final digits
        for z in irange(0, 9):
          # note: D * z = z [mod 10]
          r = (D - 1) * z
          if not(r % 10 == 0): continue
          # calculate abc...
          abc = div(x - r, y)
          if abc is None or ndigits(abc) != D - 2: continue
          # output solution
          printf("number={abc}{D}{z} [D={D}]")
      

      Solution: The number you started with is 101123595.

      So, D = 9, and: 9 × 101123595 = 910112355.

      Like

      • Frits's avatar

        Frits 12:15 pm on 10 June 2021 Permalink | Reply

        @Jim, you also could have used (regarding the r % 10 == 0) :

        for z in [0, 2, 4, 5, 6, 8]:
        

        Like

      • Jim Randell's avatar

        Jim Randell 2:57 pm on 10 June 2021 Permalink | Reply

        If we construct numbers by taking successive digits of the larger number (D:abc:z), and divide these by D, then we get the corresponding digits of the number we started with, which can then be used in the next division to determine the following digit:

        D / D = a (so: a = 1)
        Da / D = ab (so: b = 0)
        Dab / D = abc

        So, for a given value of D, we can compute successive digits of abc. And after the (D − 2) digits are calculated, z is chosen to complete the division, and then we check that it is a viable digit:

        from enigma import irange, div, printf
        
        # choose D
        for D in irange(3, 9):
          n = D
          x = 0
          # perform (D - 2) divisions
          for _ in irange(1, D - 2):
            d = (n // D) % 10
            n = 10 * n + d
            x = 10 * x + d
        
          # the final digit is z
          n = 10 * n
          x = 100 * x + 10 * D
          z = div(n - x * D, D - 1)
          if z is None or z < 0 or z > 10: continue
        
          # output solution
          n += z
          x += z
          printf("[D={D}] {x} * {D} = {n}")
        

        This appears to be slightly faster than my original code (internal runtime is reduced from 60μs to 42μs).

        Like

    • Frits's avatar

      Frits 11:57 am on 10 June 2021 Permalink | Reply

      We can also continue the analysis, so C must be 1, etc …

      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [# consider number ABCDEFGHI
         
         # (9 * H + carry digit) modulo 10 must be equal to G
         "(81 + 0.8 * I) % 10 = G",
      
         # we have established H must be 9   
         "HABCDEFGI == 9 * ABCDEFGHI",
         ],
      
        answer="ABCDEFGHI",
        verbose=0,
        # (9 * I) % 10 = I so I is 0 or 5
        # Consider 9 * FGHI = HFGI (F > 0), this can only happen when H is 9
        # so H is 9, A must be 1 and B must be 0
        # G is 1 or 5 (see G formula)
        d2i=dict([(0, "AH")] + [(1, "BHI")] + [(2, "ABHI")] + [(5, "ABH")] + 
                 [(k, "ABHI") for k in [3, 4, 6, 7, 8, 9]] +
                 [(9, "ABI")]),
        distinct="",   # allow variables with same values
        reorder=0,
      )
      
      # Print answer
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

    • GeoffR's avatar

      GeoffR 1:53 pm on 10 June 2021 Permalink | Reply

      A simple solution in MiniZinc, using Frits analysis i.e. D-digit number = 9

      
      % A Solution in MiniZinc
      
      var 0..9: A; var 0..9: B; var 0..9: C; var 0..9: D; var 0..9: E;
      var 0..9: F; var 0..9: G; var 0..9: H; var 0..9: I;
      
      constraint H > 0 /\ A > 0;
      
      var 100000000..999999999: ABCDEFGHI = I + 10*H + 100*G + 1000*F + 10000*E
      + 100000*D + 1000000*C + 10000000*B + 100000000*A;
      
      var 100000000..999999999: HABCDEFGI = I + 10*G + 100*F + 1000*E + 10000*D
      + 100000*C + 1000000*B + 10000000*A + 100000000*H;
      
      constraint HABCDEFGI == 9 * ABCDEFGHI;
      
      solve satisfy;
      
      output["The number you started with was " ++ show(ABCDEFGHI) ];
      
      % The number you started with was 101123595
      % time elapsed: 0.20 s
      % ----------
      % ==========
      
      

      Like

      • Frits's avatar

        Frits 7:20 pm on 10 June 2021 Permalink | Reply

        Unfortunately my analysis was incorrect (I mixed things up).
        Following program works well with Geocode but gives no answer with the Chuffed solver.

        Using “var 0..9: A;” gives an out of range error.

        % A Solution in MiniZinc
         
        var 0..1: A; var 0..9: B; var 0..9: C; var 0..9: D; var 0..9: E;
        var 0..9: F; var 0..9: G; var 0..9: H; var 0..9: I;
        
         % moving the penultimate digit to the left so that it becomes
         % the first digit
        constraint H > 2;
         
        var 100000000..999999999: ABCDEFGHI = I + 10*H + 100*G + 1000*F
        + 10000*E + 100000*D + 1000000*C + 10000000*B + 100000000*A;
         
        var 10000000..99999999: ABCDEFGI = I + 10*G + 100*F + 1000*E
        + 10000*D + 100000*C + 1000000*B + 10000000*A;
         
        constraint H * pow(10, H - 1) + ABCDEFGI == H * ABCDEFGHI;
         
        solve satisfy;
         
        output["The number you started with was " ++ show(ABCDEFGHI)];
        

        Like

  • Unknown's avatar

    Jim Randell 10:30 am on 8 June 2021 Permalink | Reply
    Tags: by: R A England   

    Brain-Teaser 774: Postage stamps 

    From The Sunday Times, 16th May 1976 [link]

    I went to the Post Office to buy three 5½p stamps (those were the days!) for my entries for the Brain-teaser, Crossword and Mephisto; but finding a long queue there I bought a 10p book of stamps from a machine, which contained two stamps at each of the following denominations: 2p, 1½p, 1p, ½p. Since I already had four stamps of total value 6½p left over from a similar 10p book I now had just enough stamps for the three entries.

    I stuck four of the stamps totalling 5½p on my Brain-teaser entry, but then found that there was room for only three stamps on the Crossword entry (because entrants have to write “Crossword” on the top left-hand corner of the  envelope) and the stamps I had left could not be arranged to give me three stamps totalling 5½p for the Crossword entry and five for the Mephisto entry.

    What were the denominations of stamps on the Brain-teaser entry?

    Current stamp prices in the UK are 85p (1st class) or 66p (2nd class).

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

    [teaser774]

     
    • Jim Randell's avatar

      Jim Randell 10:31 am on 8 June 2021 Permalink | Reply

      The following Python program runs in 51ms.

      Run: [ @replit ]

      # working in half-pence units the denominations avaliable are:
      #  1 (= .5p), 2 (= 1p), 3 (= 1.5p), 4 (= 2p)
      
      from enigma import (multiset, subsets, join, sprintf, printf)
      
      # one book of stamps (2 of each denomination)
      book = multiset.from_seq((1, 2, 3, 4), count=2)
      
      # make an amount <t> using <k> stamps from <ss>
      make = lambda ss, t, k: (s for s in subsets(ss, size=k, select="mC") if sum(s) == t)
      
      # there are 4 stamps making 13 (= 6.5p) left over from a book
      for rs in make(book, 13, 4):
        # combine these with a new book
        ss = book.combine(rs)
      
        # choose 4 stamps making 11 (= 5.5p) for the Teaser entry
        for ts in make(ss, 11, 4):
          # but you can't make 11 (= 5.5p) using 3 stamps from what is left over
          if any(make(ss.difference(ts), 11, 3)): continue
      
          # output solution
          fmt = lambda xs: join((sprintf("{x:g}p", x=0.5 * x) for x in xs), sep=", ")
          printf("Teaser = {ts}", ts=fmt(ts))
      

      Solution: The stamps used for the Brain-teaser entry were: 1× 1p stamp; 3× 1½p stamps.

      The 8 remaining stamps are: 2× ½p; 2× 1p; 4× 2p.

      So the correct postage can be made using 4 stamps: 2p + 2p + 1p + ½p.

      Like

    • Frits's avatar

      Frits 12:51 pm on 11 June 2021 Permalink | Reply

      Using the same approach as Jim.

      2 technical issues:

      the decompose function generates duplicates
      is there an easier way to determine “remaining” (like a list comprehension)?

      # working in half-pence units 
      
      # decompose <t> into <k> increasing numbers from <ns>, count number <= c
      # so that sum(<k> numbers) equals <t>
      def decompose(t, k, ns, s=[], m=1, c=99):
        if k == 1:
          if not(t < m) and t in ns:
            res = s + [t]
            for x in set(res):
              if res.count(x) > c:
                return
            yield tuple(res)
        else:
          for (i, n) in enumerate(ns):
            if not(n < t): break
            if not(n < m):
              yield from decompose(t - n, k - 1, ns[i:], s + [n], n, c)
      
      # there are 4 stamps making 13 (= 6.5p) left over from a book
      leftovers = set(decompose(13, 4, [1, 2, 3, 4] * 2, c=2))
      
      for lo in leftovers:
       # leftover + 10p book
       ss = lo + (1, 2, 3, 4) * 2
       # choose 4 stamps making 11 (= 5.5p) for the teaser entry
       for t in set(decompose(11, 4, ss)):
         remaining = list(ss)
         for x in t:
           if x in remaining: remaining.remove(x)
         
         # but you can't make 11 (= 5.5p) using 3 stamps from what is left over
         if any(decompose(11, 3, remaining)): continue
         print(f"teaser =",
               f"{', '.join(str(x / 2).rstrip('0').rstrip('.') + 'p' for x in t)}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:28 am on 6 June 2021 Permalink | Reply
    Tags:   

    Teaser 2000: 2000 down 

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

    Many years ago. I took a thin strip of wood and cut it into four pieces, each a different whole number of centimetres long. I could use those pieces to form the sides of many different quadrilaterals, and of all the choices I made one of the largest area possible. I then mounted the quadrilateral on my study wall. When the magazine started numbering its Brainteasers (now known as Teasers), I did them each week, and on finishing the puzzle I shaded a new patch of area [exactly] one square centimetre within the quadrilateral.

    After today’s auspicious Teaser, I will have shaded the entire quadrilateral [exactly]. So next week I shall make another quadrilateral to cover precisely the next 2000 Teasers. Again its sides will be four different whole numbers of centimetres and its area will be the largest possible with those particular lengths. I shall need a strip of wood at least as long as the one I used last time.

    What are the lengths of the sides of my first quadrilateral?

    [teaser2000]

     
    • Jim Randell's avatar

      Jim Randell 9:29 am on 6 June 2021 Permalink | Reply

      For a quadrilateral with given sides, the maximal area occurs when the quadrilateral is cyclic, and is given by the formula:

      A = (1/4)√((−a + b + c + d)(a − b + c + d)(a + b − c + d)(a + b + c − d))

      (Which is a generalisation of Heron’s Formula, known as Brahmagupta’s formula).

      We are interested in cases where: A = 2000.

      Which means were are interested in when:

      (−a + b + c + d)(a − b + c + d)(a + b − c + d)(a + b + c − d) = 64000000

      And as each of a, b, c, d are integers, so are the terms on the LHS, so we can consider factorisations of the RHS into 4 integer factors, and then solve the resulting 4 equations to give us candidate side lengths.

      The [[ divisor_tuples() ]] function is borrowed from Enigma 1062.

      This Python program runs in 162ms.

      Run: [ @replit ]

      from enigma import divisors_pairs, matrix, as_int, seq_all_different, printf
      
      # find ordered k-tuples that multiply to give n
      def divisor_tuples(n, k, s=[]):
        if k == 1:
          if not(s and n < s[-1]):
            yield s + [n]
        else:
          for (a, b) in divisors_pairs(n):
            if not(s and a < s[-1]):
              yield from divisor_tuples(b, k - 1, s + [a])
      
      # find maximal quadrilateral with area A
      def solve(A):
        # coefficients of a, b, c, d in the factors
        eqs = [
          (-1, +1, +1, +1),
          (+1, -1, +1, +1),
          (+1, +1, -1, +1),
          (+1, +1, +1, -1),
        ]
        # factorise (4A)^2
        for fs in divisor_tuples(16 * A * A, 4):
          # and solve the equations (for positive integers)
          try:
            ns = list(as_int(x, '+') for x in matrix.linear(eqs, fs))
          except ValueError:
            continue
          else:
            yield sorted(ns)
      
      # collect solutions consisting of different integers
      ss = sorted((ns for ns in solve(2000) if seq_all_different(ns)), key=sum)
      
      # output solutions
      for ns in ss:
        printf("{ns} -> {t}", t=sum(ns))
        break  # we only need the smallest solution
      

      Solution: The first quadrilateral has sides of length: 5 cm, 55 cm, 65 cm, 85 cm.

      The minimal perimeter is 210 cm, and the sides are all multiples of 5cm. One possible quadrilateral with these side lengths looks like this:

      This is the collection of side lengths with the smallest perimeter. But the program finds 8 possible collections (ordered by perimeter):

      210: (5, 55, 65, 85)
      240: (20, 40, 70, 110)
      288: (16, 19, 119, 134)
      294: (22, 47, 83, 142)
      308: (26, 29, 104, 149)
      330: (5, 40, 125, 160)
      486: (43, 83, 118, 242)
      504: (2, 124, 127, 251)

      If the setter choose the next smallest perimeter for Teasers 2001 – 4000, the quadrilateral will have sides: 20 cm, 40 cm, 70 cm, 110 cm (perimeter = 240 cm), which are all multiples of 10 cm.

      Like

    • Hugh Casement's avatar

      Hugh Casement 10:01 am on 6 June 2021 Permalink | Reply

      I reckon to have found a near-solution with a shorter perimeter.
      If a = 39, b = 42, c = 47, d = 52, then the perimeter is 180 and the area is about 2000.008 units.
      It takes only a very slight deviation from cyclic to reduce that to 2000.

      Like

      • Jim Randell's avatar

        Jim Randell 10:32 am on 6 June 2021 Permalink | Reply

        For all practical purposes this is a viable answer (and a nicer looking quadrilateral).

        If I’ve calculated correctly you would have to be able to measure and cut the wooden strip to an accuracy of about 2e-14 cm to disallow this. Which I think is about 1/1000000 th the width of an atom.

        Like

    • GeoffR's avatar

      GeoffR 5:38 pm on 6 June 2021 Permalink | Reply

      I tried a brute force solution in MiniZinc, experimenting with the upper bound value of the four sides.

      I found setting an upper bound of 150 produced four out of five of Jim’s smallest perimeter solutions, but missed the perimeter of 294. It did, however, find the same two lowest perimeters of 210 and 240.

      I found the Chuffed Solver was necessary to run it satisfactorily, and it took several seconds to run.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..150:a; var 1..150:b; var 1..150:c; var 1..150:d; 
      constraint all_different([a, b, c, d]);
      
      % Using Jim's version of the Brahmagupta formula for maximising the area
      constraint (-a + b + c + d) * (a - b + c + d) * (a + b - c + d) 
      * (a + b + c - d) == 64000000;
      
      % Minimising the perimeter finds different solutions
      solve minimize(a + b + c + d);
      
      output [" Four sides = " ++ show([a, b, c, d]) ++ 
      ", Perimeter = " ++ show(a + b + c + d)  ];
      
      %  Four sides = [104, 29, 26, 149], Perimeter = 308
      %  ----------
      %  Four sides = [16, 119, 19, 134], Perimeter = 288
      % ----------
      %  Four sides = [40, 110, 20, 70], Perimeter = 240
      % ----------
      %  Four sides = [55, 5, 85, 65], Perimeter = 210  <<< first quadrilateral
      % ----------
      % ==========
      
      
      

      Like

      • Frits's avatar

        Frits 3:57 pm on 7 June 2021 Permalink | Reply

        Adding the following is a bit faster

        set of int: forbidden = {3, 7, 9};
        
        var 1..999: p1 = -a + b + c + d;
        var 1..999: p2 = a - b + c + d;
        var 1..999: p3 = a + b - c + d;
        var 1..999: p4 = a + b + c - d;
        
        % numbers may not end on 3, 7 or 9
        constraint forall( [p1 mod 10 != i | i in forbidden] );
        constraint forall( [p2 mod 10 != i | i in forbidden] );
        constraint forall( [p3 mod 10 != i | i in forbidden] );
        constraint forall( [p4 mod 10 != i | i in forbidden] );
        

        Also adding the following slows it down again but it does report all 8 solutions (with 1..300 for a,b,c,d) with the Chuffed Solver.

        set of int: forbidden2 = {3, 7};
        
        % numbers may not be multiples of 3 or 7
        constraint forall( [p1 mod i != 0 | i in forbidden2] );
        constraint forall( [p2 mod i != 0 | i in forbidden2] );
        constraint forall( [p3 mod i != 0 | i in forbidden2] );
        constraint forall( [p4 mod i != 0 | i in forbidden2] );
        

        Like

      • Frits's avatar

        Frits 7:38 pm on 9 June 2021 Permalink | Reply

         
        constraint a < b /\ b < c /\ c < d; 
        
        is a lot faster than 
        
        constraint all_different([a, b, c, d]);
        

        Like

    • GeoffR's avatar

      GeoffR 7:15 am on 8 June 2021 Permalink | Reply

      @Frits: Yes, interesting code enhancements.
      If the UB of a,b,c,d is set to 100, it produces the single smallest answer.

      Like

    • Frits's avatar

      Frits 5:14 pm on 9 June 2021 Permalink | Reply

      Runs a lot faster with PyPy than with Python 3.9.

      T = 64000000
      
      # loop over perimeter (sum of the side lengths)
      # as from (4 * sqrt(2000))
      for P in range(179, 999999):
        for a in range(1, P // 4 + 1):
          for b in range(a + 1, (P - a) // 3 + 1):
            for c in range(b + 1, (P - a - b) // 2 + 1):
              d = P - (a + b + c)
              p1 = -a + b + c + d
              p2 = a - b + c + d
              p3 = a + b - c + d
              p4 = a + b + c - d
            
              if p1 * p2 * p3 * p4 != T: continue
      
              print(f"side lengths {a}, {b}, {c}, {d} with perimeter {P}")  
              exit()
      

      (Modified as per comment below).

      Like

      • Jim Randell's avatar

        Jim Randell 5:22 pm on 9 June 2021 Permalink | Reply

        @Frits: You can start looking for perimeters at [[ intc(4 * sqrt(2000)) ]] = 179, as a square has the minimal perimeter for a given area.

        Like

    • GeoffR's avatar

      GeoffR 8:54 pm on 9 June 2021 Permalink | Reply

      A straight translation of my MiniZinc programme for the single solution.

      
      from enigma import Timer
      timer = Timer('ST2000', timer='E')
      timer.start()
      
      for a in range(1,100):
        for b in range(a+1,100):
          for c in range(b+1,100):
            for d in range(c+1, 100):
              if (-a + b + c + d) * (a - b + c + d) \
                 * (a + b - c + d) * (a + b + c - d) == 64000000:
                print(f"4 Sides/Perimeter: {a}, {b}, {c}, {d}, {a+b+c+d}")
                timer.stop()      
                timer.report()
                exit()  
      
      # 4 Sides/Perimeter: 5, 55, 65, 85, 210
      # ST2000] total time: 0.4669819s (466.98ms)
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:56 pm on 4 June 2021 Permalink | Reply
    Tags:   

    Teaser 3063: Pie by five 

    From The Sunday Times, 6th June 2021 [link] [link]

    We had a delicious pie, rectangular and measuring 20 centimetres along the top and 13 centimetres in the other direction. We divided it into five pieces of equal area, using five straight cuts radiating from one internal point. This internal point was rather off centre, in the top left-hand quarter, although the distances from the left and top sides were both a whole number of centimetres. The points where the cuts met the edges were also whole numbers of centimetres along the edges; one edge had two cuts meeting it, and the other three edges had one each.

    How far was the internal point from the left and top sides, and how far along the four sides (starting at the top) did the cuts reach the edges (measured clockwise along the edges)?

    [teaser3063]

     
    • Jim Randell's avatar

      Jim Randell 9:59 pm on 4 June 2021 Permalink | Reply

      Each portion has an area equal to one fifth of the whole pie.

      The portion between the two cuts on the same side is composed of a single triangle (with, as it turns out, its base along the bottom edge). The remaining portions are composed of two adjacent triangles, with their bases on adjacent edges.

      Using the top left corner of the pie as the origin, we can consider integer (x, y) co-ordinates for the starting point of the cuts (in the top left quadrant of the pie). We can then consider 2 integer marks on the bottom that give a triangular slice with one fifth the area of the pie, and the marks on the other edges can then be determined.

      This Python program runs in 54ms.

      Run: [ @replit ]

      from itertools import product
      from enigma import (Rational, irange, divf, printf)
      
      Q = Rational()
      
      # is rational q an integer?
      as_int = lambda q: (q.numerator if q.denominator == 1 else None)
      
      # dimensions of the rectangle
      (X, Y) = (20, 13)
      
      # area of each portion
      A = Q(X * Y, 5)
      printf("[X={X} Y={Y}; A={A}]")
      
      # calculate area of a triangle with base b and height h
      area = lambda b, h: Q(b * h, 2)
      
      # calculate base of triangle with area A and height h
      base = lambda A, h: Q(2 * A, h)
      
      # find solutions with cuts starting at (x, y)
      def solve(x, y):
        # look for the portion composed of 1 triangle:
        # bottom marks, (a, Y), (b, Y); distance z (= b - a) apart
        z = as_int(base(A, Y - y))
        if z is None: return
        for a in irange(1, X - z - 1):
          b = a + z
          # remaining bottom triangles left and right
          (B1, C1) = (area(a, Y - y), area(X - b, Y - y))
          # left mark, (0, c)
          c = as_int(Y - base(A - B1, x))
          if c is None or not (0 < c < Y): continue
          # right mark (X, d)
          d = as_int(Y - base(A - C1, X - x))
          if d is None or not (0 < d < Y): continue
          # top mark (e, 0)
          e = as_int(base(A - area(c, x), y))
          if e is None or not (0 < e < X): continue
          printf("x={x} y={y} -> bottom: {a}, {b}; left: {c}; right: {d}; top: {e}")
      
      # choose a position for the start of the cuts
      for (x, y) in product(irange(1, divf(X - 1, 2)), irange(1, divf(Y - 1, 2))):
        solve(x, y)
      

      Solution: The cuts started from a point 8 cm in from the left edge and 5 cm in from the top edge. The end of cuts were: top = 16 cm from the left; right = 7 cm from the top; bottom = 4 cm and 17 cm from the right; left = 10 cm from the bottom.

      A similar approach can be used to look for situations with 2 cuts on other edges, but these yield no viable solutions.

      The idea of splitting each portion into triangles can be used to show that for any regular polygon, portions (cut from the centre) with equal amounts of the perimeter have equal size.

      Like

      • Frits's avatar

        Frits 10:18 am on 5 June 2021 Permalink | Reply

        @Jim, very clever. I am looking forward to next week (or earlier) when you explain the method you have used.

        Like

    • Frits's avatar

      Frits 9:48 am on 5 June 2021 Permalink | Reply

      Having done most of the work myself.

      # x = distance internal point from left edge
      # y = distance internal point from top edge
      # a = distance top left-hand corner to meeting point on left edge
      # b = distance top left-hand corner to meeting point on top edge
      # c = distance top right-hand corner to meeting point on right edge
      # d = distance bottom left-hand corner to 2nd meeting point on bottom edge
      # e = distance bottom left-hand corner to 1st meeting point on bottom edge
      
      # if we make a triangle with the bottom edge (points e and d) 
      # then the following formula can be derived: (d - e)(13 - y) = 104 = 8 * 13
      #
      # only one value for y is possible.
      
      # with this derived value of y we calculate the areas of the 5 slices
      # and end up with 5 equations and 6 variables
      #
      #  (13 - a)x = 8(13 - e)       (A)
      #  5b + ax = 104               (B)
      #  (20 - x)c = 4 + 5b          (C)
      #  13x + 8d + 20c - cx = 316   (D)
      #  d = 13 + e                  (E)
      
      for a in range(1, 7):                  # (not disclosing y)
        for x in range(1, 10):
          # consider equation B
          ax = a * x
          if ax % 10 not in {4, 9}: continue
          b = (104 - ax) // 5 
          # meeting point with edge is not in the corner
          if b > 19: continue 
         
          # consider equation A
          y = 13 * x - ax
          if y % 8: continue
          
          e = 13 - y // 8
          d = 13 + e
          
          # consider equation C
          (c, r) = divmod(4 + 5 * b, 20 - x)
          if r: continue
          
          # check equation D
          if 13 * x + 8 * d + 20 * c - c * x != 316: continue
          
          # use bogus formula for derived value y (not disclosing y)
          print(f"{x=} y={x-a}, top: {b} right: {c} bottom: {e=} {d=} left: {a}")  
      

      Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 7:53 am on 7 June 2021 Permalink | Reply

        My approach was similar to yours Frits, but I could not satisfy myself by general reasoning that the two cuts had to be on the bottom edge as opposed to the right-hand edge. I therefore tested for the triangle to be on the right-hand edge but this did not produce a valid solution. Therefore the triangle must be on the bottom edge and there is only one solution.

        I got formulae similar to yours by considering parallelograms with missing triangles. Jim’s solution is much more elegant because it divides all the areas into triangles.

        Like

        • Jim Randell's avatar

          Jim Randell 9:31 am on 7 June 2021 Permalink | Reply

          @Tony: I did start by considering the two cuts on different edges, but it turned out they had to be on the bottom.

          As my program didn’t have a neat way to try the alternates, I just removed the dead paths from my program.

          I’ve put a more complete program up as teaser3063x.py on replit.

          from itertools import product
          from enigma import (Rational, irange, divf, printf)
          
          Q = Rational()
          
          # is rational q an integer?
          as_int = lambda q: (q.numerator if q.denominator == 1 else None)
          
          # dimensions of the rectangle
          (X, Y) = (20, 13)
          
          # area of each portion
          A = Q(X * Y, 5)
          printf("[X={X} Y={Y}; A={A}]")
          
          # calculate area of a triangle with base b and height h
          area = lambda b, h: Q(b * h, 2)
          
          # calculate base of triangle with area A and height h
          base = lambda A, h: Q(2 * A, h)
          
          # find solutions with cuts starting at (x, y)
          def solve(x, y):
            # look for the portion composed of 1 triangle:
          
            # top marks, distance z apart
            z = as_int(base(A, y))
            if z is not None:
              for a in irange(1, X - z - 1):
                b = a + z
                printf("x={x} y={y} -> top: {a}, {b}; ...")
          
            # bottom marks, distance z apart
            z = as_int(base(A, Y - y))
            if z is not None:
              for a in irange(1, X - z - 1):
                b = a + z
                # remaining bottom sections of slices left and right
                (B1, C1) = (area(a, Y - y), area(X - b, Y - y))
                # left mark
                c = as_int(Y - base(A - B1, x))
                if c is None or not (0 < c < Y): continue
                # right mark
                d = as_int(Y - base(A - C1, X - x))
                if d is None or not (0 < d < Y): continue
                # top mark
                e = as_int(base(A - area(c, x), y))
                if e is None or not (0 < e < X): continue
                printf("x={x} y={y} -> bottom: {a}, {b}; left: {c}; right: {d}; top: {e}")
          
            # left marks, distance z apart
            z = as_int(base(A, x))
            if z is not None:
              for a in irange(1, Y - z - 1):
                b = a + z
                printf("x={x} y={y} -> left: {a}, {b}; ...")
          
            # right marks, distance z apart
            z = as_int(base(A, X - x))
            if z is not None:
              for a in irange(1, Y - z - 1):
                b = a + z
                # remaining sections of right slices above and below
                (B1, C1) = (area(a, X - x), area(Y - b, X - x))
                # top mark
                c = as_int(X - base(A - B1, Y - y))
                if c is None or not (0 < c < X): continue
                # bottom mark
                d = as_int(X - base(A - C1, Y - y))
                if d is None or not (0 < d < X): continue
                printf("x={x} y={y} -> right: {a}, {b}; top={c}; bottom={d}; ...")
          
          # choose a position for the start of the cuts
          for (x, y) in product(irange(1, divf(X - 1, 2)), irange(1, divf(Y - 1, 2))):
            solve(x, y)
          

          Like

  • Unknown's avatar

    Jim Randell 8:38 am on 3 June 2021 Permalink | Reply
    Tags:   

    Teaser 2793: Sum triangles 

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

    In this picture:

    there are ten upwardly-pointing triangles of all sizes.

    In a similar picture with many more rows, the total number of upward-pointing triangles is divisible by all the digits from 1 to 9.

    In this larger picture the number of upward-pointing triangles in the bottom row is equal to Trix’s age.

    How old is Trix?

    [teaser2793]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 3 June 2021 Permalink | Reply

      (See also: Enigma 1296).

      The number of upward pointing triangles on a triangular grid of size n is the sum of the triangular numbers from 1 to n. These are the tetrahedral (or triangular pyramidal) numbers (see: OEIS A000292).

      The following Python program runs in 50ms.

      Run: [ @replit ]

      from enigma import (irange, inf, tri, mlcm, call, printf)
      
      # generate increasing (grid size, upward triangles) pairs
      def generate(m=inf):
        t = 0
        for n in irange(1, m):
          t += tri(n)
          yield (n, t)
      
      # find a solution divisible by d
      d = call(mlcm, irange(1, 9))
      for (n, t) in generate():
        if t % d == 0:
          printf("n={n} t={t}")
          break  # we only need the first solution
      

      Solution: Trix is 54.


      Using the formula:

      ups(n) = n(n + 1)(n + 2) / 6

      We are looking for solutions where ups(n) is a multiple of 2520:

      n(n + 1)(n + 2) / 6 = 2520k
      n(n + 1)(n + 2) = 15120k

      Which gives the following program:

      from enigma import (irange, inf, tuples, multiply, printf)
      
      for ns in tuples(irange(1, inf), 3):
        t = multiply(ns)
        if t % 15120 == 0:
          printf("n={n} t={t}", n=ns[0])
          break
      

      Like

c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel
Design a site like this with WordPress.com
Get started