Tagged: by: Mark Valentine Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 7:20 am on 20 July 2025 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3278: Page weight 

    From The Sunday Times, 20th July 2025 [link] [link]

    An A5 booklet is produced by printing the pages onto sheets of A4 (both sides), then binding and folding along the middle. The booklet contains two chapters. The longer first chapter runs to 34 pages.

    The pages in each chapter are numbered sequentially from 1 upwards. The front cover is page 1 of chapter one. Page 1 of chapter two follows directly after page 34 of chapter one. Any blank pages at the end of the booklet are not numbered.

    Without changing their order, the sheets can be split into two piles, where the sums of the page numbers in each pile are equal.

    How many pages does the second chapter have?

    [teaser3278]

     
    • Jim Randell's avatar

      Jim Randell 7:52 am on 20 July 2025 Permalink | Reply

      Although not explicitly stated, I am assuming the booklet consists of the minimum number of A4 sheets required to fit all pages of both chapters.

      Here is a constructive solution, that constructs the pairs of pages and organises them into A4 sheets, and then looks for a scenario where the sheets can be split as required. (This allows easy output of the page numbers on each A4 sheet, if desired).

      The following Python program runs in 60ms. (Internal runtime is 539µs).

      Run: [ @codepad ]

      from enigma import (irange, divc, div, chunk, chain, printf)
      
      # pages for chapter 1
      n1 = 34
      
      # consider the number of pages in chapter 2 (< n1)
      for n2 in irange(1, n1 - 1):
      
        # pair up the pages
        pairs = list(chunk(chain(irange(1, n1), irange(1, n2)), 2))
        # total sum of all page numbers divided by 2
        s = div(sum(map(sum, pairs)), 2)
        if s is None: continue
      
        # total number sheets of A4 required
        t = divc(n1 + n2, 4)
      
        # collect the page numbers for each sheet
        sheets = [()] * t
        for (i, p) in zip(chain(irange(t), irange(t, step=-1)), pairs):
          sheets[i] += p
      
        # can we find some sheets that sum to s ?
        x = 0
        for k in irange(t):
          x += sum(sheets[k])
          if x == s:
            # output solution
            printf("chapter 2 has {n2} pages [{t} sheets in total; first {k} sheets sum to {x}]", k=k + 1)
          if x >= s:
            break
      

      Solution: Chapter 2 has 25 pages.

      There are 15 A4 sheets, and the page numbers are as follows:

      1: (1, 2) (25, -)
      2: (3, 4) (23, 24)
      3: (5, 6) (21, 22)
      4: (7, 8) (19, 20)
      5: (9, 10) (17, 18)
      6: (11, 12) (15, 16)
      7: (13, 14) (13, 14)
      8: (15, 16) (11, 12)
      9: (17, 18) (9, 10)

      10: (19, 20) (7, 8)
      11: (21, 22) (5, 6)
      12: (23, 24) (3, 4)
      13: (25, 26) (1, 2)
      14: (27, 28) (33, 34)
      15: (29, 30) (31, 32)

      The sum of the page numbers of sheets 1-9 and sheets 10-15 both come to 460.


      If we can construct the booklet with more than the minimum required number of A4 sheets, then it is possible to construct a further solution:

      If chapter 2 has 14 pages and the booklet was printed from 17 sheets, then the sum of the page numbers on sheets 1-12 and sheets 13-17 would both come to 350 (and there would be 20 blank pages at the end of the booklet).

      This justifies the initial assumption.


      The program can be used to investigate similar puzzles where the number of pages in the first chapter is other than 34.

      An interesting case is when chapter 1 has 19 pages. There are then 2 solutions:

      Chapter 2 has 4 pages.
      The booklet consists of 6 sheets.
      The first 4 (and last 2) sheets have page numbers which sum to 100.

      Chapter 2 has 16 pages.
      The booklet consists of 9 sheets.
      The first 5 (and last 4) sheets have page numbers which sum to 163.

      Like

      • Jim Randell's avatar

        Jim Randell 9:15 am on 20 July 2025 Permalink | Reply

        Here is a faster version of my program that works with a list of the sum of pairs of pages on a leaf.

        It has an internal runtime of 124µs.

        from enigma import (irange, chunk, div, divc, seq_get, printf)
        
        # pages for chapter 1
        n1 = 34
        
        # page pair sums from chapter 1
        pairs = list(map(sum, chunk(irange(1, n1), 2)))
        t = sum(pairs)
        get = seq_get(pairs, default=0)
        
        # consider the number of pages in chapter 2 (< n1)
        for (i, n2) in enumerate(irange(1, n1 - 1), start=n1):
          # make the pair sum of the final page
          if i % 2:
            pairs[-1] += n2
          else:
            pairs.append(n2)
          t += n2
          s = div(t, 2)
          if s is None: continue
        
          # now look for sheets that sum to s, by pairing up page sums
          (x, k) = (0, len(pairs))
          if k % 2 == 0: k -= 1
          for i in irange(divc(k, 2)):
            x += get(i) + get(k - i)
            if x == s:
              # output solution
              printf("chapter 2 has {n2} pages [first {i} sheets sum to {x}]", i=i + 1)
            if x >= s:
              break
        

        Like

    • Frits's avatar

      Frits 2:45 pm on 20 July 2025 Permalink | Reply

      One loop.

      The sum of 4 page numbers per sheet can only have 2, 3 or 4 different values (per number of pages in chapter 2).

      It took a while before I figured out how the booklet was constructed.

      # one folded A4 sheet results in 4 pages
      
      t = 17 * 35        # sum(1...34)
      # number of pages in chapter 2
      for n in range(1, 34):
        # sum of all pages
        t += n
        # both <isum> as <osum> are even so <h> must also be even
        if t % 4: continue
        h = t // 2   
        
        n_sheets = (37 + n) // 4
        n_inner_sheets = 17 - n_sheets
        
        # calculate starting page number of most inner sheet
        st = 2 * ((n + 1) // 4) + 17
        
        # calculate sum of 4 outer sheet page numbers (except most outer sheet)
        osum = (2 * (n + n % 2) - 1) + 3 if n > 1 else -1
        # calculate sum of most inner sheet page numbers
        isum = 4 * st + 6 if n_sheets < 17 else osum
        
        # can we make <h> using <k> sheets (beginning from the center)?
       
        # h = a.isum + b.osum or  b = (h - a.isum) / osum
        # use maximum <n_inner_sheets> inner sheets and possible <b> outer sheets
        d, r = divmod(h, isum)
        if not r and d <= n_inner_sheets:
          print("answer:", n)
        else:  
          # h - n_inner_sheets * inner = b * osum
          b, r = divmod(h - n_inner_sheets * isum, osum)
          if not r and 0 < b <= n_sheets - n_inner_sheets:
            if osum > 0: 
              print("answer:", n)
      

      Like

  • Unknown's avatar

    Jim Randell 6:53 am on 13 April 2025 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3264: Saving rum 

    From The Sunday Times, 13th April 2025 [link] [link]

    Captain Noah was tired of spilling his rum at sea. His glass (twice the density of the rum) is a hollow cylinder with a solid flat base. The outer diameter is 72 mm. The glass thickness and base height are both 4 mm. After testing the stability of the upright glass, he instructed the steward to keep his glass topped up to one-third full, where it has the lowest centre of mass.

    What is the overall height of the glass (including the base)?

    [teaser3264]

     
    • Jim Randell's avatar

      Jim Randell 8:03 am on 13 April 2025 Permalink | Reply

      Here is a numerical solution, using the [[ find_min() ]] function from the enigma.py library.

      I treat the glass and rum system as 3 separate components:

      a hollow cylinder of glass, with external diameter 72 mm (external radius = 36 mm) and 4 mm thick walls (internal radius = 32 mm), and height h.

      a solid disc of glass, with diameter 72 mm (radius = 36 mm) and height 4 mm, that sits underneath the cylinder, on a flat table.

      a cylinder of rum with radius 32 mm, and height x (between 0 and h), that sits on the disc, inside the cylinder. The density of the rum is 1/2 that of the glass.

      The individual centres of mass of the three components are then combined to give the combined centre of mass. Each of them is somewhere on the central line of the cylinder, so we only need to worry about their heights above the table.

      For a given value of h we can calculate the value of x that gives the minimum height for the combined centre of mass, and then look for a value of h where h = 3x.

      This Python program runs in 69ms. (Internal runtime is 2.3ms).

      from enigma import (sq, fdiv, find_min, find_value, printf)
      
      # given height h find height of rum that gives the minimal centre of mass
      def fn(h):
      
        # calculate centre of mass for rum height x
        def f(x):
          # mass of cylinder, base, rum (half the density)
          mc = (sq(36) - sq(32)) * h * 2
          mb = sq(36) * 4 * 2
          mr = x * sq(32)
          # heights of their respective centres of mass
          hc = 4 + h * 0.5
          hb = 2
          hr = 4 + x * 0.5
          # calculate height of combined centre of mass
          return fdiv(mc * hc + mb * hb + mr * hr, mc + mb + mr)
      
        # find minimal centre of mass = x
        r = find_min(f, 0, h)
        x = r.v
        # return h/x
        return fdiv(h, x)
      
      # find when h/x = 3 (i.e. the minimal centre of mass is when the glass is 1/3 full)
      r = find_value(fn, 3, 20, 200)
      # output overall glass height (including base = 4 mm)
      h = r.v + 4
      printf("height = {h:.2f} mm")
      

      Solution: The glass is 112 mm high.

      So the internal height is 108 mm, and the lowest centre of mass occurs with 36 mm of rum in the glass.

      We can get an exact answer to the puzzle using analysis.

      Like

      • Jim Randell's avatar

        Jim Randell 1:14 pm on 15 April 2025 Permalink | Reply

        Here is my analytical solution:

        The CoM of the cylinder is in the centre of the cylinder, and adding the base will bring the CoM down a little.

        Suppose we then start adding rum to the glass:

        We start by adding a little rum. This forms a layer of rum below the current CoM of the glass, so the combined CoM moves down a bit. And this continues until the combined CoM is level with the top of the rum. At this point, when we add additional rum, it forms a layer above the current CoM, and so the combined CoM starts to move upwards.

        Hence the combined CoM is at a minimum when it is level with the top of the rum.

        So we are interested in when:

        ∑(m[i] . h[i]) / ∑(m[i]) = x + 4

        ∑(m[i] . h[i]) = (x + 4) ∑(m[i])

        And we want to find solutions when h = 3x, which we can do manually, or use a program.

        from enigma import (Polynomial, rdiv, sq, printf)
        
        x = Polynomial('x')
        h = 3 * x
        half = rdiv(1, 2)
        
        # mass of cylinder, base, rum (half the density)
        mc = (sq(36) - sq(32)) * h * 2
        mb = sq(36) * 4 * 2
        mr = x * sq(32)
        # heights of their respective centres of mass
        hc = 4 + h * half
        hb = 2
        hr = 4 + x * half
        
        # polynomial to solve: sum(m[i] * h[i]) = (x + 4) * sum(m[i])
        p = (mc * hc + mb * hb + mr * hr) - (x + 4) * (mc + mb + mr)
        p = p.scale()
        printf("[p = {p}]")
        
        # find real, positive solutions for p(x) = 0
        for x in p.roots(domain='F', include='+'):
          h = 3 * x
          height = h + 4
          printf("height = {height} mm [x={x} h={h}]")
        

        Manually we find the resulting polynomial p(x) factorises as follows:

        p(x) = 19x^2 − 648x − 1296
        p(x) = (19x + 36)(x − 36)

        From which we see there is a single positive real root that is an integer, and this provides an integer answer to the puzzle.


        Or we can use calculus to determine when the minimum height centre of mass occurs:

        For a glass with an internal height h and a depth of rum x we can calculate the centre of mass as:

        f(x) = (544h(4 + h/2) + 10368 . 2 + 1024x(4 + x/2)) / (544h + 10368 + 1024x)
        f(x) = (17h^2 + 136h + 32x^2 + 256x + 1296) / (34h + 64x + 648)

        And we can calculate when the derivative of this function with respect to x is 0, using the quotient rule:

        f(x) = p(x) / q(x)

        f'(x) = (p'(x) q(x) − p(x) q'(x)) / q(x)^2

        So the minimum value occurs when p’ q = p q’.

        In this case:

        p(x) = (17h^2 + 136h + 32x^2 + 256x + 1296)
        p'(x) = 64x + 256

        q(x) = (34h + 64x + 648)
        q'(x) = 64

        (64x + 256)(34h + 64x + 648) = (17h^2 + 136h + 32x^2 + 256x + 1296)(64)

        32x^2 + (648 + 34h)x + (1296 − 17h^2) = 0

        And we want to find the minimum value when h = 3x:

        32x^2 + (648 + 34 . 3x)x + (1296 − 17 . (3x)^2) = 0

        19x^2 − 648x − 1296 = 0
        (19x + 36)(x − 36) = 0

        x = 36
        h = 108

        And the answer to the puzzle follows directly.

        Like

  • Unknown's avatar

    Jim Randell 7:47 am on 9 February 2025 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3255: A square for all digits 

    From The Sunday Times, 9th February 2025 [link] [link]

    I have written down a set of whole numbers, each greater than 1 and less than 100. I have squared each number and written the answers down in a list. The list contains all of the digits from 0 to 9 precisely once. Interestingly, for any two of the numbers, there is no common factor other than 1.

    Naturally the sum of the digits in the squared set is 45, but what is the sum of the digits in the original set of numbers that I wrote down?

    [teaser3255]

     
    • Jim Randell's avatar

      Jim Randell 7:59 am on 9 February 2025 Permalink | Reply

      This Python program runs in 68ms. (Internal runtime is 1.2ms).

      from enigma import (irange, gcd, dsum, seq2str, printf)
      
      # digits (as strings)
      digits = set("0123456789")
      
      # allocate squares from <d> for the remaining digits in <ds>
      # return (<ss> = squares, <rs> = roots)
      def solve(d, ds, ss, rs):
        # are we done?
        if not ds:
          yield (ss, rs)
        else:
          # choose the next square
          r_ = rs[-1]
          for (s, r) in d.items():
            # and check roots are pairwise coprime
            if r < r_ and ds.issuperset(s) and all(gcd(x, r) == 1 for x in rs):
              # solve for the remaining digits
              yield from solve(d, ds.difference(s), ss + [s], rs + [r])
      
      # record squares; map root -> square (as string)
      d = dict()
      
      # consider the largest square in the set
      for i in irange(2, 99):
        s = str(i * i)
        if len(set(s)) != len(s): continue
      
        # find other squares to use the remaining digits
        for (ss, rs) in solve(d, digits.difference(s), [s], [i]):
          # calculate total digit sum of the roots
          t = sum(dsum(r) for r in rs)
          # output solution
          printf("squares = {ss}, roots = {rs} -> t = {t}", ss=seq2str(ss))
      
        # record this square
        d[s] = i
      

      Solution: The sum of the digits in the original set of numbers is: 24.

      There are two possible original sets (with the same total digit sum):

      13, 28, 55 (digit sum = 24)
      squares → 169, 784, 3025

      28, 31, 55 (digit sum = 24)
      squares → 784, 961, 3025

      Like

      • Jim Randell's avatar

        Jim Randell 1:31 pm on 9 February 2025 Permalink | Reply

        We can treat finding the sets of squares that use each of the digits 0-9 exactly once as an exact cover problem (which we have encountered before, see Enigma 1712). And the [[ mcover() ]] routine allows us to check viable sets for those that are pairwise coprime as we go along.

        This gives a shorter program, which runs in 1.1ms.

        from enigma import (multiset, irange, is_coprime, dsum, printf)
        
        # possible squares; map root -> digit content (multiset of characters)
        d = dict((r, multiset.from_seq(str(r * r))) for r in irange(2, 99))
        
        # target is exactly one occurrence of each digit 0-9
        tgt = multiset.from_seq("0123456789")
        
        # find exact covers, with pairwise coprime roots
        for rs in tgt.mcover(d, reject=(lambda rs: not is_coprime(*rs))):
          # calculate total digit sum of roots
          t = sum(dsum(r) for r in rs)
          # output solution
          ss = list(r * r for r in rs)
          printf("squares = {ss}, roots = {rs} -> t = {t}", rs=sorted(rs), ss=sorted(ss))
        

        Liked by 1 person

    • Frits's avatar

      Frits 8:29 am on 9 February 2025 Permalink | Reply

      from itertools import combinations
      from math import gcd
      
      # squares with different digits
      sqs = [n2 for n in range(2, 100) if len(n2 := str(n * n)) == len(set(n2))]
      
      # dictionary of squares per digit
      d = {c: [s for s in sqs if c in s] for c in "0123456789"}
      
      # sort dictionary on frequency (optional)
      d = dict(sorted(d.items(), key=lambda x: len(x[1])))
      
      # get a list of squares that use digits 0-9 exactly once
      def solve(d, dgts="", ss=[]):
        if (ln_dgts := len(dgts)) == 10:
          yield ss
        else:
          # squares for next unused digit
          for sq in next(iter(d.values())):
            # refresh dictionary and remove empty entries
            d_ = {k: [v for v in vs if all(x not in v for x in sq)] 
                      for k, vs in d.items()}
            d_ = {k: vs for k, vs in d_.items() if vs}
            # digits not yet used must still have candidate squares
            if len(d_) + ln_dgts + len(sq) != 10: continue
            yield from solve(d_, dgts + sq, ss + [int(sq)])
       
      sols = set()    
      for ss in solve(d):
        # for any two of the numbers, there is no common factor other than 1
        if any(gcd(x, y) > 1 for x, y in combinations(ss, 2)): continue
        orig = [int(x**.5) for x in ss]
        sols.add(str(sum([int(x) for o in orig for x in str(o)])))
        
      print(f"answer(s): {' or '.join(sols)}")
      

      Like

  • Unknown's avatar

    Jim Randell 7:03 am on 1 December 2024 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3245: Gate escape 

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

    In front of you are six consecutive 200m sections, each gated at the far end, then you are free. Above your head a clock ticks. Each gate is open for four seconds, but then closed for a different single-digit number of seconds. The gates all open together every 143 minutes and are ordered by increasing closure time, with the nearest gate having the shortest closure time.

    Running at your highest possible constant speed, you will reach the end precisely four minutes after entry. If you go any slower, you will be caught by the guards. You wait to enter to ensure that, going at this speed, you pass through each gate in the middle of its open time.

    How many seconds after the gates open together do you enter?

    [teaser3245]

     
    • Jim Randell's avatar

      Jim Randell 7:38 am on 1 December 2024 Permalink | Reply

      See also: Enigma 198.

      There are only 6 possible single-digit closed durations that allow a whole number of open/closed cycles in a 143 minute period, so we can determine the cycles of each gate.

      We can then try possible start times to see when we would hit each gate 2 seconds into an open period.

      This Python program runs in 68ms. (Internal runtime is 200µs).

      from enigma import (irange, subsets, printf)
      
      # look for single digit closed durations
      T = 143 * 60  # cycle time (seconds)
      ts = list(t for t in irange(1, 9) if T % (4 + t) == 0)
      printf("[closed durations = {ts}]")
      
      # consider possible closed durations for the 6 gates (in order)
      for ss in subsets(ts, size=6, select='C'):
        # consider possible start times
        for t0 in irange(0, T - 240):
          # gates are open for 4s at the start of each (4 + t) period
          # do we hit each gate 2s after it opens?
          if all((t0 + 40 * i) % (4 + t) == 2 for (i, t) in enumerate(ss, start=1)):
            printf("{ss} -> t0 = {t0}")
            break
      

      Solution: You should enter the first section 282 seconds after the gates open together.

      The gate closed durations are (1, 2, 6, 7, 8, 9) seconds.

      It takes 40s to run through each section to the gate, and:

      282 + 40 = 322 = 64×(4 + 1) + 2
      322 + 40 = 362 = 60×(4 + 2) + 2
      362 + 40 = 402 = 40×(4 + 6) + 2
      402 + 40 = 442 = 40×(4 + 7) + 2
      442 + 40 = 482 = 40×(4 + 8) + 2
      482 + 40 = 552 = 40×(4 + 9) + 2

      Like

      • Jim Randell's avatar

        Jim Randell 3:10 pm on 1 December 2024 Permalink | Reply

        Once we have determined the closed duration for each gate, we can form a set of congruence equations of the form:

        t0 mod m = n

        one for each gate, where the m and n parameters are defined by the closed duration and distance for that gate.

        These can then be solved using the Chinese Remainder Theorem (CRT). (The CRT implementation in enigma.py is based on the code from Teaser 3117).

        The internal runtime is reduced to 65µs.

        from enigma import (irange, subsets, crt, printf)
        
        # look for single digit closed durations
        T = 143 * 60  # cycle time (seconds)
        ts = list(t for t in irange(1, 9) if T % (4 + t) == 0)
        printf("[closed durations = {ts}]")
        
        # consider possible closed durations for the 6 gates (in order)
        for ss in subsets(ts, size=6, select='C'):
          printf("[ss = {ss}]")
          # accumulate (residue, modulus) parameters for CRT
          vs = set()
          for (i, t) in enumerate(ss, start=1):
            (n, m) = (40 * i, 4 + t)
            r = (2 - n) % m
            printf("[(t0 + {n}) mod {m} = 2 -> t0 mod {m} = {r}]")
            vs.add((r, m))
          # solve the puzzle using CRT
          t0 = crt(vs).x
          printf("t0 = {t0}")
          printf()
        

        Manually the result is arrived at by solving the following congruences:

        (t0 mod 5 = 2)
        (t0 mod 6 = 0)
        t0 mod 10 = 2
        t0 mod 11 = 7
        t0 mod 12 = 6
        t0 mod 13 = 9

        So we can start looking for integers that are congruent to 9 mod 13, and then check that the other congruences are met, and we can just check those that are congruent to 2 mod 10, as these must end in 2:

        22 → fails mod 12
        152 → fails mod 12
        282 → answer!

        Like

  • Unknown's avatar

    Jim Randell 2:00 am on 22 September 2024 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3235: Dromedary lines 

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

    Zayed must cross the Setare desert to move to his summer camp, which lies 240 miles east, and a smaller whole number of miles north of his winter camp. Both camps are on a grid of wells every 5 miles north and east across the desert.

    His camels can only travel 100 miles before needing to stop for water. Being superstitious, he will only travel a whole number of miles, in a straight line, between wells. Subject to this, he attempts to minimise his journey by travelling to the well, in range, closest to the remaining straight-line route. If more than one well is equally close, he stops at the nearest one.

    During his journey he travels a total of 50 miles due east and 5 miles due north and visits 14 wells between his camps (not including the ones at the camps).

    List in order the length of each of the first five stages.

    [teaser3235]

     
    • Jim Randell's avatar

      Jim Randell 2:37 am on 22 September 2024 Permalink | Reply

      This Python program runs in 676ms. (Internal runtime is 570ms).

      from enigma import (
        Accumulator, irange, cproduct, line_distance,
        ihypot, unpack, call, tuples, printf
      )
      
      # calculate the next step of the journey; return ((x, y), dist)
      def step(x0, y0, x1, y1):
        # find the nearest wells to the line joining (x0, y0) to (x1, y1)
        r = Accumulator(fn=min, collect=1)
        for (x, y) in cproduct([irange(x0, x1, step=5), irange(y0, y1, step=5)]):
          # distance travelled should be an integer, less than 100
          d = ihypot(x - x0, y - y0)
          if (not d) or d > 100: continue
          r.accumulate_data(line_distance((x0, y0), (x1, y1), (x, y)), ((x, y), d))
        # find the minimum distance travelled
        return min(r.data, key=unpack(lambda pt, d: d))
      
      # calculate a journey to from p0 to p1; return (points, dists)
      def journey(p1, p0=(0, 0)):
        (pts, ds) = ([p0], [])
        while p0 != p1:
          (p0, d) = call(step, p0 + p1)
          pts.append(p0)
          ds.append(d)
        return (pts, ds)
      
      # consider possible distance north between camps
      for n in irange(5, 235, step=5):
        # look for a journey with 14 intermediate points
        (pts, ds) = journey((240, n))
        if len(pts) != 16: continue
        # accumulate due E and due N distances
        E = N = 0
        for ((x0, y0), (x1, y1)) in tuples(pts, 2):
          if y0 == y1: E += x1 - x0
          if x0 == x1: N += y1 - y0
        if not (E == 50 and N == 5): continue
        # output solution
        printf("n={n}: E={E} N={N} {pts}")
        printf("-> {ds}")
      

      Solution: The distances travelled in the first 5 stages of the journey are: 5, 5, 5, 85, 5 (miles).

      The summer camp is 115 miles north and 240 miles east of the winter camp.

      The full list of distances is:

      5, 5, 5, 85, 5, 85, 5, 5, 5, 25, 5, 25, 5, 5, 5 (miles)
      or:
      1, 1, 1, 17, 1, 17, 1, 1, 1, 5, 1, 5, 1, 1, 1 (units)

      involving the hypotenuses of (8, 15, 17) and (3, 4, 5) triangles.

      And the route looks like this:

      The direct route is shown in green (although only the first stage needs to come closest to this line – the direct line for subsequent steps runs from the current position to the destination).

      Like

    • Frits's avatar

      Frits 1:00 pm on 22 September 2024 Permalink | Reply

      @Jim, right now I come to the same “n” but to a different first five stages (different as of the 2nd stage).

      Having said that, I don’t understand the phrase: “If more than one well is equally close, he stops at the nearest one.”.

      Like

      • Jim Randell's avatar

        Jim Randell 1:20 pm on 22 September 2024 Permalink | Reply

        @Frits: I assumed that meant if there are multiple wells equally close to the direct line, then he chooses the one that is closest to his current position (i.e. the shortest distance to travel).

        Like

        • Frits's avatar

          Frits 1:23 pm on 22 September 2024 Permalink | Reply

          Thx, I will publish after the F1 has finished.

          Like

        • Frits's avatar

          Frits 9:30 pm on 22 September 2024 Permalink | Reply

          I finally understood the puzzle.

          # perpendicular distance from a point (x1, y1) to line ax + by + c = 0
          distance = lambda a, b, c, x1, y1: (abs(a * x1 + b * y1 + c) / 
                                             ((a * a + b * b)**.5))
                                             
          MX = 999999
          d = {n * n: n for n in range(1, 101)}
          
          def is_sqr(x, y):
            return d.get(x * x + y * y, None)
          
          # feasible diagonal routes (don't return z)  
          diags = [(x, y) for x in range(5, 100, 5) for y in range(5, 100, 5) 
                  if (z := is_sqr(x, y)) is not None]
          
          # find a solution that meets the requirements
          def check(journeys, a1, b1):
            a, b, c = a1, b1, 0
            pos = (0, 0)
            stops = []
            # keep adding stops if possible
            while pos[0] <= b1 and pos[1] <= -a1 and pos != (b1, -a1):
              mn = MX
              for j in set(journeys):
                pos2 = (pos[0] + j[0], pos[1] + j[1])       
                if not (pos2[0] <= b1 and pos2[1] <= -a1): continue
                # perpendicular distance to remaining line ax + by + c = 0
                dist = distance(a, b, c, pos2[0], pos2[1])
                if dist <= mn:
                  # if wells are equally close to the line, he stops at the nearest one
                  if dist == mn:
                    if j[0]**2 + j[1]**2 > min_j[0]**2 + min_j[1]**2:
                      continue
                  mn = dist
                  min_j = j
                  
              # set new position 
              pos = (pos[0] + min_j[0], pos[1] + min_j[1])   
          
              # calculate coefficients remaining line
              a, b = pos[1] + a1, b1 - pos[0]
              c = b * a1 - a * b1
              stops.append(min_j)
          
            # double check  
            if pos == (b1, -a1):
              yield stops
          
          E, dueE, dueN = 240, 50, 5
          # check possible north distances
          for N in range(5, E, 5):
            # check if the possible diagonal journeys with (0, 5) and (5, 0) 
            # can be fitted closest to the remaining straight-line route
            for stops in check(diags + [(0, 5), (5, 0)] , -N, E):
              if len(stops) != 15: continue
              if stops.count((0, 5)) != dueN // 5: continue
              if stops.count((5, 0)) != dueE // 5: continue
              print("answer:", [is_sqr(x, y) for x, y in stops[:5]],
                    "for N =", N)
          

          Like

  • Unknown's avatar

    Jim Randell 4:28 pm on 21 June 2024 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3222: Squarism 

    From The Sunday Times, 23rd June 2024 [link] [link]

    There are 84 marbles, numbered sequentially from 1, in a bag. Oliver (going first) and Pam take turns, each removing two marbles from the bag. The player finds the “most square” integer factorisation of each number. The difference between the two factors for each number is then added to the player’s score (e.g. removing marbles 1 and 84 scores 5 points since 1 = 1 × 1 and 84 = 7 × 12), and the game ends when all marbles are removed.

    After each of the first four turns (two each), both players’ expected final score was a whole number. For example, if there were 90 points remaining, with 4 turns left for Oliver and 5 for Pam, Oliver would expect to score another 40 points. In addition, each player’s score for their second turn was the same as their first. Also, Pamela scored the lowest possible game total.

    What was Oliver’s expected final score after Pamela’s second turn?

    [teaser3222]

     
    • Jim Randell's avatar

      Jim Randell 5:27 pm on 21 June 2024 Permalink | Reply

      This Python program looks for possible repeated scores for O and P that could happen in the first 4 turns, that give the required whole number expectations, to give candidate solutions.

      Once candidates are found (there are only two) it then attempts to choose marble values for the turns that give the required score, and any non-viable candidates (one of them) are eliminated.

      It runs in 71ms. (Internal runtime is 656µs).

      from enigma import (irange, isqrt, div, ediv, group, subsets, disjoint_cproduct, disjoint_union, printf)
      
      # calculate the score for a marble
      def score(n):
        for a in irange(isqrt(n), 1, step=-1):
          b = div(n, a)
          if b is not None: return abs(a - b)
      
      # group the marbles by score
      g = group(irange(1, 84), by=score)
      
      # calculate total number of points
      tot = sum(k * len(vs) for (k, vs) in g.items())
      
      # find possible scores available to O and P
      (keysO, keysP) = (list(), list())
      t = 0
      for k in sorted(g.keys()):
        n = len(g[k])
        t += n
        if not (t < 42):
          keysO.append(k)
        if not (t > n + 42):
          keysP.append(k)
      
      # construct example scores for O and P
      def choose(OPs, i=0, ss=list(), used=set()):
        # are we done?
        if not OPs:
          yield ss
        else:
          # choose values for the next score
          s = OPs[0]
          for vs in subsets([keysO, keysP][i], size=2, select='R'):
            if not (sum(vs) == s): continue
            # choose marbles
            for xs in disjoint_cproduct(g[v] for v in vs):
              used_ = disjoint_union([used, xs])
              if used_ is not None:
                yield from choose(OPs[1:], 1 - i, ss + [xs], used_)
      
      # possible scores for O and P
      scoresO = set(sum(ks) for ks in subsets(keysO, size=2, select='R'))
      scoresP = set(sum(ks) for ks in subsets(keysP, size=2, select='R'))
      
      # check expected remaining points
      def check_exp(r, *fs):
        try:
          return list(ediv(r * a, b) for (a, b) in fs)
        except ValueError:
          return None
      
      # consider scores for O's first 2 turns
      for sO in scoresO:
        # expected remaining points after turn 1 (41 remaining turns)
        if not check_exp(tot - sO, (20, 41), (21, 41)): continue
      
        # consider scores for P's first 2 turns
        for sP in scoresP:
          # expected remaining points after turn 2 (40 remaining turns)
          if not check_exp(tot - (sO + sP), (20, 40)): continue
          # expected remaining points after turn 3 (39 remaining turns)
          if not check_exp(tot - (sO + sP + sO), (19, 39), (20, 39)): continue
          # expected remaining points after turn 4 (38 remaining turns)
          es = check_exp(tot - (sO + sP + sO + sP), (19, 38))
          if not es: continue
      
          # check scores can be made twice
          if not any(choose([sO, sP] * 2)): continue
      
          # output solution candidate
          printf("expected total scores: O={tO} P={tP} [sO={sO} sP={sP}]", tO=sO + sO + es[0], tP=sP + sP + es[0])
      

      Solution: Oliver’s expected final score after Pam takes her second turn is 685.

      There are only two candidate turn scores that give the required whole number expectations:

      O scores 52 on turns 1 and 3
      P scores 8 on turns 2 and 4

      and:

      O scores 134 on turns 1 and 3
      P scores 0 on turns 2 and 4

      However a score of 134 can only be made by choosing marbles 53 (score = 52) and 83 (score = 82), so this cannot be repeated in the third turn. Hence the second candidate solution is eliminated.

      But there are many ways to make the scores for the first candidate solution, for example:

      O picks (7, 47); score = 6 + 46 = 52
      → 1230 points remaining; O’s expected total = 652; P’s expected total = 630.

      P picks (3, 27); score = 2 + 6 = 8
      → 1222 points remaining; O’s expected total = 663; P’s expected total = 619.

      O picks (11, 43); score = 10 + 42 = 52
      → 1170 points remaining; O’s expected total = 674; P’s expected total = 608.

      P picks (8, 55); score = 2 + 6 = 8
      → 1162 points remaining; O’s expected total = 685; P’s expected total = 597.

      In fact, when the game ends, P has scored the lowest possible total, which is 92 points, and so O has scored the remaining 1190 points.

      Like

      • Jim Randell's avatar

        Jim Randell 12:59 pm on 22 June 2024 Permalink | Reply

        We can simplify this approach by just keeping a multiset (or bag(!)) of the possible scores, and then we don’t need to worry about the actual marbles chosen at all.

        Run: [ @replit ]

        from enigma import (irange, isqrt, div, ediv, group, multiset, first, subsets, cproduct, call, printf)
        
        # calculate the score for a marble
        def score(n):
          for a in irange(isqrt(n), 1, step=-1):
            b = div(n, a)
            if b is not None: return abs(a - b)
        
        # make a multiset of scores
        scores = multiset.from_seq(score(x) for x in irange(1, 84))
        
        # calculate total number of points
        tot = scores.sum()
        
        # scores for P and O (P has the lowest 42)
        scoresP = multiset.from_seq(first(scores.sorted(), 42))
        scoresO = scores.difference(scoresP)
        
        # possible total scores for O and P in a turn
        turnsO = group(scoresO.subsets(size=2), by=sum)
        turnsP = group(scoresP.subsets(size=2), by=sum)
        
        # check expected remaining points
        def check_exp(r, *fs):
          try:
            return list(ediv(r * a, b) for (a, b) in fs)
          except ValueError:
            return None
        
        # check scores can be made twice
        def check_scores(sO, sP):
          for (ssO, ssP) in cproduct(subsets(xs, size=2, select='R') for xs in [turnsO[sO], turnsP[sP]]):
            if call(multiset.from_dict, ssO + ssP).issubset(scores):
              return True
        
        # consider scores for O's first 2 turns
        for sO in turnsO:
          # expected remaining points after turn 1 (41 remaining turns)
          if not check_exp(tot - sO, (20, 41), (21, 41)): continue
        
          # consider scores for P's first 2 turns
          for sP in turnsP:
            # expected remaining points after turn 2 (40 remaining turns)
            if not check_exp(tot - (sO + sP), (20, 40)): continue
            # expected remaining points after turn 3 (39 remaining turns)
            if not check_exp(tot - (sO + sP + sO), (19, 39), (20, 39)): continue
            # expected remaining points after turn 4 (38 remaining turns)
            es = check_exp(tot - (sO + sP + sO + sP), (19, 38))
            if not es: continue
        
            # check the turn scores can be made twice
            if not check_scores(sO, sP): continue
        
            # output solution candidate
            printf("expected total scores: O={tO} P={tP} [sO={sO} sP={sP}]", tO=sO + sO + es[0], tP=sP + sP + es[0])
        

        Like

      • Frits's avatar

        Frits 11:35 am on 23 June 2024 Permalink | Reply

        Just for fun:

        g = {i: set() for i in range(84)}
        for a in range(1, 85):
          for b in range(a, 0, -1):
            if a * b > 84 or any(a * b in g[i] for i in range(a - b)): continue
            g[a - b].add(a * b)
        g = {k: vs for k, vs in g.items() if vs}    # remove empty items
        

        @Jim, you don’t use the “ss” list so you could just yield True “if not OPs”.

        Like

    • Frits's avatar

      Frits 8:40 pm on 21 June 2024 Permalink | Reply

      There must be a smarter way of getting this solution.
      The program runs in 350ms (PyPy).

       
      from enigma import divisor_pairs
      from itertools import combinations
      
      # determine differences for "most square" integer factorisation
      seq = [sorted(b - a for a, b in divisor_pairs(i))[0] for i in range(1, 85)]
      # total of all scores
      T = sum(seq)
      
      P, O = [], []
      # determine the lowest scores for Pam
      for i, s, in enumerate(sorted(seq), 1):
        if i <= 42:
          P.append(s)
        else:  
          O.append(s)
      
      sols = set()
      # first turn for Oliver
      for o12 in combinations(O, 2):
        # 20/41 * remaining is a whole number
        if (T - (sum_o12 := o12[0] + o12[1])) % 41: continue
        # first turn for Pam
        for p12 in combinations(P, 2):
          # 20/40 * remaining is a whole number
          if (T - (sum_p12 := p12[0] + p12[1]) - sum_o12) % 2: continue
          O_ = O.copy()
          for o in o12:
            O_.remove(o)
          # second turn for Oliver  
          for o34 in combinations(O_, 2):
            # 19/39 * remaining is a whole number
            if (T - (sum_o34 := o34[0] + o34[1]) - sum_o12 - sum_p12) % 39: continue
            # score for their second turn was the same as their first
            if sum_o34 != sum_o12: continue
            
            P_ = P.copy()
            for p in p12:
              P_.remove(p)
            for p34 in combinations(P_, 2):
              remaining = (T - (sum_p34 := p34[0] + p34[1]) - 
                           sum_o12 - sum_p12 - sum_o34)
              # 19/38 * remaining is a whole number
              # score for their second turn was the same as their first
              if remaining % 2 or sum_p34 != sum_p12: continue
              # Oliver's expected final score after Pamela’s second turn
              sols.add(str(sum_o12 + sum_o34 + remaining // 2))
      
      print("answer:", ' or '.join(sols))    
      

      Like

      • Frits's avatar

        Frits 2:16 pm on 22 June 2024 Permalink | Reply

        An easy performance improvement is to use keep2() in the combinations() lines.

        # keep only a maximum of 2 occurences of a sequence item
        keep2 = lambda seq: [b for a in [[x] * min(2, seq.count(x)) 
                             for x in sorted(set(seq))] for b in a]     
        

        Like

    • Frits's avatar

      Frits 7:19 am on 24 June 2024 Permalink | Reply

      from itertools import combinations
       
      seq = []
      for n in range(1, 85):
        # find the two factors nearest to a square
        for a in range(int(n**.5), 0, -1):
          b, r = divmod(n, a)
          if r: continue
          seq.append(b - a) 
          break            
       
      seq.sort()
      T = sum(seq)                 # total of all scores
      assert T % 2 == 0            # whole number expected score after 4th turn
      P, O = seq[:42], seq[42:]    # points for Pam and Oliver
       
      # sum of points of 2 marbles for Oliver that guarantees divisibility by 41
      o_sums = [sm for i in range(5) 
                if sum(O[:2]) <= (sm := (T % 41) + 41 * i) <= sum(O[-2:])]
       
      # indices of 2 marbles for Oliver that guarantees divisibility by 41
      o_pairs = {sm: [(i1, i1 + i2 + 1) for i1, m1 in enumerate(O[:-1]) 
                 for i2, m2 in enumerate(O[i1 + 1:]) if m1 + m2 == sm] 
                 for sm in o_sums}
                 
      # reduce items if possible           
      o_pairs = {k: vs for k, vs in o_pairs.items() if len(vs) > 1}      
      o_sums = [sm for sm in o_sums if sm in o_pairs]    
       
      # valid Oliver/Pam points that also guarantees divisibility by 39
      # after the third turn and divisibility by 2 after the second turn
      op_pts = [(o_pts, p_pts) for o_pts in o_sums 
                if (T - o_pts - (p_pts := (T - 2 * o_pts) % 39)) % 2 == 0]
       
      sols = set()
      # can solutions be formed by picking 4 different marbles per person
      for o, p in op_pts:
        # pick 4 marbles for Oliver
        if not any(len(set(a + b)) == 4 for a, b in combinations(o_pairs[o], 2)):
          continue
         
        # indices of 2 marbles for Pam
        p_pairs = [(i1, i1 + i2 + 1) for i1, m1 in enumerate(P[:-1]) 
                    for i2, m2 in enumerate(P[i1 + 1:]) if m1 + m2 == p]
        
        # pick 4 marbles for Pam
        if not any(len(set(a + b)) == 4 for a, b in combinations(p_pairs, 2)):
          continue
        
        # store Oliver's expected final score after Pamela's second turn
        sols.add(str(T // 2 + o - p))
       
      print("answer:", ' or '.join(sols))       
      

      Like

  • Unknown's avatar

    Jim Randell 4:16 pm on 29 March 2024 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3210: Random rabbit 

    From The Sunday Times, 31st March 2024 [link] [link]

    Every year Easter Bunnies must pass a series of demanding tests, the most important being the double jump. Rabbits perform a single jump comprising two hops, with the total distance scoring. For both hops, Max knows he has an equal chance of jumping any distance between two limits. For the first hop, the limits are 80 and 100cm. For his weaker second hop, these limits decrease but keep the same proportion.

    However, the instructors have increased the required standard from 152 to 163cm. Max is worried; he can still pass, but his chance is half what it was before.

    What, as a percentage, is his chance of passing this year?

    This brings the total number of Teaser puzzles posted on the site to 1024 (= 2^10), which is roughly 1/3 of all published Teaser puzzles.

    [teaser3210]

     
    • Jim Randell's avatar

      Jim Randell 5:47 pm on 29 March 2024 Permalink | Reply

      We can consider the possible jumps to be a region of the cartesian plane between x = [80, 100] (= hop 1) and y = [80f, 100f] (= hop 2), where f ∈ (0, 1) gives the reduction for the second hop.

      The probabilities are then calculated from the proportion of this rectangular region that is above the lines x + y = 152 (last year) and x + y = 163 (this year). (Assuming that hops are uniformly distributed).

      This Python program finds the value of f numerically, such that the second area (dark blue) is half the first area (both blues), and then calculates the corresponding probabilities.

      It runs in 71ms. (Internal runtime is 179µs).

      Run: [ @replit ]

      from enigma import (find_value, fdiv, sq, inf, printf)
      
      # find area where x + y > t
      def A(t, f):
        # where does x + y = t hit y = 80f and 100f
        (x1, x2) = (t - 80 * f, t - 100 * f)
        if x1 < 80:
          # entire area = 20 * 20f
          return 400 * f
        if x2 < 80:
          # remove bottom left corner
          return 400 * f - 0.5 * sq(x1 - 80)
        if x1 < 100:
          # trapezium
          return 20 * f * (10 * f + 100 - x1)
        if x2 < 100:
          # top left corner
          return 0.5 * sq(100 - x2)
        # otherwise, zero area
        return 0
      
      # find ratio of areas for the two scenarios
      def fn(f):
        # area for t = 152
        A1 = A(152, f)
        if A1 == 0: return inf
        # area for t = 163
        A2 = A(163, f)
        # return the ratio A2/A1
        return fdiv(A2, A1)
      
      # find when fn(f) = 0.5, for f in the interval (0, 1)
      r = find_value(fn, 0.5, 0.0, 1.0)
      f = r.v
      
      # calculate probabilities
      T = 400 * f  # total area
      P1 = fdiv(A(152, f), T)
      P2 = fdiv(A(163, f), T)
      
      # output solution
      printf("{P1:.1%} -> {P2:.1%} [f={f:.2f}]")
      

      Solution: Max’s chance of passing this year is 45%.

      The value of f is 0.8, so the range for the second hop is 64 – 80 cm.

      This gives a total area of the rectangle as: 20 × 16 = 320.

      For last year we want the area of the rectangle above the line (80, 72) – (88, 64) which gives: 288.

      For this year we want the area above the line (83, 80) – (99, 64) which gives: 144.

      And so the probability this year (144/320 = 45%) is exactly half of the probability last year (288/320 = 90%).

      Like

    • Frits's avatar

      Frits 7:38 pm on 29 March 2024 Permalink | Reply

      For the time being an approximation, less than half a second with PyPy.
      Starts to slow down quickly when increasing the precision.

      from itertools import product
      
      lmts = [80, 100]
      stds = {2023: 152, 2024: 163}
      fr, to = 52, 79 
      done = 0
      
      m = 1 # multiplication factor
      while not done:
        lmts = [10 * x if m > 1 else x for x in lmts]
        rng1 = range(lmts[0], lmts[1] + 1)
        
        # a = lower limit for 2nd jump
        for a in range(to, fr - 1, -1):
          rng2 = [a * x / lmts[0] for x in rng1]
          # chance of passing per year
          P2023 = sum(x + y >= m * stds[2023] for x, y in product(rng1, rng2)) / (len(rng1) * len(rng2))
          P2024 = sum(x + y >= m * stds[2024] for x, y in product(rng1, rng2)) / (len(rng1) * len(rng2))
          # check if we are close to 50% 
          if abs(P2024 / P2023 - 0.500) <= 1e-3:
            print(f"answer: approximately {round(100 * P2024, 1)}%")
            done = 1
            break
      
          if P2024 / P2023 < 0.5: break  
        
        # try to get closer to 50,00% by increasing the ranges by 10 
        m *= 10
        (fr, to) = (10 * a, 10 * (a + 1))      
      

      Like

      • Frits's avatar

        Frits 2:50 pm on 30 March 2024 Permalink | Reply

        More efficient.

        from math import ceil
         
        lmts = [80, 100]
        stds = {2023: 152, 2024: 163}
        # if 2nd hop is below 52 we can never reach 152
        fr, to = 52, 79
        done = 0
        
        # calculate the chance if we pick one element from r1 and r2 that
        # their sum is greater or equal <std> 
        def calcP(r1, r2, std):
          miss, strt = 0, 0
          ln1, ln2 = len(rng1), len(rng2)
          df1, df2 = rng1[1] - rng1[0], rng2[1] - rng2[0]
          # factor f: f * df2 >= df1 --> f >= df1 / df2
          f = ceil(df1 / df2)
          r2rev = rng2[::-1]
        
          # if x1 + x2 is lower than <std> for this x2 then x1 + (x2 + df2) >= std
          # --> (x1 - df1) + (x2 + df2 + f * df2) >= std  (as f * df2 >= df1)
          # meaning: for the next x1 iteration we can select a subset of rng2
          
          # loop from the high value to low value
          for x1 in rng1[::-1]:
            for j, x2 in enumerate(r2rev[strt:], start=strt):
              if x1 + x2 < std:
                strt = j - f if j - f >= 0 else j - 1 
                # increment number of combinations below the standard
                miss += (ln2 - j)
                # for the next x1 we can start checking x2 as of (x2 + k * df2)
                break
            else:
              continue
              
            # a break has occurred  
            if j == 0:
              # for lower x1's add maximum missing number (ln2)
              miss += (ln2 * (x1 - rng1[0]))
              break
         
          return ((ln1 * ln2) - miss) / (ln1 * ln2)       
          
         
        m = 1 # multiplication factor
        while not done:
          lmts = [10 * x if m > 1 else x for x in lmts]
          rng1 = range(lmts[0], lmts[1] + 1)
           
          # a = lower limit for 2nd jump
          for a in range(to, fr - 1, -1):
            rng2 = [a * x / lmts[0] for x in rng1]
            # chance of passing per year
            P2023 = calcP(rng1, rng2, m * stds[2023]) 
            P2024 = calcP(rng1, rng2, m * stds[2024])
            
            # check if we are close to 50% 
            if abs(P2024 / P2023 - 0.5) <= 1e-4:
              print(f"answer: approximately {round(100 * P2024, 2)}%")
              done = 1
              break
         
            if P2024 / P2023 < 0.5: break 
            
          # try to get closer to 50,00% by increasing the elements by 10
          m *= 10
          (fr, to) = (10 * a, 10 * (a + 1))       
        

        PyPy is a lot faster than CPython. I don’t recall a program where PyPy was more than 17 times quicker.

        D:\Python>pypy RandomRabbit1D.py
        answer: approximately 45.0%
        [temp.py] total time: 1.4625589s (1.46s)
        D:\Python>pyth RandomRabbit1D.py
        answer: approximately 45.0%
        [temp.py] total time: 25.6650562s (25.67s)  
        

        Like

        • Frits's avatar

          Frits 12:11 pm on 1 April 2024 Permalink | Reply

          line 24 can be done more efficiently.

               
          for x1 in [x for x in rng1 if x < std - r2[-0]][::-1]:
          

          The program runs now in 200ms/500ms for PyPy/CPython.

          Like

  • Unknown's avatar

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

    Teaser 3203: Circuit maker 

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

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

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

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

    [teaser3203]

     
    • Jim Randell's avatar

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

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

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

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

      But is this simple design minimal?

      Like

      • Jim Randell's avatar

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

        No, it isn’t.

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

        Like

      • Jim Randell's avatar

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

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

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

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

        Like

        • Mark Valentine's avatar

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

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

          Like

    • Jim Randell's avatar

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

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

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

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

      Like

      • Brian Gladman's avatar

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

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

        Like

        • Jim Randell's avatar

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

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

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

          Like

          • Brian Gladman's avatar

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

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

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

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

            Like

    • Jim Randell's avatar

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

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

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

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

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

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

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


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

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

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

      Solution: The track was constructed using 168 triangles.


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

      Like

    • Hugo's avatar

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

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

      Like

      • Tony Smith's avatar

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

        Station Hairpin, Monaco Grand Prix.

        Like

  • Unknown's avatar

    Jim Randell 4:41 pm on 22 December 2023 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3196: Mind the edge 

    From The Sunday Times, 24th December 2023 [link] [link]

    Kate and Lucy play a pub game on an isosceles triangle table top. They take turns to push a penny from the centre of the table top’s base towards the triangle’s apex, 120cm distant, scoring the sum of their distances from the base, or zero if it ever falls off the table.

    Each player aims to maximise their distance, avoiding the chance that the penny might fall off the table. Their penny has an equal chance of landing anywhere within an error radius around the aimed point. This radius is proportional to the distance aimed. As a skilled player Lucy’s error ratio is half of Kate’s.

    They both expect to score a whole number of cm per push, but to give each an equal chance of winning Kate gets one more push than Lucy. This number of pushes is the largest possible, given the above information.

    How many pushes complete a game between the two?

    Happy Christmas from S2T2!

    For my selection of the more interesting puzzles encountered in 2023 see 2023 in review on Enigmatic Code.

    [teaser3196]

     
    • Jim Randell's avatar

      Jim Randell 5:15 pm on 22 December 2023 Permalink | Reply

      If I understand the puzzle correctly it works like this:

      Suppose the integer distances they are aiming for are L and K (where 0 < K < L < 120), and if L has n turns (so K has (n + 1) turns), then we have:

      n L = (n + 1) K
      ⇒ n = K / (L − K)

      so n is a divisor of K.

      And if L’s target circle has radius r(L):

      r(L) = k L

      for some constant k.

      And K’s circle is given by:

      r(K) = 2k K

      And if θ is half the angle at the apex of the triangle we have:

      sin(θ) = r(L) / (120 − L)
      sin(θ) = r(K) / (120 − K)

      (120 − K) L = 2 K (120 − L)
      K L = 120 (2K − L)
      L = 240 K / (120 + K)

      This Python program finds possible K, L, n values, and looks for maximal n values.

      It run in 55ms (and has an internal runtime of 110µs).

      Run: [ @replit ]

      from enigma import (Accumulator, irange, div, printf)
      
      # find maximum number of goes
      r = Accumulator(fn=max, collect=1)
      
      # consider distance K
      for K in irange(1, 118):
        # calculate L
        L = div(240 * K, 120 + K)
        if L is None or not (L < 120): continue
        # calculate n
        n = div(K, L - K)
        if n is None: continue
        # record maximal n
        printf("[K={K} L={L} n={n}]")
        r.accumulate_data(n, (K, L))
      
      # output solution
      n = r.value
      for (K, L) in r.data:
        printf("K={K} L={L} n={n} -> {t} turns", t=2 * n + 1)
      

      Solution: The total number of turns in the game is 31.

      L has 15 turns, and K has 16.

      Like

    • Frits's avatar

      Frits 11:56 am on 25 December 2023 Permalink | Reply

         
      from enigma import divisors
       
      # consider the distance for Lucy
      for L in range(119, 1, -1):
        # Lucy has n pushes, n = 120 / (120 - L), K = 120L / (240 - L)
        if (120 - L) not in divisors(120) or (120 * L) % ((240 - L)): continue
        
        print(f"answer: {240 // (120 - L) + 1} pushes")
        break # maximum reached
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:24 am on 26 December 2023 Permalink | Reply

        @Frits: A very compact and efficient approach (only 8 cases are considered).

        Here’s a version that prints a bit more information:

        Run: [ @replit ]

        from enigma import (irange, div, printf)
        
        # find values of n in decreasing order
        for L in irange(119, 2, step=-1):
          K = div(120 * L, 240 - L)
          if K is None: continue
          n = div(120, 120 - L)
          if n is None: continue
          # output solutions
          printf("L={L} -> K={K} n={n} -> {t} turns", t=2 * n + 1)
          break  # only need largest n
        

        Like

  • Unknown's avatar

    Jim Randell 4:17 pm on 20 October 2023 Permalink | Reply
    Tags: by: Mark Valentine,   

    Teaser 3187: Wired squares 

    From The Sunday Times, 22nd October 2023 [link] [link]

    Sitting at his study desk one morning, Ted noticed a paperclip poking out of his notebook, unbent to a straight thin wire. Opening it up he found that his granddaughter Jessica had drawn a square grid (less than 14 cm side width) on one of the pages, with vertical and horizontal grid lines at 1 cm intervals.

    Musing this, Ted numbered each cell consecutively from 1, working left to right along each row from top to bottom in turn. Moving the wire over the page, he rested the wire over (or touching the corner of) some cells containing a square number. Placing the wire carefully, he was able to connect as many square-numbered cells as possible in this way. No square grid of less than 14 cm side width could have allowed the connection of a larger number of squares.

    What squares did he connect?

    When originally posted on The Sunday Times website the upper limit was “less than 15 cm”, but this was revised down to “less than 14 cm” when the paper was published.

    [teaser3187]

     
    • Jim Randell's avatar

      Jim Randell 8:06 am on 21 October 2023 Permalink | Reply

      Initially I drew out the various grids and looked for straight lines that could connect cells. (I am assuming the wire forms a sufficiently long straight line).

      This program examines lines that pass through the corners of square-numbered cells (so it may not find all possible lines), but it does better than I did on my manual attempt.

      It runs in 664ms.

      Run: [ @replit ]

      from enigma import (fdiv, inf)
      
      # find the slope and intercept of a line defined by points p1, p2
      def line_slope_intercept(p1, p2, div=fdiv):
        ((x1, y1), (x2, y2)) = (p1, p2)
        if x1 == x2: return (inf, x1)
        m = div(y2 - y1, x2 - x1)
        c = y1 - m * x1
        return (m, c)
      
      
      from enigma import (
        Accumulator, irange, subsets, tuples, line_intersect, catch, uniq,
        unpack, rdiv, seq2str, printf
      )
      
      # return the corners of a square
      corners = lambda x, y: [(x, y), (x + 1, y), (x + 1, y + 1), (x, y + 1)]
      
      # solve for an n x n grid
      # return max hits
      def solve(n, verbose=1):
        # calculate the corners of each square cell
        sqs = dict()
        pts = set()
        for i in irange(1, n):
          k = i * i
          (y, x) = divmod(k - 1, n)
          sqs[k] = (x, y)
          pts.update(corners(x, y))
      
        # find maximal hits
        ss = Accumulator(fn=max, collect=1)
      
        # consider lines that pass through two of the points
        fn = unpack(lambda p1, p2: line_slope_intercept(p1, p2, div=rdiv))
        for (p1, p2) in uniq(subsets(pts, size=2), fn=fn):
          # which squares are hit
          hit = set()
          for (k, (x, y)) in sqs.items():
            for (c1, c2) in tuples(corners(x, y), 2, circular=1):
              r = catch(line_intersect, p1, p2, c1, c2, internal=2, div=rdiv)
              if r is None: continue
              hit.add(k)
              break
      
          ss.accumulate_data(len(hit), tuple(sorted(hit)))
      
        # output information
        if verbose:
          printf("[n={n}]")
          printf("max hits = {ss.value}")
          for hit in uniq(ss.data):
            printf("-> {hit}", hit=seq2str(hit, enc="[]"))
          printf()
      
        # return max hits
        return ss.value
      
      # consider grids up to size 13
      r = Accumulator(fn=max, collect=1)
      for n in irange(1, 13):
        h = solve(n)
        r.accumulate_data(h, n)
      printf("max hits = {r.value} on grids {r.data}")
      

      Solution: The connected cells are: 1, 4, 16, 36, 49, 64.

      My answer to the originally posted puzzle (with an upper limit of “less than 15 cm”) was the same, and is as follows:

      The program finds two grids (up to 14×14) that allow a maximal 6 square-numbered cells to be connected, shown below:

      (Although the program looks for “reasonable” solutions, it is possible there may be a line with more than 6 connections that is not found by the program).

      However the puzzle is looking for a single unique solution.

      The first (13×13) grid needs a wire of around 11.3 cm in length to link the cells, and the second (14×14) grid needs a wire of around 14 cm to link the cells.

      So if the length of wire is between these values, we can link the cells in the 13×13 grid, but not the 14×14 grid.

      I unbent a couple of paperclips, and found the smaller one used around 10 cm of wire, and the big one about 16 cm. So it is plausible that a paperclip could fall between the two required values, leaving the 13×13 grid as the unique answer.

      In the revised puzzle, only the 13×13 grid is considered, so this must provide the answer. (Which makes me wonder why the puzzle has all the stuff about straightening the paperclip at all).


      The [[ line_slope_intercept() ]] function is included in the latest version of enigma.py.

      Like

  • Unknown's avatar

    Jim Randell 4:29 pm on 18 August 2023 Permalink | Reply
    Tags: by: Mark Valentine,   

    Teaser 3178: Drying boards 

    From The Sunday Times, 20th August 2023 [link] [link]

    Chef Ignacio liked to prop his two identical thin rectangular chopping boards against the shelf at the end of his counter to dry. He placed the top of the first one flush with the shelf corner and rested the second on the first, as shown in the diagram. To aid drying, he positioned the second to maximise the air volume in the bounded region below it. The length of each board is an even number of cm (less than 26cm). The distance between the bases of the two boards was a whole number of mm.

    What is this distance?

    It seems that the setter intends that the height of the shelf above the counter is an exact (but not specified) number of mm. Without this requirement we can find multiple potential solutions.

    [teaser3178]

     
    • Jim Randell's avatar

      Jim Randell 5:30 pm on 18 August 2023 Permalink | Reply

      I am assuming the distance between the bases of the boards is the separation between the edges that touch the counter top.

      At the moment I don’t understand how we can work out a unique answer without knowing more information, say, the height of the shelf above the surface of the counter, or the angle of one of the boards.

      Solution: [See my comment below]

      Like

      • Jim Randell's avatar

        Jim Randell 8:26 am on 19 August 2023 Permalink | Reply

        I have now found a single solution using the additional assumption that the height of the shelf above the counter is an exact whole number of millimetres. It seems it is likely this is what the setter intended.

        If the first board makes an angle θ with the surface (0° < θ < 90°), then the maximum cross-sectional area under the second board is achieved when it makes an angle θ/2 with the surface (and the bounded area is an isosceles triangle).

        With the length of the board and the height of the shelf we can calculate the angle θ, and hence θ/2. Then cos(θ/2) is the ratio of half the board length to the required separation.

        I used the half-angle formula:

        \cos \left( \frac{\theta}{2} \right)\; =\; \sqrt{\frac{1\; +\; \cos \left( \theta \right)}{2}}

        This Python program runs in 56ms. (Internal runtime is 1.7ms).

        Run: [ @replit ]

        from enigma import (Rational, irange, is_square_q, div, as_int, printf)
        
        Q = Rational()
        
        # solve for a board of length x (mm)
        for x in irange(20, 240, step=20):
        
          # and a shelf of height h (mm)
          for h in irange(1, x - 1):
        
            # separation between the boards on the counter = y (mm)
            d = is_square_q(x * x - h * h)
            if d is None: continue
            q = is_square_q(Q(d + x, 2 * x))
            if q is None: continue
            y = Q(x, 2 * q)
            # check for integer value
            y = as_int(y, include="+", default=None)
            if y is None: continue
        
            # output solution
            printf("x={x} h={h} -> y={y}")
        

        Solution: The boards are 125 mm apart.

        The boards are 200 mm in length (x = 200), and the shelf is height 192 mm above the counter top (h = 192).

        So the first board makes a (56, 192, 200) = 8× (7, 24, 25) right-angled triangle.

        And we have:

        cos(θ) = 7/25

        θ ≈ 73.74°

        And we calculate cos(θ/2) as:

        cos(θ/2) = √((1 + cos(θ))/2)
        = √((1 + 7/25)/2)
        = √(16/25)
        = 4/5

        θ/2 ≈ 36.87°

        And the required separation is:

        y = (x/2) / cos(θ/2)
        y = 100 / (4/5)
        y = 125


        If the height of the shelf h is unconstrained, then we can find an appropriate value to give any (reasonable) answer we choose.

        For a board of length x and a required separation distance y, we calculate the angle of θ (between 0° and 90°) as:

        cos(θ/2) = x/2y

        Then the required height h for this scenario is given by:

        h = x sin(θ)

        For example:

        x = 240, y = 140

        cos(θ/2) = 6/7
        θ ≈ 62.01°
        h ≈ 211.92 mm

        Like

        • Frits's avatar

          Frits 12:29 pm on 19 August 2023 Permalink | Reply

          I had to make two assumptions, not one, to get to the same answer.

          Luckily my initial derived rule for maximal air volume remains true.

           
          from enigma import Rational
          
          Q = Rational()
          
          M = 26 # length of each board in cm is smaller than M
          
          # air volume in the bounded region is maximal if line perpendicular to
          # the 2nd board splits the 2nd board in half (isoceles triangle)
          
          # board length in mm
          for b in range(20, 10 * M, 20):
            # assume shelf height is a whole number of mm
            for h in range(1, b):
              # assume the distance from corner to 1st board touching 
              # the counter <a> is a whole number of mm
              a = (b**2 - h**2)**(1/2)
              if not (a == (a := int(a))): continue
              # using cosine half-angle formula the following must be true
              d2 = Q(b**2 + a * b, 2 + 4 * Q(a, b) + Q(2 * a**2, b**2))
              if d2.denominator != 1: continue
              
              # distance <d> must be a whole number of mm
              if (d := d2.numerator**(1/2)) != int(d): continue
              print("answer:", int(d), "mm")
          

          Like

          • Frits's avatar

            Frits 10:12 pm on 19 August 2023 Permalink | Reply

            line 17 is not really necessary.

            Like

        • Jim Randell's avatar

          Jim Randell 1:04 pm on 19 August 2023 Permalink | Reply

          For a shorter/faster program we can use [[ pythagorean_triples() ]] from the enigma.py library (and a bit of analysis).

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

          Run: [ @replit ]

          from enigma import (pythagorean_triples, div, is_square, printf)
          
          # generate pythagorean triples for the first board (in mm)
          for (a, b, x) in pythagorean_triples(240):
            if x % 20 != 0: continue
            for (d, h) in [(a, b), (b, a)]:
              # calculate the separation of the boards (in mm)
              y = is_square(div(x**3, 2 * (d + x)))
              if y is None: continue
              # output solution (in mm)
              printf("x={x} h={h} -> y={y}")
          

          Analysis:

          Using the identity:

          2 cos²(θ/2) = 1 + cos(θ)

          we have:

          cos(θ/2) = x / 2y
          cos(θ) = d / x

          hence:

          x²/2y² = 1 + d/x

          y² = x³ / 2(d + x)

          And x and y are integers, hence d is also an integer, so (d, h, x) is a Pythagorean triple.

          Liked by 1 person

  • Unknown's avatar

    Jim Randell 3:59 pm on 2 June 2023 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3167: Binary primes 

    From The Sunday Times, 4th June 2023 [link] [link]

    Grace and Helen play a turn-based card game. Each player is given two cards with a 0 printed on them, and unlimited cards with a 1 printed on them. Before the game starts a 1 is placed on the table. Sitting on the same side of the table, a turn then involves placing a card to the left of the table cards, such that the value shown in binary remains prime or one (allowing leading zeros). The winner is the player who is last able to play a card, earning points equal to the value on the table at that time.

    Being expert logicians, they play the best possible cards to maximise their points or minimise their opponent’s points. Grace goes first.

    Who wins and with what cards on the table from left to right?

    [teaser3167]

     
    • Jim Randell's avatar

      Jim Randell 4:35 pm on 2 June 2023 Permalink | Reply

      This Python program plays the game with each player aiming to maximise their winnings (or minimise their losses) at each stage. So it performs a depth first traversal of the game tree.

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

      Run: [ @replit ]

      from enigma import (Accumulator, is_prime, int2base, compare, printf)
      
      # explore the outcome of this move
      def explore(n, k, a0, b0, rs):
        (rn, rk) = play(n, k, a0, b0)  # play the move
        rs.accumulate_data(-rn, rk)  # swap the result
      
      # play the game with current value <n> from <k> cards
      # player A to move, with <a0> zero cards
      # player B has <b0> zero cards
      # return (<pts>, <k>) [+ve for win, -ve for loss]
      def play(n, k, a0, b0):
        # choose the best move (maximum points)
        rs = Accumulator(fn=max)
        # play 0
        if a0 > 0: explore(n, k + 1, b0, a0 - 1, rs)
        # play 1
        n_ = n + (1 << k)
        if is_prime(n_): explore(n_, k + 1, b0, a0, rs)
        # make the best available move
        if rs.count: return (rs.value, rs.data)
        # if there are no moves, current points go to opponent
        return (-n, k)
      
      # play starting with value 1, on 1 card, and 2 zero cards for each player
      (n, k) = play(1, 1, 2, 2)
      # determine points won and the sequence of cards
      pts = abs(n)
      ss = int2base(pts, base=2, width=k)
      # output solution
      printf("{w} wins, {pts} pts, cards = {ss}", w=compare(n, 0, 'B?A'))
      

      Solution: Helen wins. The cards on the table are: (0 1 0 0 1 1 1 0 1).

      The game proceeds:

      start → (1) = 1
      G chooses 0 → (0 1) = 1
      H chooses 1 → (1 0 1) = 5
      G chooses 1 → (1 1 0 1) = 13
      H chooses 1 → (1 1 1 0 1) = 29
      G chooses 0 → (0 1 1 1 0 1) = 29
      H plays 0 → (0 0 1 1 1 0 1) = 29
      G plays 1 → (1 0 0 1 1 1 0 1) = 157
      H plays 0 → (0 1 0 0 1 1 1 0 1) = 157
      G cannot play; H wins 157 points

      The the final moves (0, 1, 0, out) are forced.

      There are 61 positions in the game tree, 17 of them are terminal positions, and only 2 of these are wins for the player who goes first.

      Like

      • Mark Valentine's avatar

        Mark Valentine 11:11 pm on 5 June 2023 Permalink | Reply

        What does run time vs zeros per person look like? How many zeroes can you reasonably solve for?

        Like

        • Jim Randell's avatar

          Jim Randell 10:43 am on 6 June 2023 Permalink | Reply

          Hi, Mark. Thanks for the puzzle!

          I ran my program for games with up to 28 zero cards per player. The number of positions grows at a reasonable rate, but things start to get a bit slow above z = 20.

          But we can switch to using the Miller-Rabin primality test [[ is_prime_mr() ]] for larger primes, which allows us to check up to z = 55 in under 1s (or [[ gmpy2.is_prime() ]] which is even faster).

           z    pos   winner       pts  runtime
          
           1     17    B            23
           2     61    B           157
           3    130    B            17
           4    237    B           769
           5    376    B          1223
           6    554    B          1223
           7    772    B         65543
           8   1036    B         65537
           9   1353    B         65537
          10   1727    B       2097169
          11   2141    B         65537
          12   2631    B         65537
          13   3158    B         65537
          14   3770    B         65537
          15   4455    B         65537
          16   5220    B       2097169  [101ms]
          17   6029    B         65537  [128ms]
          18   6909    B         65537  [243ms]
          19   7854    B         65537  [482ms]
          20   8870    B         65537  [886ms]
          21   9954    B         65537  [1.96s]
          22  11130    B         65537  [5.34s]
          23  12378    B     268435493  [10.7s]
          24  13736    B         65537  [23.6s]
          25  15160    B         65537  [38.6s]
          26  16663    B  274877906957  [81.6s]
          27  18234    B         65537  [ 161s]
          28  19904    B         65537  [1279s] [107ms with is_prime_mr]
          29  21668    B         65537          [127ms]
          30  23532    B         65537          [156ms]
          31  25463    B         65537          [181ms]
          32  27475    B         65537          [213ms]
          33  29559    B         65537          [225ms]
          34  31731    B         65537          [253ms]
          35  34001    B         65537          [265ms]
          36  36384    B         65537          [292ms]
          37  38847    B         65537          [305ms]
          38  41407    B         65537          [336ms]
          39  44048    B         65537          [346ms]
          

          z = 40 ends with a win for B with 302231454903657293676551 points.

          It doesn’t seem like going first is an advantage. In the z = 2 case only 2 of the 17 terminal positions are wins for A.

          Like

          • Mark Valentine's avatar

            Mark Valentine 3:37 pm on 6 June 2023 Permalink | Reply

            Impressive, it doesn’t blow up as much as I would have thought. I guess the primes becoming increasingly scarce helps.

            Like

    • Frits's avatar

      Frits 3:39 pm on 9 June 2023 Permalink | Reply

      @Jim, if you start with a list of prime numbers is there an upper limit for a prime number that you beforehand can determine?

      Like

      • Jim Randell's avatar

        Jim Randell 5:41 pm on 9 June 2023 Permalink | Reply

        @Frits: Do you mean can we determine what the largest possible value on the table can be without playing the game?

        Each position in the game must be a left-truncatable prime in binary (treating 1 as prime), and as there are a limited number of zeros, it is reasonable to suppose there are only a limited number of such primes. (See: Enigma 1075 for more on truncatable primes).

        With up to 4 zeros there are 28 left-truncatable binary primes, and the largest is:

        01100111101 = 829 [base 10]

        (Although determining this is pretty much the same as playing the game anyway).

        And 829 does indeed show up as the largest possible score for player B in the game tree.

        Like

    • Frits's avatar

      Frits 10:09 am on 11 June 2023 Permalink | Reply

      @Jim, thanks.

      On PuzzlingInPython (see below) I published a program where I start with a list of all prime numbers up to a certain limit where you keep pruning this list until you have all 17 games.

      I hoped to find by logic a limit on the number of consecutive 1’s but in some cases none of the new primes end on a 5 anymore (fi if the last used power of 2 ends on an 8 and your last prime ends on a 1 then your next primes will end on (7, 9, 3, 1, 7, 9, 3, 1, 7, ….) ).

      It was relative easy to generate the 17 games by recursion but picking the winner from this list I found difficult to program.

      Do you also plan to add some kind of binary tree implementation to enigma.py? I guess not that many puzzles you have published can be solved from a binary tree.

      [ https://brg.me.uk/?p=8290#comment-3820 ]

      Like

      • Jim Randell's avatar

        Jim Randell 3:33 pm on 12 June 2023 Permalink | Reply

        @Frits: In these game puzzles I usually find it is easiest to write a recursive function to do a depth first traversal of the game tree. From the current position you can examine all possible moves, and then choose the “best” one for the player, and this means we can determine what the outcome of a game is from the result of the initial position (presuming the players are aiming to maximise their metric at each position). (And in some games we don’t even need to explore the entire tree).

        I’ve done a couple of recent Tantalizer puzzles using this approach (see: Tantalizer 4, Tantalizer 7a), but there are other puzzles where the same approach works.

        This particular puzzle is relatively simple, in that there are at most two plays from any position (playing “0” or “1”), but in other games there may be many more plays available, so the game tree is not always binary.

        I didn’t really think about constructing the graph by starting from the all potential endpoints and then removing those that are not reachable. It seems that this might not be tractable on more complex games as the number of potential positions could be very large, and only a sparse subset of them would be reachable. I think it would be more efficient to build the tree going forward from the starting position.

        Like

  • Unknown's avatar

    Jim Randell 3:57 pm on 7 April 2023 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3159: King coin 

    From The Sunday Times, 9th April 2023 [link] [link]

    The new King’s currency has 64 Minims (circular coins) per Suttas (notes). Each coin’s value is proportional to its face area, and is a whole number of cm in diameter, starting with 1 cm for 1 Minim.

    The King only wanted denominations such that his citizens can pay any amount below 1 Suttas using no more than a certain number of coins for the transaction. This number is the smallest possible, given the above conditions. His mint suggested that if just two values could require an extra coin, they could reduce the number of denominations needed.

    The King agreed and placed one of each minted denomination flat and face up in a rectangular display, with each coin’s edge resting along the display’s base. The order of the coins minimised the width of the display, with the smallest coin to the right of the centre.

    What are the diameters in cm, from left to right?

    [teaser3159]

     
    • Jim Randell's avatar

      Jim Randell 4:59 pm on 7 April 2023 Permalink | Reply

      A bit involved. Once we have found the set of denominations suggested by the mint, we then have to determine the minimal width arrangement of the coins (which is not quite as straightforward as it might first appear).

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

      Run: [ @replit ]

      from enigma import (Accumulator, irange, sq, subsets, sqrt, find, printf)
      
      # the 1 denomination is given
      # possible remaining denominations (squares less than 64)
      ps = dict((sq(i), i) for i in irange(2, 7))
      
      # for each set of coins record values from 1 - 63 that can be made with just
      # k coins, and record the smallest possible maximum value
      ts = set(irange(1, 63))
      d = dict()
      K = Accumulator(fn=min, collect=1)
      for ss in subsets(ps.keys()):
        # make the set of coins
        cs = (1,) + ss
        # values that can be made with up to k coins
        k = 0
        vs = {0}
        r = { k: vs }
        while ts.difference(vs):
          vs = vs.union(ts.intersection(v + x for v in vs for x in cs))
          k += 1
          r[k] = vs
        # record this set of values
        d[cs] = r
        K.accumulate_data(k, cs)
      
      # find the smallest max number of coins required
      # and the sets that achieve this
      (k, css) = (K.value, K.data)
      printf("change in {k} coins = {css}")
      
      # calculate the total width (without overlaps) from a sequence of radii
      dist = lambda a, b: 2 * sqrt(a * b)  # = sqrt(sq(a + b) - sq(a - b))
      def width(rs):
        xs = list()
        for (i, r) in enumerate(rs):
          if i == 0:
            xs.append(r)
          else:
            x = max(xs[j] + dist(a, r) for (j, a) in enumerate(rs[:i]))
            xs.append(x)
        # calculate the width
        (a, b) = (min(x - r for (x, r) in zip(xs, rs)), max(x + r for (x, r) in zip(xs, rs)))
        w = b - a
        # and check the 1 coin is (entirely) in the right half of the arrangement
        i = find(rs, 1)
        if i != -1 and not (xs[i] - 1 > a + 0.5 * w): return None
        return w
      
      # consider sets of coins
      for (cs, r) in d.items():
        # must manage change in (k + 1) coins
        if not (max(r.keys()) == k + 1): continue
        # must be a (strict) subset of one of the sets that achieve change in k coins
        ss = set(cs)
        if not any(len(ss) < len(xs) and ss.issubset(xs) for xs in css): continue
        # find the values that require 5 coins
        xs = r[k + 1].difference(r[k])
        # there must be exactly 2 of them
        if len(xs) != 2: continue
        printf("reduced set = {cs}; change in {k1} coins = {xs}", k1=k + 1, xs=sorted(xs))
      
        # consider possible arrangements of coin radii
        W = Accumulator(fn=min, collect=1)
        for rs in subsets(list(ps.get(x, x) for x in cs), size=len, select='P'):
          w = width(rs)
          if w:
            W.accumulate_data(w, rs)
      
        # output solution
        for rs in W.data:
          printf("-> minimal width arrangement = {rs}")
      

      Solution: The diameters of the coins (left to right) are: 4cm, 2cm, 6cm, 1cm, 3cm.

      So they are arranged as follows:

      Note that the 1cm diameter coin does not contribute to the overall width of the display, as it fits in the gap between the 6cm and 3cm coin, but it is (wholly) in the right hand side of the display.

      The width of the minimum arrangement (distance between the red lines) is: 14.04cm.


      The smallest number of coins for which all change amounts can be given is 4.

      This can be achieved using the following denominations:

      (1, 4, 9, 16, 25, 36)
      (1, 4, 9, 16, 36, 49)
      (1, 4, 9, 16, 25, 36, 49)

      The last of these is a set consisting of all possible coins, and this must be able to change all amounts using the smallest possible number of coins.

      So the King could have suggested any of these sets.

      The mint then suggested a reduced set of coins, which all change amounts can be made in at most 4 coins, except for 51 and 59 (which require 5 coins).

      This set is:

      (1, 4, 9, 16, 36)
      51 = 1× 1 + 2× 9 + 2× 16 (5 coins)
      59 = 3× 9 + 2× 16 (5 coins)


      The names “Minim” and “Suttas” were chosen as together they use the letters from “numismatist” (i.e. a collector of coins).

      >>> multiset("minim" + "suttas") == multiset("numismatist")
      True
      

      Like

    • Brian Gladman's avatar

      Brian Gladman 6:56 pm on 8 April 2023 Permalink | Reply

      Nice work on the “circles width” algorithm Jim. I initially missed the subtleties involved here which make what seems at first to be an easy task surprisingly difficult.

      Like

      • Jim Randell's avatar

        Jim Randell 10:30 pm on 8 April 2023 Permalink | Reply

        Yes, it is quite a tricky one. It took me a couple of goes to come up with a routine to calculate the minimum width correctly, which was not as simple as I had originally thought it would be.

        Like

    • Hugo's avatar

      Hugo 7:27 pm on 16 April 2023 Permalink | Reply

      That’s a relief not to have to keep 49-minim coins in my pocket.
      Even the 36-minim coin is rather large,
      but at least this one isn’t as silly as Teasers 1812 and 2926,
      where the value of a coin is proportional to its diameter.

      We see why, in the real world, it’s more usual to have the value of a coin proportional to its weight (which is likely to rise as the cube of the diameter), and with several series (e.g. copper, nickel-silver, and some other alloy).

      Like

    • Hugo's avatar

      Hugo 1:49 pm on 17 April 2023 Permalink | Reply

      The horizontal distance between the centres of the coins of diameter 6 and 1 is √6 ≈ 2.449.
      The horizontal distance between the centres of the coins of diameter 1 and 3 is √3 ≈ 1.732.
      The horizontal distance between the centres of the coins of diameter 6 and 3 is √18 ≈ 4.263,
      which is slightly greater than the sum of the other two, so the smallest coin has room to rattle.
      Add on √8 ≈ 2.828 and √12 ≈ 3.464 and the radii 2 and 1.5 of the outer coins,
      and we get about 14.035 between Jim’s red lines (all in cm, of course).

      It’s basically a simple calculation, thanks to Pythagoras,
      but I still managed to get the wrong configuration.
      The clever bit, I reckon, was spotting the anagram of numismatist.

      Like

  • Unknown's avatar

    Jim Randell 4:34 pm on 3 February 2023 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3150: Pyramids of Wimbledon 

    From The Sunday Times, 5th February 2023 [link]

    Edward, the sports shop owner, had an annual display of tennis balls. He arranged the balls in four identical pyramids, with a square base holding each in place (one ball on the top of each pyramid, four on the layer below, nine below that and so on).

    However, this year he wanted to display the same number of balls but reduce the total footprint of the bases by at least 55 per cent, to allow for other stock. His son Fred suggested arranging all the balls in one large pyramid with an equilateral triangular base (one ball on the top, three on the layer below, six below that and so on). Edward realised that this would work, but if there were any fewer balls, it wouldn’t work.

    How many balls did Edward display?

    [teaser3150]

     
    • Jim Randell's avatar

      Jim Randell 5:21 pm on 3 February 2023 Permalink | Reply

      I took the footprint of the base to be the area of the enclosing polygon (so a square for the square based pyramids, and an equilateral triangle for the tetrahedral pyramids). And we are looking for a reduction of at least 55%, so the footprint of the tetrahedral pyramid must be not more than 45% that of the total footprint of the four square based pyramids.

      This Python program finds the solution constructively. It runs in 53ms. (Internal runtime is 134µs).

      Run: [ @replit ]

      from enigma import (sqrt, sq, tri, fdiv, printf)
      
      r3 = sqrt(3)
      
      # generate pyramid numbers, base defined by <fn>
      def pyramid(fn):
        n = 0  # number of layers
        b = 0  # number of balls in base
        t = 0  # total number of balls
        while True:
          yield (n, b, t)
          n += 1
          b = fn(n)
          t += b
      
      sq_pyr = pyramid(sq)  # square pyramidal numbers
      tri_pyr = pyramid(tri)  # tetrahedral numbers
      
      # find number of layers for a tetrahedral pyramid
      (cache, max_tt) = (dict(), -1)
      
      # consider number of balls in a square based pyramid
      for (sn, sb, st) in sq_pyr:
        # the puzzle text implies there are at least 3 layers
        if sn < 3: continue
        # total number of balls in the 4 square based pyramids
        t = 4 * st
      
        # can we find a tetrahedral number with t balls?
        while max_tt < t:
          (tn, tb, tt) = next(tri_pyr)
          cache[tt] = (tn, tb, tt)
          max_tt = tt
        rs = cache.get(t)
        if rs is None: continue
        (tn, tb, tt) = rs
      
        # calculate the ratio of the footprints (= enclosing polygons)
        f = fdiv(0.25 * r3 * sq(tn + r3 - 1), 4 * sq(sn))
        if f > 0.45: continue
        # output solution
        printf("4x {sn}-layer SP = {t} balls -> 1x {tn}-layer tetra [f = {f:0.3f}]")
        break
      

      Solution: The display consisted of 9880 balls.

      Each square pyramid consisted of 19 layers, giving 2470 balls, and altogether the 4 pyramids use 9880 balls.

      The single tetrahedral pyramid has 38 layers, which also uses 9880 balls.


      Analytically:

      The formulae for the square pyramidal numbers (SP[n]) and tetrahedral numbers (Te[n]) are:

      SP[n] = n(n + 1)(2n + 1)/6
      Te[n] = n(n + 1)(n + 2)/6

      And we see:

      Te[2n] = 2n(2n + 1)(2n + 2)/6
      Te[2n] = 4 n(n + 1)(2n + 1)/6
      Te[2n] = 4 SP[n]

      So we are looking for the smallest value where the footprint of Te[2n] is not more than 0.45 the footprint of 4× SP[n].

      The footprints can be calculated as follows:

      fpSP(n) = n²
      fpTe(n) = (n + √3 − 1)² (√3/4)

      Hence:

      fpTe(2n) / 4.fpSP(n) ≤ f
      (√3 − 1) / n ≤ r − 2; where: r = 4 √(f / √3)
      n = ceil((√3 − 1) / (r − 2))

      For f = 0.45 we get n = 19.

      So the answer occurs with 4 square based pyramids with 19 layers, or 1 tetrahedral pyramid with 38 layers.

      Each display uses 9880 balls.

      Like

  • Unknown's avatar

    Jim Randell 4:46 pm on 9 September 2022 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3129: Bounce count 

    From The Sunday Times, 11th September 2022 [link] [link]

    At the local arcade, Claire and David played an air hockey game, using a square table with small pockets at each corner, on which a very small puck can travel 1m left-right and 1m up-down between the perimeter walls. Projecting the puck from a corner, players earn a token for each bounce off a wall, until the puck drops into a pocket.

    In their game, one puck travelled 1m further overall than its left-right distance (and the other, travelled 2m further). Claire’s three-digit number of tokens was a cube, larger than David’s number which was triangular (1 + 2 + 3 + …). Picking up an extra token, they found they could split their collection into two piles, one consisting of a cube number of tokens and the other a square.

    How many tokens did they end up with?

    I’ve modified the wording slightly to remove a typo and improve clarity.

    [teaser3129]

     
    • Jim Randell's avatar

      Jim Randell 5:23 pm on 9 September 2022 Permalink | Reply

      With this kind of puzzle it is easier to reflect the table, rather than reflecting the puck. (Assuming the puck bounces in a well behaved fashion). See: Teaser 1917.

      If a play has x bounces off the left/right sides, and y bounces of the top/bottom sides, then in order for the play to be viable, (x + 1) and (y + 1) must be coprime, and:

      score = x + y
      left/right distance = x + 1
      top/bottom distance = y + 1
      distance travelled, d = hypot(x + 1, y + 1)

      It seems we need to find scores C (a cube) and D (a triangular number) such that (C + D + 1) can be expressed as the sum of a cube and a square.

      This Python program runs in 71ms. (Internal run time is 14.6ms).

      Run: [ @replit ]

      from enigma import (
        coprime_pairs, is_square, sq, is_cube,
        is_triangular, cproduct, powers, inf, printf
      )
      
      # generate (x, y, z) values, where z is d - x
      def generate():
        # consider coprime pairs
        for (a, b) in coprime_pairs(200):
          d = is_square(sq(a) + sq(b))
          if d is None: continue
          for (x, y) in [(a, b), (b, a)]:
            z = d - x
            if z in {1, 2}:
              yield (x - 1, y - 1, z)
      
      # find possible values for C and D
      (Cs, Ds) = (list(), list())
      for (x, y, z) in generate():
        t = x + y
        if 99 < t < 1000 and is_cube(t):
          Cs.append((x, y, z, t))
        if t < 1000 and is_triangular(t):
          Ds.append((x, y, z, t))
      
      # can total t be decomposed into a square and a cube?
      def is_sq_cb(t):
        for c in powers(1, inf, 3):
          s = t - c
          if s < 1: return False
          if is_square(s): return True
      
      # choose values for C and D
      for ((Cx, Cy, Cz, Ct), (Dx, Dy, Dz, Dt)) in cproduct([Cs, Ds]):
        T = Ct + Dt + 1
        # Cz/Dz are different; Ct > Dt; T is a square + a cube
        if Cz != Dz and Dt < Ct and is_sq_cb(T):
          # output solution
          printf("T={T} [C=(x={Cx} y={Cy} z={Cz} t={Ct}); D=(x={Dx} y={Dy} z={Dz} t={Dt})]")
      

      Solution: They ended up with a total of 171 tokens.

      C won 125 (= 5^3) tokens on her go. The puck made 111 left/right bounces and 14 up/down bounces. The left/right distance travelled was 112m and the total distance travelled was 113m. So C’s overall distance was 1m more than the total left/right distance.

      D won 45 (= tri(9)) tokens on his go. The puck made 34 left/right bounces and 11 up/down bounces. The left/right distance travelled was 35m and the total distance travelled was 37m. So D’s overall distance was 2m more than the total left/right distance.

      Combining their tokens with 1 extra token give 125 + 45 + 1 = 171 tokens in total.

      And 171 = 144 + 27 = 12^2 + 3^3.

      Like

      • Jim Randell's avatar

        Jim Randell 9:33 pm on 9 September 2022 Permalink | Reply

        Faster (and a bit shorter) to use [[ pythagorean_triples() ]] to generate the (x, y, z) values. And we don’t need to check the coprime condition if we just look at primitive pythagorean triples.

        This Python program runs in 54ms. (Internal run time is 579µs).

        Run: [ @replit ]

        from enigma import (
          pythagorean_triples, is_square, is_cube, is_triangular,
          cproduct, powers, inf, ifirst, lt, printf
        )
        
        # generate (x, y, z) values, where z is d - x
        def generate():
          for (a, b, c) in pythagorean_triples(1001, primitive=1):
            z = c - b
            if z in {1, 2}:
              yield (b - 1, a - 1, z)
        
        # find possible values for C and D
        (Cs, Ds) = (list(), list())
        for (x, y, z) in generate():
          t = x + y
          if 99 < t < 1000 and is_cube(t):
            Cs.append((x, y, z, t))
          if t < 1000 and is_triangular(t):
            Ds.append((x, y, z, t))
        
        # can total t be decomposed into a square and a cube?
        def is_sq_cb(t):
          return any(is_square(t - c) for c in ifirst(powers(1, inf, 3), count=lt(t)))
        
        # choose values for C and D
        for ((Cx, Cy, Cz, Ct), (Dx, Dy, Dz, Dt)) in cproduct([Cs, Ds]):
          T = Ct + Dt + 1
          # Cz/Dz are different; Ct > Dt; T is a square + a cube
          if Cz != Dz and Dt < Ct and is_sq_cb(T):
            # output solution
            printf("T={T} [C=(x={Cx} y={Cy} z={Cz} t={Ct}); D=(x={Dx} y={Dy} z={Dz} t={Dt})]")
        

        Like

        • Frits's avatar

          Frits 9:48 am on 10 September 2022 Permalink | Reply

          @Jim, how do you guarantee one puck travelled 1m and the other 2m?

          Like

        • Frits's avatar

          Frits 9:54 am on 10 September 2022 Permalink | Reply

          Can’t both C and D not have a difference of 1 m? or both 2m?

          Like

          • Jim Randell's avatar

            Jim Randell 9:55 am on 10 September 2022 Permalink | Reply

            No. Line 30 ensures the z values are different.

            Like

            • Frits's avatar

              Frits 10:00 am on 10 September 2022 Permalink | Reply

              @you are right, I only saw line 10. I may have overlooked it as I use z as the hypotenuse.

              Like

    • Frits's avatar

      Frits 8:31 pm on 9 September 2022 Permalink | Reply

         
      from itertools import product
      from enigma import is_square, is_cube, is_triangular
      
      # can number <n> be decomposed into a square and a cube?
      def check(n):
        for i in range(int(n**(1/3)) + 1):
          if is_square(n - i**3): 
            return True
        return False  
      
      g1 = []
      g2 = []
      # travelled distance is hypothenuse z
      # for one person one side is z - 1, for the other person it is z - 2
      for z in range(1, 1003):
        # store sides of a right triangle (x, y, z) if x + y - 2 < 1000 
        # z**2 - (z - 1)**2 = 2z - 1
        if (rt := is_square(2 * z - 1)) and (z - 1 + rt - 2) < 1000:
          g1.append((z, z - 1, rt))
        # z**2 - (z - 2)**2 = 4(z - 1) 
        if (rt := is_square(4 * (z - 1))) and (z - 2 + rt - 2) < 1000:
          g2.append((z, z - 2, rt))  
      
      # check if number of earned tokens x + y - 2 is cube or triangular
      g1 = [(x + y - 2, (c, t), (z, x, y)) for z, x, y in g1 
            if ((c := is_cube(x + y - 2)) and x + y - 2 > 99) or 
              (t := is_triangular(x + y - 2))]    
      g2 = [(x + y - 2, (c, t), (z, x, y)) for z, x, y in g2 
            if ((c := is_cube(x + y - 2)) and x + y - 2 > 99) or 
              (t := is_triangular(x + y - 2))]     
      
      # check all combinations
      for p1, p2 in product(g1, g2):
        # there should be at least one cube and one triangular number
        if any(p1[1][i] == p2[1][i] == None for i in range(2)): continue
        # cube should be higher than triangular number
        if p1[0] == p2[0]: continue
        if p1[0] > p2[0] and p1[1][0] == None: continue
        if p2[0] > p1[0] and p2[1][0] == None: continue
        
        # can we arrange all their tokens plus one into a cube and a square?
        if check(p1[0] + p2[0] + 1):
          print("answer:", p1[0], "and", p2[0])
      

      Like

      • Frits's avatar

        Frits 11:31 am on 10 September 2022 Permalink | Reply

        Easier to read.

           
        from enigma import is_square, is_cube, is_triangular, ceil
        
        # can number <n> be decomposed into a square and a cube?
        def check(n):
          for i in range(1, ceil(n**(1/3))):
            if is_square(n - i**3): 
              return True
          return False  
        
        # travelled distance is hypothenuse z of a right triangle
        # for one person one side is z - 1, for the other person it is z - 2
        
        cubes, tris = [], []
        for z in range(1, 1003):
          # 1m or 2m further
          for f in range(1, 3):
            # calculate other side (besides z and z - f)
            other = is_square(2 * f * z - f**2)          # z**2 - (z - f)**2
            if not other: continue
            # for a right triangle (x, y, z) we earn x + y - 2 tokens
            tkns = z - f + other - 2
            if is_cube(tkns) and tkns > 99:
              cubes.append((tkns, f, (z - f, other))) 
            if is_triangular(tkns):
              tris.append((tkns, f, (z - f, other))) 
        
        # check all combinations
        for c in cubes:
          for t in tris:
            # cube larger than triangular and using both 1m further and 2m further
            if t[0] >= c[0] or t[1] == c[1]: continue
            # can we arrange all their tokens plus one into a cube and a square?
            if check(c[0] + t[0] + 1):
              print("answer:", c[0], "and", t[0])  
        

        Like

        • Jim Randell's avatar

          Jim Randell 3:38 pm on 10 September 2022 Permalink | Reply

          @Frits: Yes, I think that is much clearer.

          Although I don’t see a check to ensure that the left/right and up/down distances are coprime. This is necessary for a valid path. If this is not the case the puck will hit a pocket before it reaches the end of the path.

          Like

          • Frits's avatar

            Frits 9:14 pm on 10 September 2022 Permalink | Reply

            You are right, I had to verify the coprime thing with the following graph.
            If both distances share a factor then there is at least one point on the hypothenuse (besides the end points) where both x and y are whole numbers (and thus the puck drops prematurely in a pocket).

             
              |               f(x) = b - (x * b) / a   
            b-|               suppose b and a share factor k, k > 1
              |\              so a = k * a2, b = k * b2
              | \
              |  \            f(x) = b - (x * b2 / k) / (a2 / k) 
              |   \           if x = a2 then f(x) = b - b2 (integer)
              |    \
              +---------
                    a
            

            Like

  • Unknown's avatar

    Jim Randell 4:26 pm on 25 February 2022 Permalink | Reply
    Tags: by: Mark Valentine   

    Teaser 3101: Lap progression 

    From The Sunday Times, 27th February 2022 [link] [link]

    In a charity fundraising drive, two friends completed laps of their local track. Alan, a cyclist, raised £1 for his first lap, £2 for the second, £3 for the third and so on, with each extra lap earning £1 more than the prior. Bob, who walks, raised £1 for his first lap, £2 for the second, £4 for the third and so on, with each extra lap earning double the prior. Starting together, they travelled at constant speeds, and finished their last lap simultaneously.

    After they had added up their totals, they were surprised to find they each raised an identical four-figure sum.

    Including the beginning and end of their drive, how many times did Alan and Bob cross the lap start-finish line together?

    [teaser3101]

     
    • Jim Randell's avatar

      Jim Randell 5:28 pm on 25 February 2022 Permalink | Reply

      We are looking for 4-digit numbers that are both triangular and one less than a power of 2.

      This Python program runs in 46ms.

      Run: [ @replit ]

      from enigma import (irange, inf, is_triangular, gcd, printf)
      
      # consider powers of 2
      p = 1
      for n in irange(1, inf):
        p *= 2
        t = p - 1
        # we are looking for a 4-digit total
        if t < 1000: continue
        if t > 9999: break
        # that is also triangular
        r = is_triangular(t)
        if r is None: continue
        # calculate number of coincident boundaries
        g = gcd(n, r) + 1
        printf("{t} = 2^({n})-1 = T[{r}] -> {g} times")
      

      Solution: Alan and Bob were together at the start/finish line 7 times.


      Manually:

      There are only four 4-digit numbers that are 1 less than a power of 2:

      1023 = 2^10 − 1
      2047 = 2^11 − 1
      4095 = 2^12 − 1
      8191 = 2^13 − 1

      The triangular root of a number is given by trirt(x) = (√(8x + 1) − 1) / 2, and can be easily calculated for these 4 numbers:

      trirt(1023) = 44.735…
      trirt(2047) = 63.486…
      trirt(4095) = 90
      trirt(8191) = 127.493…

      Hence the solution occurs when Bob completes 12 laps and Alan 90 laps.

      We can then calculate the number of times they are at the start/finish together:

      gcd(12, 90) + 1 = 7


      The puzzle also works (but with a different answer) if Bob’s progression is the odd numbers, 1, 3, 5, 7, 9, ….

      Like

    • GeoffR's avatar

      GeoffR 12:22 pm on 2 March 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Four figure sums for Alan and Bob
      var 1000..9999:A;  
      var 1000..9999:B;
      
      var 45..140:a; % triangular number range
      var 10..13:b;  % power of 2 range
      var 1..13:laps; % times crossing start/finish line together
      
      constraint A == a * (a + 1) div 2 ;
      constraint B =  pow(2, b) - 1 ;
      constraint A == B;
      
      % reusing Hakan's GCD Function
      function var int: gcd(var int: x, var int: y) =
        let {
          int: p = min(ub(x),ub(y));
          var 1..p: g;
          constraint
             exists(i in 1..p) (
                x mod i = 0 /\ y mod i = 0
                /\
                forall(j in i+1..p) (
                   not(x mod j = 0 /\ y mod j = 0)
                ) /\ g = i);
        } in g;
        
      % Times Alan and Bob cross the lap start-finish line together
      constraint laps == gcd(a, b) + 1;
      
      solve satisfy;
      
      output ["No. of coincident start/finish crossings = " ++ show(laps)];
      
      

      Liked by 1 person

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