Tagged: corrected Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:17 pm on 20 October 2023 Permalink | Reply
    Tags: , corrected   

    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 8:34 am on 12 February 2023 Permalink | Reply
    Tags: , corrected   

    Brain-Teaser 931: Seven primes for seven brothers 

    From The Sunday Times, 25th May 1980 [link]

    Large families often have problems, particularly when it comes to deciding who has first choice, etc.

    My six brothers and I solved this particular one by devising our own dice game. Each of us in turn would roll a die four times and put the results together in that order to form a four-figure number. (So that, for example: 6 followed by 2, then 3 and 1, gives the number 6231). We would each do this and the one of us getting the highest four-figure number would be declared the winner.

    One such game had some very strange results. Each of us got a different prime number, using the same four different digits. The average of the seven primes was a perfect square, to which Jon’s number was the nearest.

    The difference between Ron’s and Don’s numbers, multiplied by one of the numbers from the die, gave the difference between Len’s and Ken’s numbers.

    Omitting Ben’s score, the total could have been formed by throwing the die five times. And so could the total of the numbers thrown by Len, Ken, Ron and me.

    What was my score, and who ate the biggest piece of apple pie?

    The originally published version of this puzzle was flawed, so the version above was taken from the book Microteasers (1986), in which it was noted:

    There is an interesting postscript to [this] puzzle. When it first appeared, the condition “each of us got a different prime number using the same four different digits” was given as “each of us got a different prime number; two digits never came up”. That allowed numbers like 5651, which was not the intention at all. The odd thing is that all the readers who sent in answers found the intended solution anyway. So perhaps, even if one relaxes the condition about all the primes using four different digits, the teaser still has a unique solution. The keener reader might like to adapt the program to take four digits in turn and see how many primes can be made from those four (but not necessarily using all four each time). Then you have to consider all possible sets of seven from those. We leave this much harder problem as an exercise.

    However the original formulation of the puzzle does not give a unique solution.

    [teaser931]

     
    • Jim Randell's avatar

      Jim Randell 8:36 am on 12 February 2023 Permalink | Reply

      The original formulation of the puzzle is flawed, so I used the version from the Microteasers (1986) book.

      The following Python program runs in 58ms. (Internal runtime is 1.2ms).

      Run: [ @replit ]

      from enigma import (irange, primes, nconcat, nsplit, subsets, div, is_square, diff, printf)
      
      # digits on a die
      digits = set(irange(1, 6))
      
      # check the numbers <ns> form a valid solution
      def check(ns):
        # total must be > 4-digit
        t = sum(ns)
        if t < 10000: return
        mx = max(ns)
        # calculate the mean
        m = div(t, len(ns))
        if m is None or not is_square(m): return
        # J is the closest to the mean
        fn = lambda n: abs(n - m)
        ns = sorted(ns, key=fn)
        J = ns.pop(0)
        if not fn(J) < fn(ns[0]): return
        # choose R, D values
        for (R, D) in subsets(ns, size=2):
          d = abs(R - D)
          # find another pair that is a multiple of d for L, K
          for (L, K) in subsets(diff(ns, {R, D}), size=2):
            x = div(abs(L - K), d)
            if x not in digits: continue
            # remaining pair are B and M
            for (B, M) in subsets(diff(ns, {R, D, L, K}), size=2, select="P"):
              # total less B can be expressed with 5 dice
              ds = nsplit(t - B)
              if not (len(ds) == 5 and digits.issuperset(ds)): continue
              # as can L + K + R + M
              for (R_, D_) in [(R, D), (D, R)]:
                ds = nsplit(L + K + R_ + M)
                if not (len(ds) == 5 and digits.issuperset(ds)): continue
                # output solution
                printf("J={J} R={R_} D={D_} L+K={L}+{K} B={B} M={M} [m={m} d={d} x={x}]")
                n2t = { J: 'Jon', R_: 'Ron', D_: 'Don', L: '{LK}en', K: '{KL}en', B: 'Ben', M: 'Me'}
                printf("-> Me = {M}; max = {mx}", mx=n2t[mx])
      
      # only 4 digits are used
      for ds in subsets(digits, size=4, fn=set):
        # collect possible primes
        ps = list(n for n in subsets(ds, size=len, select="P", fn=nconcat) if primes.is_prime(n))
        # choose 7 of the primes
        for ns in subsets(ps, size=7):
          # and check for a solution
          check(ns)
      

      Solution: The setter scored 4153. And Don got the highest score.

      There is only one set of 4 dice digits that can form 7 primes, and this set gives exactly seven primes, so we just need to distribute them amongst the brothers according to the given conditions.

      The scores are:

      Len = 1543
      Ken = 1453
      Jon = 3541
      Me = 4153
      Ben = 4513
      Ron = 5413
      Don = 5431

      (Although Ken and Len can swap scores and still give a valid solution).

      The mean value is: 3721 = 61², and Jon has the closest value to this.

      The difference between Ron and Don is 18. And the difference between Len and Ken is 90 = 5 × 18.

      The total without Ben is 21534, which uses 5 dice digits.

      The total of Len, Ken, Ron and Me is 12562, which also uses 5 dice digits.


      However, with the formulation of the puzzle (as originally published in The Sunday Times) there are many further solutions, which the program above can be adapted to find.

      For example, the following primes use only the digits: 1, 3, 5, 6:

      Ben = 1151
      Jon = 5153
      Len = 5651
      Ron = 6133
      Ken = 6311
      Don = 6353
      Me = 6551

      The mean value is: 5329 = 73², and Jon has the closest value to this.

      The difference between Ron and Don is 220. And the difference between Len and Ken is 660 = 3 × 220.

      The total without Ben is: 36152, which uses 5 dice digits.

      The total of Len, Ken, Ron and Me is: 24646, which also uses 5 dice digits.

      And in this case the answer would be that the setters score is 6551, and this is also the highest of the scores.

      Like

    • Hugh Casement's avatar

      Hugh Casement 7:15 pm on 13 February 2023 Permalink | Reply

      There are only 32 four-digit primes that use just the digits 1 to 6 with no repeat.
      And the subset forming the solution to this puzzle is the only one with seven members.
      There is one with six members: 1523, 2153, 2351, 2531, 3251, 5231.
      I haven’t tried constructing a puzzle with those numbers.

      Like

    • GeoffR's avatar

      GeoffR 8:50 am on 15 February 2023 Permalink | Reply

      I found the 32 no. 4-digit primes and divided them into sets of primes with the same 4 digits. For a dice range of (1..6), all primes must end in 1 or 3.

      There was 1 set of 7 primes, 1 set of 6 primes, 2 sets of 4 primes, 2 sets of 3 primes, 2 sets of 2 primes, and 1 set of 1 prime.

      {4231, 2341, 2143, 1423}
      {4513, 5413, 1543, 1453, 3541, 5431, 4153} – 7 no. primes in this set
      {2531, 2153, 2351, 5231, 1523, 3251}
      {4523, 4253, 2543}
      {3461, 6143}
      {6421, 4621, 4261}
      {4561, 4651, 6451, 5641}
      {6521, 5261}
      {5623}

      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int : primes = {4513, 5413, 1543, 1453, 3541, 5431, 4153};
      
      var primes: Len; var primes: Ken; var primes: Jon; var primes: Me;
      var primes: Ben; var primes: Ron; var primes: Don;
      
      constraint all_different([Len, Ken, Jon, Me, Ben, Ron, Don]);
      
      predicate is_sq(var int: y) =
      exists(z in 1..ceil(sqrt(int2float(ub(y))))) (z*z = y );
      
      % The average of the seven primes was a perfect square.
      constraint is_sq((4513 + 5413 + 1543 + 1453 + 3541 + 5431 + 4153) div 7);
      var 3000..4000: average = (4513 + 5413 + 1543 + 1453 + 3541 + 5431 + 4153) div 7;
      
      % The difference between Ron’s and Don’s numbers, multiplied by one of the numbers
      % from the die, gave the difference between Len’s and Ken’s numbers.
      var 1..300:d1; var 1..6:m;  % difference d1 and multiplier m from dice values.
      
      constraint abs (Ron - Don) == d1;
      constraint d1 * m == (Len - Ken);
      
      % The total without Ben uses 5 dice digits.
      var int: total = 4513 + 5413 + 1543 + 1453 + 3541 + 5431 + 4153; 
      
      var 20000..26000: tot_minus_Ben = total - Ben;
      
      % Digits of (total - Ben)
      var 1..6:a; var 1..6:b; var 1..6:c; var 1..6:d; var 1..6:e;
      
      constraint tot_minus_Ben div 10000 == a /\ a in {1,2,3,4,5,6};
      constraint tot_minus_Ben div 1000 mod 10 == b /\ b in {1,2,3,4,5,6};
      constraint tot_minus_Ben div 100 mod 10 == c /\ c in {1,2,3,4,5,6};
      constraint tot_minus_Ben div 10 mod 10 == d /\ d in {1,2,3,4,5,6};
      constraint tot_minus_Ben mod 10 == e /\ e in {1,2,3,4,5,6};
      
      % The total of Len, Ken, Ron and Me also uses 5 dice digits.
      var 1..6:f; var 1..6:g; var 1..6:h; var 1..6:i; var 1..6:j;
      
      var 12000..20000: LKRMe = Len + Ken + Ron + Me;
      
      constraint LKRMe div 10000 == f /\ f in {1,2,3,4,5,6};
      constraint LKRMe div 1000 mod 10 == g /\ g in {1,2,3,4,5,6};
      constraint LKRMe div 100 mod 10 == h /\ h in {1,2,3,4,5,6};
      constraint LKRMe div 10 mod 10 == i /\ i in {1,2,3,4,5,6};
      constraint LKRMe mod 10 == j /\ j in {1,2,3,4,5,6};
      
      solve minimize( abs(Jon - average));
      
      output ["[Len, Ken, Jon, Me, Ben, Ron, Don] = " 
      ++ show([Len, Ken, Jon, Me, Ben, Ron, Don]) 
      ++ "\n" ++ "[a,b,c,d,e ] = " ++ show( [a,b,c,d,e] )
      ++ "\n" ++ "[f,g,h,i,j ] = " ++ show( [f,g,h,i,j] ) 
      ++ "\n" ++ "Average = " ++ show(average)];
      
      % [Len,   Ken, Jon,   Me,   Ben, Ron,  Don] = 
      % [1543, 1453, 3541, 4153, 4513, 5413, 5431]
      % [a,b,c,d,e ] = [2, 1, 5, 3, 4]
      % [f,g,h,i,j ] = [1, 2, 5, 6, 2]
      % Average = 3721
      % ----------
      % ==========
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:37 am on 16 January 2023 Permalink | Reply
    Tags: , corrected   

    Teaser 2709: Around America 

    From The Sunday Times, 24th August 2014 [link] [link]

    The 51 states of America have two-letter abbreviations:

    AK, AL, AR, AZ, CA, CO, CT, DC, DE, FL, GA, HI, IA,
    ID
    , IL, IN, KS, KY, LA, MA, MD, ME, MI, MN, MO, MS,
    MT
    , NC, ND, NE, NH, NJ, NM, NV, NY, OH, OK, OR,
    PA
    , RI, SC, SD, TN, TX, UT, VA, VT, WA, WI, WV, WY.

    I have written as many different letters as possible around a circle such that, if you look at any adjacent pair of letters, then clockwise they are one of the above abbreviations.

    One of my letters is R.

    Starting at R, list the letters clockwise.

    In the book The Sunday Times Brain Teasers Book 2 (2020), the abbreviation NI is given instead of NJ (which I believe was how the puzzle was originally published), but this has been corrected on The Sunday Times website.

    [teaser2709]

     
    • Jim Randell's avatar

      Jim Randell 11:39 am on 16 January 2023 Permalink | Reply

      (See also: Enigma 1754).

      The USA has 50 states and 1 federal district (DC), giving us 51 abbreviations in all. Although none of them are NI, but this was probably a typo for NJ (New Jersey), as it has been corrected on The Sunday Times website. And we get the same solution if NJ or NI are used.

      This Python program runs in 52ms. (Internal runtime is 250µs).

      Run: [ @replit ]

      from enigma import (group, item, Accumulator, printf)
      
      states = str.split(
        "AK AL AR AZ CA CO CT DC DE FL GA HI IA ID IL IN KS "
        "KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV "
        "NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY"
      )
      
      # map letters to next letters
      g = group(states, by=item(0), f=item(1), fn=set)
      
      # generate possible circular chains of letters
      def solve(s):
        # possible next letters
        xs = g.get(s[-1])
        if not xs: return
        # does the sequence form a chain?
        if len(s) > 1 and s[0] in xs:
          yield s
        # try to extend the sequence
        for x in xs.difference(s):
          yield from solve(s + x)
      
      # find maximal chains starting with 'R'
      r = Accumulator(fn=max, collect=1)
      for s in solve('R'):
        r.accumulate_data(len(s), s)
      
      # output solution
      printf("[max length = {r.value}]")
      for s in r.data:
        printf("chain = [{s}]")
      

      Solution: The maximal length chain is: RILAKSDCTNMO (12 letters).

      Making the following 12 states: RI, IL, LA, AK, KS, SD, DC, CT, TN, NM, MO, OR.


      Starting with the earliest letter in the alphabet we can write this maximal length chain as:

      AKSDCTNMORIL

      There is another length 12 chain that does not include R:

      AKSDCTNMOHIL

      And if we wish to include V the longest chains have length 10:

      AKSDCORINV
      AKSDCOHINV

      Like

    • Jim Randell's avatar

      Jim Randell 12:18 pm on 16 January 2023 Permalink | Reply

      Here is a variation on this puzzle:

      If the state abbreviations (above) are printed on to 51 cards (one to each abbreviation), it is possible to make a circular chain of cards, so that the same letters are next to each other at the joins. (You are not allowed to reverse the letters in an abbreviation, i.e. invert a card).

      For example, here is a chain of 8 cards:

      IN:NV:VT:TN:NM:MO:OR:RI:[IN]

      How long is the longest circular chain of cards you can make?

      My solution follows…


      This Python program runs in 82ms. (Internal runtime is 29.7ms).

      Run: [ @replit ]

      from enigma import (group, item, Accumulator, join, printf)
      
      states = str.split(
        "AK AL AR AZ CA CO CT DC DE FL GA HI IA ID IL IN KS "
        "KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV "
        "NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY"
      )
      
      # group states by first letter
      g = group(states, by=item(0), fn=set)
      
      # generate possible circular chains of states
      def solve(ss):
        # possible next letters
        xs = g.get(ss[-1][-1])
        if not xs: return
        # does the sequence form a chain?
        if len(ss) > 1 and ss[-1][-1] == ss[0][0]:
          yield ss
        # try to extend the sequence
        for x in xs:
          if not (x < ss[0] or x in ss):
            yield from solve(ss + [x])
      
      # find maximal chains starting with the alphabetically first state
      r = Accumulator(fn=max, collect=1)
      for s0 in states:
        for ss in solve([s0]):
          r.accumulate_data(len(ss), ss)
      
      # output solution
      printf("[max length = {r.value}]")
      for ss in r.data:
        printf("chain = [{ss}]", ss=join(ss, sep=","))
      

      Solution: The longest chain uses 21 cards.

      There are 56 possible chains of length 21. Here is one of them:

      AK:KS:SC:CO:OH:HI:IN:NM:MN:NC:CA:AR:RI:ID:DC:CT:TN:NV:VA:AL:LA:[AK]

      Like

    • Jim Olson's avatar

      Jim Olson 5:00 pm on 16 January 2023 Permalink | Reply

      Thanks for pointing out the Times ignorance of the number of states!

      Like

  • Unknown's avatar

    Jim Randell 4:55 pm on 8 January 2023 Permalink | Reply
    Tags: , corrected   

    Teaser 3146: Curling league [revised] 

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

    The number of teams in our curling league is more than 4, but less than 26. In a tournament each team plays each of the other teams once, and the teams are ranked according to the number of wins they achieve (draws are impossible). If any teams are tied on wins, ranking is only possible if those teams have different numbers of wins in their mutual games. For example, in a three-way tie if A beats B, B beats C and A beats C, the ranking is ABC, but if C beats A (or A has not yet played C), then ranking is impossible, as A and B have one win each.

    At one point (when each team had played G games), ranking the teams as above was possible. However, if each team had played G−1 games, a ranking would not have been possible, irrespective of results. With one more team in the league, the minimum number of games needed to allow a ranking would be G+2.

    How many teams are in the league and what was the value of G?

    This is a revised version of the puzzle posted as Teaser 3146. The underlined text has been changed from the published version, to be in line with the intention of the setter.

    [teaser3146]

     
    • Jim Randell's avatar

      Jim Randell 9:41 am on 9 January 2023 Permalink | Reply

      I solved the previous version of this puzzle with a fairly straightforward search [link]. And it came up with a solution quickly, without having to explore the entire solution space. Which was handy because that would have taken a long time.

      So I looked at making my program more efficient. By looking at the number of wins achieved by each team we find there are only certain patterns of wins that are viable. Limiting the search to these patterns of wins, and doing more checks to reject candidate solutions early I was able to come up with a (more complicated) program that explores the entire solution space in a reasonable time, and constructively finds a single solution to the puzzle.

      (This program can also be used to find both solutions to the puzzle as originally worded).

      The following Python program runs in 7.6s.

      Run: [ @replit ]

      from enigma import (
        irange, group, item, div, subsets, multiset, rev,
        diff, filter2, seq_all_different, cache, printf
      )
      
      # labels for <n> teams; 1..n
      teams = lambda n: irange(1, n)
      
      # a match is represented by (<winning-team>, <losing-team>)
      
      # check a set of matches for <n> teams is orderable for teams <ts>
      def orderable(ms, ts):
        # extract the winners
        (ws, _) = zip(*ms)
        # and count the number of wins for each team
        w = dict((t, ws.count(t)) for t in ts)
        # group teams with the same number of wins together
        g = group(w.items(), by=item(1), f=item(0), fn=sorted)
        # look for tied groups
        for (_, xs) in g.items():
          if len(xs) < 2: continue
          # collect the mutual games amongst the group
          ws_ = list(x for (x, y) in ms if x in xs and y in xs)
          # and check they are all different
          if not seq_all_different(ws_.count(t) for t in xs): return False
        # looks good
        return True
      
      # allocate played matches for <n> teams, <k> matches per team, <p> matches in total,
      # from unallocated matches <ps>
      def allocate(n, k, p, ps, ws, t=1, ss=[]):
        # are we done?
        if t > n:
          yield ss
        else:
          # allocated games involving team <t>
          xs = list(x for x in ss if t in x)
          r = k - len(xs)
          if r < 0: return
          nw = ws[0]
          # number of allocated wins
          xw = sum(x == t for (x, y) in xs)
          if xw > nw: return
          # consider unallocated games involving team <t>
          (ts, ps_) = filter2((lambda x: t in x), ps)
          # select the remaining matches for team <t>
          for rs in subsets(ts, size=r):
            for js in subsets(irange(r), size=nw - xw, fn=set):
              # construct the new set of matches
              ss_ = ss + list(((t, x + y - t) if j in js else (x + y - t, t)) for (j, (x, y)) in enumerate(rs))
              # check wins/losses are still viable for the remaining teams
              xws = multiset.from_seq(x[0] for x in ss_)
              xls = multiset.from_seq(x[1] for x in ss_)
              if any(xws.count(j) > w or xls.count(j) > k - w for (j, w) in zip(irange(t, n), ws)): continue
              # and check it is orderable for teams up to t
              if orderable(ss_, teams(t)):
                yield from allocate(n, k, p - r, ps_, ws[1:], t + 1, ss_)
      
      # decompose total played matches <p> into wins for <n> teams, each played <k> matches
      def decompose(p, n, k, w=0, ws=[]):
        if len(ws) == n:
          if p == 0:
            yield rev(ws)
        elif not (w > k):
          # choose number of teams with <w> wins
          for m in irange(min(w, k - w) + 1, 0, step=-1):
            if len(ws) + m > n or m * w > p: continue
            yield from decompose(p - m * w, n, k, w + 1, ws + [w] * m)
      
      # find orderable sets of wins for <n> teams, each having played <k> matches
      def wins(n, k):
        # calculate the number of played matches
        p = div(n * k, 2)
        if p is None: return
        # construct the possible matches
        ps = list(subsets(teams(n), size=2))
        # construct possible wins
        for ws in decompose(p, n, k):
          # we can allocate matches between teams with the same number of wins
          g = group(enumerate(ws, start=1), by=item(1), f=item(0), fn=sorted)
          ss = list()
          for (_, vs) in g.items():
            if len(vs) > 1:
              ss.extend(subsets(vs, size=2))
          if len(ss) > p: continue
          # allocate the remaining matches
          yield from allocate(n, k, p - len(ss), diff(ps, ss), ws, 1, ss)
      
      # check if <n> teams, each played <k> games is orderable
      @cache
      def check(n, k):
        if not (n > k > 0): return None
        for r in wins(n, k):
          printf("[n={n} k={k} -> {r}]", r=sorted(r))
          return True
        printf("[n={n} k={k} -> None]")
        return False
      
      # solve the puzzle
      def solve(start, stop):
        # n = number of teams in the league
        for n in irange(start + 1, stop + 1):
          # k = number of matches played by each team
          # look for minimum k that is orderable
          for k in irange(1, n - 1):
            # can we find an orderable set of wins?
            r = check(n, k)
            # find the smallest k that gives an orderable set
            if r:
              printf("-> n={n}: min_k = {k}")
              (T, G) = (n - 1, k - 2)
              # look to see if (T, G) is possible and (T, G - 1) is not possible
              if check(T, G) is True and check(T, G - 1) is False:
                # output solution
                printf("=> T = {T}; G = {G}")
                printf("##### SOLUTION FOUND -> league has {T} teams; games played = {G} #####")
                yield (T, G)
              break
          printf()
      
      # output solution summary
      for (T, G) in list(solve(5, 25)):
        printf("=> SOLUTION: league has {T} teams; games played = {G}")
      

      Solution: There are 16 teams in the league. G = 6.

      For example, with teams A – P, each can play 6 games, and be orderable as follows:

      1st = A [6 wins; beat: B D E G H I]
      2nd = B [5 wins; beat: C D G H K]
      3rd = C [5 wins; beat: G K L M N]
      4th = D [4 wins; beat: E F K L]
      5th = E [4 wins; beat: F K L N]
      6th = F [4 wins; beat: M N O P]
      7th = G [3 wins; beat: H I J]
      8th = H [3 wins; beat: I J N]
      9th = I [3 wins; beat: J O P]
      10th = J [3 wins; beat: N O P]
      11th = K [2 wins; beat: L M]
      12th = L [2 wins; beat: M P]
      13th = M [2 wins; beat: O P]
      14th = N [1 win; beat: O]
      15th = O [1 win; beat: P]
      16th = P [0 wins]

      And it is not possible for 16 teams to have each played 5 games and be orderable.

      >>> from enigma import first
      >>> first(wins(16, 5))
      []
      

      But it is possible for 17 teams, A – Q, to have each played 8 games and be orderable:

      1st = A [8 wins; beat: B D E G H I J K]
      2nd = B [7 wins; beat: C D G H I J K]
      3rd = C [7 wins; beat: G I J K L M N]
      4th = D [6 wins; beat: E F I J K L]
      5th = E [6 wins; beat: F J L M N O]
      6th = F [6 wins; beat: L M N O P Q]
      7th = G [5 wins; beat: H L M O P]
      8th = H [5 wins; beat: L M O P Q]
      9th = I [4 wins; beat: N O P Q]
      10th = J [3 wins; beat: K O Q]
      11th = K [3 wins; beat: O P Q]
      12th = L [2 wins; beat: M N]
      13th = M [2 wins; beat: N Q]
      14th = N [2 wins; beat: P Q]
      15th = O [1 win; beat: P]
      16th = P [1 win; beat: Q]
      17th = Q [0 wins]

      but not for any number of games less than 8.


      Here are some notes on the refinements I used to eliminate cases from testing compared with my original program, that allows it to run in a reasonable time over the entire solution space…

      Firstly we don’t care what order the teams are in, so we can assume team 1 is 1st in the order, team 2 is 2nd, and so on.

      For a league with n teams, each having played k matches there are p = (n × k /2) matches played in total (each match involves 2 teams), so we can allocate these p wins among the n teams in a non-decreasing sequence of [0..k] wins. With team 1 having the largest number of wins, down to team n having the least wins.

      Then looking at groups of teams with the same number of wins, these groups must be ordered according to the matches played amongst the teams involved. If team X has 0 wins there cannot be another team Y with 0 wins, as we can only order them with an X vs. Y match, which neither team can be allowed to win. So there can be at most 1 team with 0 wins, at most 2 teams with 1 wins, etc.

      And by considering losses we get the same pattern coming down. There can be at most 1 team with k wins, at most 2 teams with (k – 1) wins, and so on.

      So, I generated viable patterns of wins and then allocated matches according to these patterns by working through the list of wins and allocating appropriate matches for each team.

      When allocating the remaining matches for each team I check after each allocation that we haven’t allocated too many wins (or losses) to one of the later occurring teams, and also after we have allocated the matches for a team the matches for that team and all earlier teams are fixed, so the set of matches restricted to just these teams must already be orderable (as they are not going to change later).

      Also, it soon became obvious that using [[ enigma.decompose() ]] to generate all possible decompositions and discarding those that were unsuitable was not very efficient as n increased, so I wrote a specific [[ decompose() ]] routine to do this for me.

      Then I realised that if we have groups of teams with the same number of wins, and we know the order that we want them to appear in, then we can allocate the wins involved between those teams immediately, before we start the general allocation of the remaining matches.

      Together all these improvements let the program run in 10s or less (running under PyPy 7.3.11). This was hugely faster than my initial program for the original puzzle, and let the program examine the entire solution space, and construct orderings where they were possible.

      Like

      • Frits's avatar

        Frits 4:41 pm on 9 January 2023 Permalink | Reply

        Using a different solve() is a bit faster. Basically we start with making a list of orderable set of wins (first item only) by calling decompose().

        Only changes made to the code is to return a tuple in decompose() and to suppress printing in the check routine.

        The “not found” branche is not further developed as it seems that all decompose() output is orderable.

           
        def solve(start, stop):
          F = dict()
          # n = number of teams in the league
          for n in range(start, stop):
            # k = number of matches played by each team
            for k in range(1, n):  
              if n % 2 and k % 2: continue
              # get first orderable set of wins
              lst = list(first(decompose(n * k // 2, n, k)))
              if lst: 
                print((n, k), "-->", lst[0])
                if not check(n, k, p=0):
                  print("not found")
                else:
                  F[n] = k
                break
        
          # check dictionary of first orderable set of wins
          for n, k in F.items():
            if n - 1 in F:
              if F[n - 1] == k - 2:
                printf("=> SOLUTION: league has {n - 1} teams; games played = {k - 2}")
              elif k > 3 and F[n - 1] < k - 2:
                if check(n - 1, k - 2, p=0) and not check(n - 1, k - 3, p=0):
                  printf("=> SOLUTION: league has {n - 1} teams; games played = {k - 2}")
        

        Like

        • Jim Randell's avatar

          Jim Randell 9:46 pm on 9 January 2023 Permalink | Reply

          @Frits: I think most of the speed increase you are seeing comes from not checking the full range for n. If the number of teams can be up to 25 then we need to check up to n = 26. Otherwise it seems to be doing pretty much the same thing as my program, but finding solutions at the end rather than as it goes along.

          The values generated by [[ decompose() ]] all give an orderable set of matches when the program is run normally, but this is not the case in general. While playing around I found [[ check(10, 8) ]] (for example) needed to look at multiple decompositions before an orderable set was found.

          Like

          • Frits's avatar

            Frits 11:21 pm on 9 January 2023 Permalink | Reply

            @Jim, you are right, with range(start, stop + 2) the performance gain is minimal.

            A bit surprising, your program calls check() 135 times and mine only 23 times (but these are all time consuming).

            Like

          • Jim Randell's avatar

            Jim Randell 11:28 pm on 9 January 2023 Permalink | Reply

            The [[ check() ]] function is cached (via the [[ @cache ]] decorator), so we get access to previously computed values without recalculating them.

            Like

            • Frits's avatar

              Frits 11:34 pm on 9 January 2023 Permalink | Reply

              Removing @cache most of the time slightly improves the run time!

              Like

    • Jim Randell's avatar

      Jim Randell 9:36 am on 27 January 2023 Permalink | Reply

      Here is a non-constructive solution, that uses the pattern of wins given in my notes above.

      Thinking about leagues of n teams where each team has played k matches, then if we start with a value for k we see the smallest possible value that n can be is (k + 1).

      And when we think about allocating the p = (n.k / 2) matches among the n teams, then there can only be 1 team with 0 wins or k wins, 2 teams with 1 win or (k − 1) wins, etc.

      So, if k is even (say 2x) we get a pattern of the form:

      1, 2, …, (x + 1), …, 2, 1
      max(n) = 2T(x) + (x + 1) = (x + 1)²

      And if k is odd (say 2x + 1) we get a pattern of the form:

      1, 2, …, (x + 1), (x + 1), …, 2, 1
      max(n) = 2T(x + 1) = (x + 1)(x + 2)

      So by considering increasing values of k we can find a range of potentially viable n values and build up a set of viable (n, k) values. Note that no (n, k) value where both n and k are odd is viable, as a corresponding p value cannot be calculated, but we assume that all remaining (n, k) value will allow a viable ordering to be constructed. (In my program above this assumption is not made, and we don’t accept an (n, k) until an orderable scenario has been constructed. Removing this requirement significantly reduces the amount of work required to find a solution, and is the approach used in the setters’ own solution).

      Once an appropriate set of (n, k) values has been constructed we can look for scenarios that provide a solution to the puzzle.

      We are looking for a number of teams T and number of games G such that:

      (T, G) is viable
      (T, G − 1) is not viable
      (T + 1, G + 2) is viable
      (G + 2) is the smallest viable number of games for a league with (T + 1) teams

      The Python program performs this search. It runs in 57ms. (Internal runtime is 534µs).

      Run: [ @replit ]

      from enigma import (irange, divc, tri, div, peek, printf)
      
      # for a given k value generate possible range of n values
      def nrange(k):
        lo = k + 1
        # count maximum possible wins = number of matches
        h = divc(k, 2)
        t = k * tri(h)
        if k % 2 == 0: t += h * (h + 1)
        hi = div(2 * t, k)
        return (lo, hi)
      
      def solve(start, stop):
        ss = list()
        # possible number of teams
        t = 1
        # record possible (n, k) values
        R = set()
        # consider increasing k values
        for k in irange(1, stop + 1):
          (lo, hi) = nrange(k)
          # fill out possible (n, k) values in R
          for n in irange(lo, hi):
            if k % 2 == 0 or n % 2 == 0:
              R.add((n, k))
          # check possible teams
          while t + 1 < lo:
            t += 1
            # find min_k for t
            min_k = peek(k for k in irange(1, t - 1) if (t, k) in R)
            printf("t={t} -> min_k={min_k}")
            # look for solutions
            (T, G) = (t - 1, min_k - 2)
            if T < start or T > stop: continue
            if (T, G) in R and (T, G - 1) not in R:
              printf("=> T = {T}; G = {G}")
              printf("##### SOLUTION FOUND -> league has {T} teams; games played = {G}")
              ss.append((T, G))
        # output solution summary
        if ss:
          printf()
          for (T, G) in ss:
            printf("=> SOLUTION: league has {T} teams; games played = {G}")
      
      # solve the puzzle
      solve(5, 25)
      

      Like

  • Unknown's avatar

    Jim Randell 4:00 pm on 15 October 2022 Permalink | Reply
    Tags: , corrected   

    Teaser 3134: Stringing along [revised] 

    From The Sunday Times, 16th October 2022 [link] [link]

    An artist hammered thin nails from a pack of 40 into a board to form the perimeter of a rectangle with a 1 cm gap between adjacent nails. He created a work of art by stringing a long piece of wire from one nail to another, such that no section of wire was parallel to an edge of the rectangle. The wire started and ended at two different nails, no nail was used more than once and the length of the wire was a whole number of cm. No longer wire was possible that satisfied the above conditions.

    What were the dimensions of the rectangle and the length of the wire chain (in cm)?

    This is a revised version of the puzzle posted as Teaser 3134. The underlined text has been changed from the previously published version on The Sunday Times website.

    This is the version of the puzzle that appeared in the printed newspaper.

    [teaser3134]

     
    • Jim Randell's avatar

      Jim Randell 4:01 pm on 15 October 2022 Permalink | Reply

      Having solved the (more general) previously published version of this puzzle [link], it is a simple case of adapting my program to account for the additional condition.

      For this version I am assuming we want to find the maximum achievable total length of wire between the nails that can be made using a rectangle constructed from up to 40 nails, with the wire strung between nails where no nail is used more than once and consecutive nails are on different edges of the rectangle (i.e. each linear section of wire must cross part of the interior of the rectangle, and be a whole number of centimetres long), and no linear section of wire is parallel to the edges of the rectangle.

      You can think of the wire being available in various stiff lengths with looped ends, but these individual links can only ever fit between nails that are a whole number of centimetres apart. We wish to make a chain of these wire links, where each link goes from the end of the previous link to a currently unoccupied nail. What is the longest possible chain that can be made, on a rectangle whose perimeter is constructed from no more than 40 nails spaced 1 cm apart, where each link must cross the interior of the rectangle not parallel to the edges of the rectangle, and the start and end of the complete chain are on different nails.

      This Python program runs in 104ms.

      Run: [ @replit ]

      from enigma import (irange, ihypot, chain, Accumulator, printf)
      
      # generate possible <x> by <y> rectangles, using (up to) <n> nails
      def generate(n):
        for y in irange(2, n // 4):
          for x in irange(2, (n - 2 * y) // 2):
            yield (x, y)
      
      # find paths by adding nails to <vs>
      def solve(x, y, nails, vs, d=0):
        # return the current path
        yield (d, vs)
        # choose the next nail
        (x1, y1) = vs[-1]
        for v in nails:
          (x2, y2) = v
          (dx, dy) = (x2 - x1, y2 - y1)
          if dx == 0 or dy == 0: continue
          dd = ihypot(dx, dy)
          if dd is not None:
            yield from solve(x, y, nails.difference([v]), vs + [v], d + dd)
      
      # collect maximal distance paths
      r = Accumulator(fn=max, collect=1)
      
      # consider possible rectangles
      for (x, y) in generate(40):
        # collect possible edge nails
        nails = set()
        for i in irange(0, x - 1):
          nails.update([(i, 0), (x - i, y)])
        for i in irange(0, y - 1):
          nails.update([(0, y - i), (x, i)])
        # start in the bottom/left quadrant
        botm = ((i, 0) for i in irange(0, x // 2))
        left = ((0, i) for i in irange(1, y // 2))
        for v in chain(botm, left):
          for (d, vs) in solve(x, y, nails.difference([v]), [v]):
            r.accumulate_data(d, (x, y, vs))
      
      # output solution(s)
      printf("max wire length = {r.value} cm")
      for (x, y, vs) in r.data:
        printf("-> {x} cm by {y} cm grid {vs}")
      

      Solution: The rectangle was 12 cm × 8 cm. The total length of wire used was 102 cm.


      Here is a diagram of the maximal length layout:

      Like

      • Jim Randell's avatar

        Jim Randell 11:28 am on 28 October 2022 Permalink | Reply

        In my program above, in order to get a total distance that is a whole number we require the individual segments between consecutive pins in the stringing to be a whole number.

        This is a reasonable assumption. As we can show that if we have a collection of positive integers, and at least one of them has a square root that is irrational, then the sum of the square roots of the entire collection is irrational. (See below [*]).

        However, this does not mean we cannot construct stringings that are very close to an integer value using segments that are not themselves integers. And this lets us find solutions that are much longer than the published solution.

        For example here is a stringing on the 12cm × 8 cm grid that has a total length of 440.999925 cm (i.e. 750 nanometres short of 441 cm). It would be practically impossible to tell this apart from a 441 cm length of wire.

        Note that in this construction every pin is visited.

        If we want more accuracy we can get within 6nm of 438 cm.


        [*] The core of the proof is:

        Suppose x, y are integers, such that at least one of √x and √y are irrational.

        Then (x − y) is also an integer, and:

        (x − y) = (√x + √y)(√x − √y)

        If (√x + √y) is rational (= p), then:

        (√x − √y) = (x − y)/(√x + √y) is also rational (= q)

        But then:

        √x = (p + q)/2 is rational; and
        √y = (p − q)/2 is rational

        Which is a contradiction, hence the sum (√x + √y) is irrational.

        Like

    • Hugh Casement's avatar

      Hugh Casement 10:16 am on 16 October 2022 Permalink | Reply

      The change in wording makes quite a difference to the total length, at least with the solution I worked out.

      The puzzle does not seem to be well thought out. That the nails are thin clearly means that we are to ignore their diameter (and the thickness of the wire). But with centimetres as the unit they are not nails at all but pins, and it’s fine fuse wire. Inches would have been somewhat less unrealistic.

      I also feel that if the “artist” had allowed himself more nails, the resulting pattern could have been more interesting. For example, 12 occurs in the Pythagorean triangles (9, 12, 15) and (12, 16, 20) as well as (5, 12, 13), which would presumably give more scope. But the length + width of the rectangle must not exceed 20, so we are much restricted.

      Like

      • Jim Randell's avatar

        Jim Randell 12:03 pm on 17 October 2022 Permalink | Reply

        @Hugh: Yes for the original wording I found a maximal solution with 24 links. For this revised version there are only 10 links. The rectangle was the same in both cases.

        Like

  • Unknown's avatar

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

    Teaser 2823: Queuing 

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

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

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

    At what time did Joe join the queue?

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

    [teaser2823]

     
    • Jim Randell's avatar

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

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

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

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

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

      Run: [ @replit ]

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

      Solution: Joe joined the queue at 10:06.

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

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

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

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

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

      Like

      • Jim Randell's avatar

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

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

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

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

        At what time did Joe join the queue?

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

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

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

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

        We get the solution:

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

        Like

  • Unknown's avatar

    Jim Randell 8:11 am on 2 June 2020 Permalink | Reply
    Tags: , corrected,   

    Teaser 2566: A fulfilling strategy 

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

    I drove down a road with a number of petrol stations whose locations I saw on my map. I decided to check the price at the first station then fill up when I found one offering a lower price (or, failing that, the last one).

    When I got home I noticed that I could arrange the prices (in pence per litre) into an ascending sequence of consecutive whole numbers of pence, plus 0.9p (i.e. 130.9p, 131.9p, 132.2p, etc). I also worked out the average price that I would expect to pay using this strategy, if I were to encounter this set of prices in an unknown order, and I was surprised to find that this value turned out to be a whole number of pence per litre.

    How many petrol stations were there?

    When the puzzle was originally published in The Sunday Times it had been edited into a form that meant there were no solutions. Here I have rephrased the middle paragraph so that it works as the setter originally intended.

    [teaser2566]

     
    • Jim Randell's avatar

      Jim Randell 8:12 am on 2 June 2020 Permalink | Reply

      (See also: Teaser 2988).

      Here is a program that calculates the result directly. It runs in 50ms.

      Run: [ @replit ]

      from enigma import (subsets, irange, inf, factorial, div, printf)
      
      # strategy, take the first amount lower than the first n items
      def strategy(s, n):
        t = min(s[:n])
        for x in s[n:]:
          if x < t: break
        return x
      
      # play the game with k items
      def play(k):
        # collect total amount
        t = 0
        # consider possible orderings of the items
        ps = list(1299 + 10 * p for p in irange(1, k))
        for s in subsets(ps, size=k, select="P"):
          t += strategy(s, 1)
        return t
      
      # consider numbers of items
      for k in irange(2, inf):
        t = play(k)
        # calculate the average expected price (in 10th pence)
        d = factorial(k)
        p = div(t, d)
        if p is not None and p % 10 == 0:
          printf("k={k} -> p={p}")
          break
      

      Solution: There were 5 petrol stations.

      And the expected average fuel price was 132.0 pence.

      It doesn’t actually matter what the prices actually are, all the petrol stations could adjust their prices by the same whole number of pence, and the expected average price would be adjusted by the same amount.


      Analytically:

      As determined in Teaser 2988 we can use the formula:

      S(n, k) = ((2n + 1)k − n(n + 1)) (k + 1) / 2(n + 1)k

      To determine the mean expected value of choosing from k boxes with values from 1..k, using the strategy of picking the next box that is better than any of the first n boxes (or the last box if there are no boxes better than the first).

      In this case n = 1:

      S(1, k) = (3k − 2) (k + 1) / 4k

      If we award ourselves a prize of value k for selecting the cheapest fuel, and a prize of value 1 for selecting the most expensive fuel, then we have the same situation as in Teaser 2988, and if p is expected average prize value, then the corresponding expected average fuel price is (130.9 + k − p).

      And k is an integer, so in order to make the expected average fuel price a whole number of pence we are looking for expected average prize value that is an integer plus 0.9.

      i.e. when the value of 10 S(1, k) is an integer with a residue of 9 modulo 10.

      10 S(1, k) = (15k + 5) / 2 − (5 / k)

      When k is odd, the first part gives us a whole number, so we would also need the (5 / k) part to give a whole number. i.e. k = 1, 5.

      When k is even, the first part gives us an odd number of halves, so we also need (5 / k) to give an odd number of halves. i.e. k = 2, 10.

      And we are only interested in values of k > 1, so there are just 3 values to check:

      k = 5: 10 S(1, k) = 39
      k = 2: 10 S(1, k) = 15
      k = 10: 10 S(1, k) = 77

      The only value for k that gives a residue of 9 modulo 10 is k = 5.

      And in this case our expected average prize is p = 3.9 so our expected average fuel price is 132.0 pence.

      Like

  • Unknown's avatar

    Jim Randell 12:50 pm on 5 November 2019 Permalink | Reply
    Tags: , corrected,   

    Brain-Teaser 508: [Family birthdays] 

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

    Grandma said: “Grandpa and I share between 100 and 120 years of age, being both over 50 years old. The total ages of our family (our son and his children — all of different age years, and over 12 months) exactly equal [my own age. On the same day next year they will exactly equal] Grandpa’s age.”

    “At present, Grandpa’s age is divisible by the age of only one child, but in one year’s time it will be divisible by the separate ages of three children, and also by the number in our family, while my own age will be divisible by the age of one child only (unity always excluded).”

    “During the year ahead on just one day in May, Grandpa’s age will be divisible by that of our son, whose birthday is earlier.”

    “From Grandpa downwards, what are our ages?”

    The following apology was published with Brain-Teaser 511:

    It is regretted that by a printing error the following words were omitted after “exactly equal”: “my own age. On the same day next year they will exactly equal”.

    I have inserted the missing text in the puzzle above.

    This puzzle was originally published with no title.

    [teaser508]

     
    • Jim Randell's avatar

      Jim Randell 2:22 pm on 5 November 2019 Permalink | Reply

      I wrote a program to solve the puzzle as originally stated (without the additional text) and found 15 solutions.

      So I created a MiniZinc model that makes it easy to play around with variations on the puzzle.

      When the constraints arising from the missing text are incorporated we get a single solution.

      %#! minizinc -a
      
      % grandpa and grandma are aged between 51 and 69
      var 51..69: gp;
      var 51..69: gm;
      
      % but the total is not more than 120
      constraint not(gp + gm > 120);
      
      % son's age
      var 18..51: son;
      
      % kids ages (0 for no kid)
      array[1..10] of var 0..16: kids;
      
      % kids are descending order, and all different
      constraint forall (i in 2..10) (kids[i] > 0 -> kids[i - 1] > kids[i]);
      
      % son + total of kids ages = gm [originally was = gp]
      constraint son + sum(kids) = gm;
      
      % son + total of kids ages in 1 year = gp, so: gp - gm = number of children
      % [originally this condition was omitted]
      constraint sum (i in 1..10) (kids[i] > 0) = gp - gm;
      
      % exactly 1 kid divides gp
      constraint sum (i in 1..10) (kids[i] > 1 /\ gp mod kids[i] = 0) = 1;
      
      % but exactly 3 kids divide gp in 1 year
      constraint sum (i in 1..10) (kids[i] > 0 /\ (gp + 1) mod (kids[i] + 1) = 0) = 3;
      
      % and exactly 1 kid divides gm in 1 year
      constraint sum (i in 1..10) (kids[i] > 0 /\ (gm + 1) mod (kids[i] + 1) = 0) = 1;
      
      % also the number of kids + 1 divides into gp in 1 year
      constraint (gp + 1) mod (1 + sum (i in 1..10) (kids[i] > 0)) = 0;
      
      % (son + 1) divides gp
      constraint gp mod (son + 1) = 0;
      
      % at least 16 years between generations
      constraint son + 16 < min([gp, gm]);
      constraint max(kids) + 16 < son;
      
      solve satisfy;
      

      And we can format the output with Python 3 by using the minizinc.py wrapper:

      from minizinc import MiniZinc
      
      for (gp, gm, son, kids) in MiniZinc("teaser508.mzn").solve(result="gp gm son kids"):
        kids = list(x for x in kids.values() if x > 0)
        print(f"Grandpa = {gp}; Grandma = {gm}; Son = {son}; Children = {kids}")
      

      So the answer to the corrected puzzle is:

      Solution: Grandpa = 62; Grandma = 56; Son = 30; Children = 8, 6, 5, 4, 2, 1.

      Without the constraints to enforce a 16 year gap between generations we can have a further solution (with 7 children):

      Grandpa = 63; Grandma = 56; Son = 20; Children = 15, 6, 5, 4, 3, 2, 1

      Like

  • Unknown's avatar

    Jim Randell 9:10 am on 4 August 2019 Permalink | Reply
    Tags: by: Angela Humphreys, corrected   

    Brain-Teaser 491: [Family names] 

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

    Each of the five families in Admirals Walk comprised three boys; one of the boys in each family had the same christian name as the surname of one of his neighbours, and no boy had the same christian and surname.

    The three christian names of Mr. Arnold’s three sons all had the same initial as the surname of Lawrence, who lived opposite to Benjamin Thomas.

    Mr. Gordon’s oldest son had a christian name with the same initial as Clement’s surname. Clement’s father played chess with Jasper Lawrence’s father.

    Mr. Oliver was Arnold’s father’s business partner. Godfrey had measles. Mr. Oliver’s oldest son had the same christian name as Crispin’s surname, and also the same initial to his christian name as Arnold’s surname. Arnold lived next door to the man whose son Oswald played truant from school with his cousin Hector Lawrence, until Oswald’s brother Walter, a school prefect, caught them.

    Gilbert and Oscar had red hair.

    Oliver was in Borstal. What was his surname?

    When originally published the condition relating to Mr. Oliver’s sons was given as:

    Mr. Oliver’s oldest son had the same christian name as Crispin’s surname, and Mr. Oliver’s youngest son had the same initial to his christian name as Arnold’s surname.

    However a correction was published with Brain-Teaser 493 stating:

    It is regretted that in para. 3, line 7, “youngest” should have read “eldest”. However, as the correct initials of these boys can be proved independently and most solvers gave the correct answer, this answer will stand.”

    I have made the correction in the puzzle text above, although instead of just changing “youngest” to “eldest”, as we are already talking about Mr. Oliver’s oldest son, I used “and also”.

    This puzzle was originally published with no title.

    [teaser491]

     
    • Jim Randell's avatar

      Jim Randell 9:11 am on 4 August 2019 Permalink | Reply

      There are 15 first names mentioned, so each boy must have a different first name.

      This Python 3 program starts by filling out the full names we are given. Then it looks for three first names with the same initial for the Arnold family (which also gives us Lawrence’s surname). It then allocates the remaining names, and checks the remaining conditions.

      It runs in 271ms.

      Run: [ @repl.it ]

      from collections import defaultdict
      from enigma import subsets, update, diff, multiset, Accumulator, join, printf
      
      # names we know (map: firstname -> lastname)
      names = { "Benjamin": "Thomas", "Hector": "Lawrence", "Jasper": "Lawrence" }
      
      # the surnames are all distinct by initial
      lastnames = dict((x[0], x) for x in ("Arnold", "Gordon", "Lawrence", "Oliver", "Thomas"))
      
      # the remaining first names
      firstnames = ["Clement", "Crispin", "Gilbert", "Godfrey", "Oscar", "Oswald", "Walter" ]
      firstnames.extend(lastnames.values())
      
      # group firstnames by initial
      initial = defaultdict(list)
      for f in firstnames:
        initial[f[0]].append(f)
      
      # complete the families, ensuring no-one has the same first/last name
      def complete(ns, fs):
        # are we done?
        if not fs:
          yield ns
        else:
          # find an incomplete family to fill out
          s = multiset(ns.values())
          r = Accumulator(fn=min)
          for v in lastnames.values():
            n = 3 - s.get(v, 0)
            if n > 0: r.accumulate_data(n, v)
          (n, v) = (r.value, r.data)
          # choose names
          for ts in subsets(diff(fs, [v]), size=n):
            yield from complete(update(ns, ts, [v] * n), diff(fs, ts))
      
      # collect results
      r = multiset()
      
      # the Arnold children share the same first initial
      for (k, vs) in initial.items():
        if k == 'L': continue
        for fs in subsets(vs, size=3):
          if "Arnold" in fs: continue
          ns1 = update(names, fs, ["Arnold"] * 3)
      
          # Lawrence has one of these names as a surname
          ns1["Lawrence"] = lastnames[k]
      
          # complete the allocations
          for ns in complete(ns1, diff(firstnames, ns1.keys())):
      
            # "Clement's father played chess with Jasper Lawrence's father"
            # so Clement's surname is not Lawrence
            if ns["Clement"] == "Lawrence": continue
      
            # "Mr. Oliver was Arnold's father's business partner"
            # so Arnold's surname is not Oliver
            if ns["Arnold"] == "Oliver": continue
      
            # "Oswald played truant from school with his cousin Hector Lawrence"
            # so Oswald's surname is not Lawrence
            if ns["Oswald"] == "Lawrence": continue
      
            # "Arnold lived next door to ... Oswald"
            # so Arnold and Oswald have different surnames
            if ns["Arnold"] == ns["Oswald"]: continue
      
            # "... Oswald's brother Walter ..."
            # so Oswald and Walter have the same surname
            if ns["Oswald"] != ns["Walter"]: continue
      
            # "Mr. Oliver's oldest son had the same christian name as Crispin's surname ..."
            oliver_maj = ns["Crispin"]
            if ns[oliver_maj] != "Oliver": continue
      
            # "...and the same initial to his christian name as Arnold's surname"
            if oliver_maj[0] != ns["Arnold"][0]: continue
      
            # "Mr. Gordon's oldest son had a christian name with the same initial as Clement's surname"
            gordon_maj = list(k for (k, v) in ns.items() if v == "Gordon" and k[0] == ns["Clement"][0])
            if not gordon_maj: continue
      
            # each family has a child whose name is the surname of one of the other families
            ss = sorted(lastnames.values())
            fss = list()
            for v in ss:
              fss.append(set(x for (x, s) in ns.items() if s == v))
            if not all(fs.intersection(ss) for fs in fss): continue
      
            # output families
            fmt = lambda s: join(sorted(s), sep=", ")
            for (s, fs) in zip(ss, fss):
              printf("{s}: {fs}", fs=fmt(fs))
            printf("Oliver maj = {oliver_maj}; Gordon maj = {gordon_maj}", gordon_maj=fmt(gordon_maj))
            printf()
      
            # record the answer (Oliver's surname)
            r.add(ns["Oliver"])
      
      
      # output the answer
      for (s, n) in r.most_common():
        printf("Oliver {s} [{n} solutions]")
      

      Solution: Oliver’s surname is Lawrence.

      The families are:

      Arnold: Gilbert, Godfrey, Gordon
      Gordon: Lawrence, Oswald, Walter
      Lawrence: Hector, Jasper, Oliver
      Oliver: Clement, Oscar, Thomas
      Thomas: Arnold, Benjamin, Crispin

      Oswald Gordon and Oliver Thomas are the eldest sons in their respective families.

      The originally published puzzle said that “Mr. Oliver’s youngest son had the same initial to his christian name as Arnold’s surname”, which cannot be satisfied with the rest of the conditions. The youngest of the Oliver children must be Clement or Oscar, and Arnold’s surname is Lawrence or Thomas.

      With the change of the puzzle text to refer to the oldest son (who is Thomas Oliver), he shares the same initial (and indeed the same name) as Arnold, as Arnold and Crispin are brothers (both Thomases).

      Like

  • Unknown's avatar

    Jim Randell 8:02 am on 16 April 2019 Permalink | Reply
    Tags: , corrected   

    Brain-Teaser 473: Gouttes d’Or 

    From The Sunday Times, 21st June 1970 [link]

    The conical glasses at the Hôtel d’Or hold one eighth of a litre and are embellished from base to brim with a picture of Bacchus. In making the hotel’s famous Gouttes d’Or, sold at NF 5.20, it was customary to fill the glass up to Bacchus’ neck (4/5th of the way up the glass) and then add an olive; the profit on raw materials was a modest 100 per cent.

    Gaston, the barman, has decided that he must make the more realistic profit of 400 per cent. So now the olive is put in first and then liquor is added up to the level of Bacchus’ chest (3/5ths of the way up the glass). The price has been reduced to NF 4.80.

    Gaston explained: “Ze olives are standard sized and cost me one centime a cc. Yes, I could put in ze olive after measuring out the liquid — but zen it would be necessary to charge …”

    How much?

    This is a corrected version of the originally published puzzle. In the original version the price of the first glass was incorrectly given as NF 5.30 (instead of NF 5.20). The puzzle can still be solved in the same way, but the numbers don’t work out as nicely.

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

    [teaser473]

     
    • Jim Randell's avatar

      Jim Randell 8:03 am on 16 April 2019 Permalink | Reply

      If the conical glass has a height of h and a radius (at the top) of r, then a completely full glass will have a volume of:

      𝛑 r² h / 3 = 125

      So, if it was filled to a height of (k/5) h (and hence a radius of (k/5) r, then the volume of liquid is:

      𝛑 (kr/5)² (kh/5) / 3 = (k/5)³ 125 = k³

      So a 4/5 full glass contains 64 cc, and a 3/5 full glass 27 cc.

      If olives have a volume of x cc (and cost 1 centime per cc) and the liquor has a cost of y centimes per cc, and the profit is 100% (i.e. the cost of the drink is twice the cost of the raw materials), then for the first glass we have:

      520 = 2(x + 64y)
      260 = x + 64y

      And for the second glass we have a 400% profit (i.e. the cost of the drink is 5 times the cost of the raw materials), on a drink with the olive added before the liquor we have:

      480 = 5(x + (27 − x)y)
      96 = x + 27y − xy

      (assuming the olive is submerged, and so displaces its own volume of liquid).

      These two equations have a reasonable solution at x = 4, y = 4. (And an unreasonable solution where x = 219, which is a very large olive, and wouldn’t fit in the glass).

      So the cost of a 3/5 glass, if the olive was added last, and the profit multiple is 5, would be:

      5(x + 27y) = 560

      Solution: The drink would cost NF 5.60.

      Here is a Python program that solves the puzzle numerically. It runs in 81ms.

      from enigma import (Polynomial, fdiv, find_zero, printf)
      
      # for a 4/5 full glass, with olive added last, the cost is k1, profit multiple is m1
      # for a 3/5 full glass, with olive added first, the cost is k2, profile multiple is m2
      # return (<olive volume>, <liquor cost>, <answer>)
      def solve(k1, m1, k2, m2):
      
        # volumes for a 4/5 and 3/5 glass
        (v4, v3) = (4**3, 3**3)
      
        # create a polynomial that gives the cost of liquor in terms of the volume of an olive
        # using the first equation:
        # k1 = m1 * (x + v4 * y)
        q = Polynomial([fdiv(k1, m1 * v4), fdiv(-1, v4)])
      
        # create the polynomial whose roots are the volume of an olive
        # using the second equation:
        # k2 = m2 * (x + (v3 - x) * y)
        p = q * Polynomial([v3, -1]) - Polynomial([fdiv(k2, m2), -1])
      
        # find a solution with a reasonably sized olive
        r =  find_zero(p, 0, 10)
        x = r.v
        y = q(x)
        # cost of a 3/5 glass, with an olive last, profit multiple m2
        k = m2 * (x + v3 * y)
        return (k, x, y)
      
      # solve for the required amounts
      (k, x, y) = solve(520, 2, 480, 5)
      
      printf("cost = NF {f:.02f} [x = {x:.02f}, y = {y:.02f}]", f=0.01 * k)
      

      If we change the first argument to [[ solve() ]] on line 30 to 530 we get the solution to the puzzle as originally set: approximately NF 5.72.

      Like

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