Tagged: by: Nick MacKinnon Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:25 pm on 10 June 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3116: Poll positions 

    From The Sunday Times, 12th June 2022 [link] [link]

    In an election for golf-club president, voters ranked all four candidates, with no voters agreeing on the rankings. Three election methods were considered.

    Under first-past-the-post, since the first-preferences order was A, B, C, D, the president would have been A.

    Under Alternative Vote, since A had no majority of first preferences, D was eliminated, with his 2nd and 3rd preferences becoming 1st or 2nd preferences for others. There was still no majority of 1st preferences, and B was eliminated, with his 2nd preferences becoming 1st preferences for others. C now had a majority of 1st preferences, and would have been president.

    Under a Borda points system, candidates were given 4, 3, 2, or 1 points for each 1st, 2nd, 3rd or 4th preference respectively. D and C were equal on points, followed by B then A.

    How many Borda points did each candidate receive?

    [teaser3116]

     
    • Jim Randell's avatar

      Jim Randell 5:17 pm on 10 June 2022 Permalink | Reply

      There are only factorial(4) = 24 different ways to order the candidates, so that gives us an upper bound on the number of voters.

      This Python program finds the required points values, and the unique set of votes that leads to it.

      It isn’t particularly fast (although it is faster than my first program, which just tried all possible sets of votes). It runs in 251ms.

      Run: [ @replit ]

      from enigma import (group, subsets, join, unpack, irange, decompose, cproduct, map2str, printf)
      
      candidates = "ABCD"
      
      # consider possible voting patterns (first, second, third, fourth)
      # grouped by first choice
      patterns = group(subsets(candidates, size=len, select="P", fn=join), by=(lambda x: x[0]))
      
      # count the number of first choice votes
      def first(vs, ks):
        d = dict((k, 0) for k in ks)
        for v in vs:
          d[v[0]] += 1
        return sorted(d.items(), key=unpack(lambda p, n: n), reverse=1)
      
      # eliminate a candidate
      def eliminate(vs, x):
        return list(v.replace(x, '', 1) for v in vs)
      
      # calculate Borda points
      def borda(vs, ks):
        n = len(ks)
        d = dict((k, 0) for k in ks)
        for v in votes:
          for (i, x) in enumerate(v):
            d[x] += n - i
        return d
      
      # check the remaining puzzle constraints
      # return Borda points
      def check(N, votes):
      
        # first D is eliminated
        vs = eliminate(votes, 'D')
        # count the number of first choices
        ((p1, n1), (p2, n2), (p3, n3)) = first(vs, "ABC")
        #  there is no majority of first choices, and B is eliminated
        if 2 * n1 > N or not (n2 > n3 and p3 == 'B'): return
      
        # then B is eliminated
        vs = eliminate(vs, 'B')
        # count the number of first choices again
        ((p1, n1), (p2, n2)) = first(vs, "AC")
        if not (p1 == 'C' and 2 * n1 > N): return
      
        # count Borda points
        pts = borda(votes, candidates)
        # D and C are equal first, then B, then A
        if not (pts['D'] == pts['C'] > pts['B'] > pts['A']): return
      
        # return Borda points
        return pts
      
      # consider the number of voters [6, 24]
      for N in irange(6, 24):
        # consider the number of first choice votes (A does not have a majority)
        for (A, B, C, D) in decompose(N, 4, increasing=-1, sep=1, min_v=0, max_v=min(6, N // 2)):
          # choose the votes
          vs = (subsets(patterns[k], size=n) for (k, n) in zip("ABCD", (A, B, C, D)))
          for (As, Bs, Cs, Ds) in cproduct(vs):
            votes = As + Bs + Cs + Ds
            pts = check(N, votes)
            if pts:
              printf("points: {pts}", pts=map2str(pts, arr="=", sep=" ", enc=""))
              printf("-> {n} votes {votes}", n=len(votes), votes=join(votes, sep=" ", enc="[]"))
              printf()
      

      Solution: The Borda points are: A=33, B=35, C=36, D=36.

      There are 14 voters and the votes cast are:

      ABCD, ABDC, ACDB, ADBC, ADCB
      BCAD, BCDA, BDAC, BDCA
      CBDA, CDAB, CDBA
      DCAB, DCBA

      Using “first-past-the=post”, A wins with 5 votes, B has 4, C has 3, D has 2.

      Under “alternative vote” the first round is as given above. D is eliminated and his 2 votes are redistributed to give: A=5, B=4, C=5. Again there is no outright winner, so C’s 4 votes are redistributed to give: A=6, C=8, and C wins.

      Like

      • Frits's avatar

        Frits 10:58 am on 11 June 2022 Permalink | Reply

        @Jim,

        With a little analysis you can halve the run time by choosing a better range of number of voters than [6, 24].

        Like

        • Jim Randell's avatar

          Jim Randell 1:32 pm on 11 June 2022 Permalink | Reply

          I went with the smallest possible number of voters: D=0, C=1, B=2, A=3, gives a total of 6 voters.

          But in this scenario when D is eliminated there would be no votes to redistribute, so we can move to: D=1, C=2, B=3, A=4, to give a total of 10 voters.

          But when D’s votes are redistributed it is enough to knock B into last place, so C needs to gain at least 2 votes to leapfrog B.

          So we can increase minimum possible to: D=2, C=3, B=4, A=5, for a total of 14 voters.

          Incorporating this into my program brings the run time down to 384ms.

          And with a bit more analysis of the alternative vote system we can see that the number of voters is in {14, 15, 17, 18}.

          Like

          • Frits's avatar

            Frits 1:59 pm on 11 June 2022 Permalink | Reply

            @Jim,

            Yes, [14, 15, 17, 18] is what I had in mind.

            Like

          • Frits's avatar

            Frits 2:05 pm on 11 June 2022 Permalink | Reply

            My PuzzlingInPython program also early rejects N=18.

            Like

    • Hugh+Casement's avatar

      Hugh+Casement 9:48 am on 19 June 2022 Permalink | Reply

      I really don’t see the point of optimizing the run time for a program that is run only once.
      It takes far longer to write it!

      Like

      • Jim Randell's avatar

        Jim Randell 10:59 pm on 19 June 2022 Permalink | Reply

        If possible, I aim for a generic program that runs in under 1s, and if I can get the run time down to 100ms that’s even better (although I still like to keep it generic if possible).

        In this case my initial attempt ran in 44s, so I accepted I might have to tune it a little to improve on the run time. It wasn’t very much work, and the program posted above ran in less than 1s. I did do some more tweaking which got the run time down to 74ms, but I didn’t think the extra complication was worth posting.

        Like

        • Hugh+Casement's avatar

          Hugh+Casement 9:51 am on 20 June 2022 Permalink | Reply

          This is not process control, where milliseconds may well matter!

          If a program takes many minutes to run (while I go and do something else), I probably find it worth spending a bit of time to try and improve the method or find a different line of attack.
          But one has to keep a sense of proportion.

          Like

  • Unknown's avatar

    Jim Randell 9:12 am on 12 May 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2845: Baled out 

    From The Sunday Times, 2nd April 2017 [link] [link]

    In my fantasy Euro football championship the four home teams were in the same group, with each team playing each other team once. Group positions were decided on points, then using “goal differences” and then “goals scored” if necessary.

    After having played two games each, just three goals had been scored in the group, leading to England being ahead with Northern Ireland second, Scotland third and Wales fourth. Wales realised that they had to score at least three goals in their remaining game in order to have a chance of being group leaders. In the final game of the group, Bale scored a third goal in the dying seconds, resulting in Wales being group winners and knocking England into second place.

    What were the scores in the six games (in the order EvN, EvS, EvW, NvS, NvW, SvW)?

    [teaser2845]

     
    • Jim Randell's avatar

      Jim Randell 9:15 am on 12 May 2022 Permalink | Reply

      There are 4 teams, and each team plays each other team once. So there are 6 matches in total, each team playing 3 matches.

      When each team has played each other team twice, 4 of the matches have been played, leaving 2 matches remaining.

      My first programmed solution was quite long and a bit clunky (and relied on a bit of analysis).

      The following program is neater. It considers all possibilities for the 4 played matches, and looks for sets of scorelines that have 3 goals scored in total and place the teams in order (E, N, S, W). It then examines scenarios for the remaining 2 matches, such that W can take 1st place, but must score at least 3 goals in order to do so. Once a viable scenario is found, we look for the actual scenario (where W scores 3 goals in their final match to come 1st, and E finishes in 2nd place). The scenarios are then output (matches and table).

      It runs in 244ms.

      Run: [ @replit ]

      from enigma import (defaultdict, Football, subsets, partitions, irange, inf, diff, peek, update, zip_eq, join, sprintf, printf)
      
      # scoring system
      football = Football(points=dict(w=3, d=1))
      
      # labels for the teams
      teams = (E, N, S, W) = (0, 1, 2, 3)
      
      # keys for the matches
      matches = list(subsets(teams, size=2))
      
      # generate possible scores for <n> matches with <g> total goals scored
      def _scores(n, g, ss=[]):
        if n == 1:
          for x in irange(0, g):
            yield ss + [(x, g - x)]
        else:
          # choose the number of goals scored in this match
          for m in irange(0, g):
            for x in irange(0, m):
              yield from _scores(n - 1, g - m, ss + [(x, m - x)])
      
      # generate sets of <n> matches with the specified range of goals scored
      def scores(n, goals=None, min_goals=0, max_goals=inf):
        if goals is not None: min_goals = max_goals = goals
        for g in irange(min_goals, max_goals):
          yield from  _scores(n, g)
      
      # assign positions to teams, based on scores <ss>
      def table(ss):
        # calculate the points, goals for, goals against for each team
        (pts, gf, ga) = (list(), list(), list())
        for t in teams:
          (ss_, ts_) = football.extract(ss, t)
          table = football.table(football.outcomes(ss_), ts_)
          pts.append(table.points)
          (f, a) = football.goals(ss_, ts_)
          gf.append(f)
          ga.append(a)
        # calculate the positions of the teams (1st, 2nd, 3rd, 4th)
        pos = sorted(teams, key=(lambda t: (pts[t], gf[t] - ga[t], gf[t])), reverse=1)
        return (pos, pts, gf, ga)
      
      # names for the teams
      name = "ENSW"
      
      # output matches
      def output_matches(n, ss):
        fmt = lambda k, v: sprintf("{x}v{y} = {a}-{b}", x=name[k[0]], y=name[k[1]], a=v[0], b=v[1])
        printf("{n} {s}", s=join((fmt(k, v) for (k, v) in sorted(ss.items())), sep="; "))
      
      # output the table
      def output_table(pos, pts, gf, ga):
        for (i, t) in enumerate(pos, start=1):
          printf("  {i}: {n} -> {p} pts, f={f} a={a}", n=name[t], p=pts[t], f=gf[t], a=ga[t])
      
      # choose the 2 unplayed matches
      for unplayed in partitions(teams, 2):
        played = diff(matches, unplayed)
      
        # allocate scores for the 4 played matches, with 3 goals scored
        for ps in scores(len(played), goals=3):
          ss1 = dict(zip(played, ps))
      
          # check the first scenario: E > N > S > W
          (pos1, pts1, gf1, ga1) = table(ss1)
          if not zip_eq(pos1, [E, N, S, W]): continue
      
          # now consider scores for the 2 unplayed matches (say up to 6 goals)
          # and look for situations where W can take 1st place
          uW = peek(x for x in unplayed if W in x)
          Ws = defaultdict(list)
          for us in scores(len(unplayed), min_goals=0, max_goals=6):
            ss2 = update(ss1, unplayed, us)
      
            # check W can take 1st place
            (pos2, pts2, gf2, ga2) = table(ss2)
            if not (pos2[0] == W): continue
      
            # record match results by W's goals
            (_, w) = ss2[uW]
            Ws[w].append(ss2)
            if w < 3: break  # we can stop if w < 3
      
          # W needs to score at least 3 goals to take 1st place
          if not (Ws and min(Ws.keys()) == 3): continue
      
          # now look for the actual situation, where W scores 3 goals, and E takes 2nd place
          for ss2 in Ws[3]:
            # check E is in 2nd place
            (pos2, pts2, gf2, ga2) = table(ss2)
            if not (pos2[1] == E): continue
            # output solution
            output_matches("[1]", ss1)
            output_table(pos1, pts1, gf1, ga1)
            printf("")
            output_matches("[2]", ss2)
            output_table(pos2, pts2, gf2, ga2)
            printf("\n")
      

      Solution: The scores were: EvN = 0-0; EvS = 0-1; EvW = 2-0; NvS = 1-0; NvW = 0-3; SvW = 0-0.


      At first I wasn’t sure whether we were using a “2 points for a win” or a “3 points for a win” scoring regime. (The puzzle text does not mention how the points are allocated).

      In the program I used “3 points for a win, 1 point for a draw”, but the answer is the same if “2 points for a win, 1 point for a draw” is used. This can be changed at line 5.

      When the program generates possible scorelines for the final 2 matches it only looks for matches with up to 6 goals scored in total. You can increase this value at line 74 of the program, but it doesn’t affect the answer found.

      Like

  • Unknown's avatar

    Jim Randell 3:02 pm on 8 April 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3107: Room 101 

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

    In the Ministry of Love, O’Brien told Winston Smith to calculate five-digit perfect squares, none sharing a prime factor with 1984.

    “And Winston, no two of them may differ by a multiple of 101 or you know what will happen. Now find as many as you can.”

    When Winston said he had found more than fifty, O’Brien replied:

    “Well done, Winston. Although there were a quadrillion solutions, I know some of your squares.”

    “Does the party control my mind?”

    “We do Winston. Did you choose 10201, for example?”

    “Of course not! It’s the worst square in the world!”

    “How many fingers am I holding up Winston?”

    “Three? Four? I don’t know!”

    “That’s how many of your squares we know.”

    What four squares did O’Brien know?

    [teaser3107]

     
    • Jim Randell's avatar

      Jim Randell 3:19 pm on 8 April 2022 Permalink | Reply

      This Python program collects groups of 5-digit squares (excluding 10201) by their residue modulo 101. Any two squares with the same residue will differ by a multiple of 101.

      So Smith can choose at most one square from each group.

      It turns out there are 51 groups, so if Smith has correctly found more than 50 squares he must have chosen 1 from each group. And the only choices we (and O’Brien) can know for sure are from groups that have exactly 1 member.

      The program runs in 55ms.

      Run: [ @replit ]

      from enigma import (powers, is_coprime, group, mod, join, printf)
      
      # group 5-digit squares, except 10201, that are coprime to 1984 by their residue mod 101
      d = group(
        powers(100, 316, 2),
        st=(lambda x: x != 10201 and is_coprime(x, 1984)),
        by=mod(101),
      )
      printf("[{n} groups]", n=len(d.keys()))
      
      # look for keys with only one candidate
      for k in sorted(d.keys()):
        vs = d[k]
        if len(vs) == 1:
          printf("{k} -> {vs}", vs=join(vs, sep=", "))
      

      Solution: The four squares O’Brien knew are: 15625, 34969, 62001, 91809.

      Like

    • GeoffR's avatar

      GeoffR 8:13 am on 14 April 2022 Permalink | Reply

      
      from enigma import is_coprime
      from collections import defaultdict
      SQList = defaultdict(list)
      
      # 5-digit squares must be co-prime with 1984
      # omit 101 as 101 ^ 2 = 10201, which is not needed
      squares = [x * x for x in range(102, 317) if is_coprime(x * x, 1984)]
      
      # index squares on remainder after dividing square by 101.
      for n in squares:
          SQList[n % 101].append(n)
      
      print("O'Brien knew the following squares:" )
      for k, v in SQList.items():
          if len(v) == 1:
              print( v[0], end = ' ')
      
      

      Like

    • Hugh+Casement's avatar

      Hugh+Casement 10:58 am on 17 April 2022 Permalink | Reply

      I think it can be done without a program.
      1984 = 2^6 × 31, so all even integers are excluded, as are the squares of multiples of 31, which are 155, 217, and 279 in the range under consideration (101 to 315). We are also told that 101² is absent.

      If i² – j² is a multiple of 101, then so is either (i – j) or (i + j).
      That means that nearly every odd integer in the range is paired with at least one other. The exceptions are 125 = 404 – 279, 187 = 404 – 217, 249 = 404 – 155, and 303 = 404 – 101 = 101 + 202.

      And there certainly weren’t a quadrillion: in English we would say “Though there be a quadrillion …”.

      Like

  • Unknown's avatar

    Jim Randell 8:55 am on 5 April 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2866: On the rack 

    From The Sunday Times, 27th August 2017 [link] [link]

    I have wine in a rectangular rack consisting of five rows and eight columns. The bottles are made up of five different vintages with each vintage (four reds and four whites) forming a rectangle in the rack. Within each vintage-rectangle the places occupied by the whites form a connected set, and the same is true of the reds. No two vintage-rectangles have the same red/white pattern, no matter what angle they are viewed at from the front.

    In the rack the first row is a mixture of reds and whites, with a red on the extreme right. Another row contains just reds, and the first column contains more reds than than any other column.

    What (in order) are the colours of the wines in the second row?

    [teaser2866]

     
    • Jim Randell's avatar

      Jim Randell 8:56 am on 5 April 2022 Permalink | Reply

      There are 5 possible rectangular patterns of 8 bottles:

      And 5×8 = 40 so each pattern must appear exactly once. All we need to do is work out how to fit them into an 8×5 grid meeting the specified conditions.

      It is fairly straightforward to sketch out a solution manually on the back of an envelope in a few minutes. But making a programmed solution is a bit more involved, and my first program only just managed a run time less than a second.

      However, we can restructure the program using a few techniques to make it more efficient:

      Firstly we take the pieces and precompute the data for the 4 possible rotations. The rotations are then stored with the values furthest from the origin listed first, so when we try to fit them into the grid, pieces that fall outside the bounds of the grid will fail sooner.

      The grid is stored as a linear array to make copying it easy, and the values are stored in columns in the grid, so we fill out the columns in order as we go along.

      As the pieces are placed we check that (row 0, col 7) must be red, and as the columns are filled out we ensure that column 0 (which is filled out first) has more reds than any of the other columns.

      So when the grid is complete we only need to check that row 0 also has whites and one of the other rows is entirely red.

      Together, these measures bring the run time down to just 64ms. (Internal runtime is 12ms).

      Run: [ @replit ]

      from enigma import (irange, unpack, chunk, join, printf)
      
      # red and white
      (R, W) = (0, 1)
      
      # pieces: [ (r, c, v) ... ]
      pieces = [
        # R R R R W W W W
        [ (0, 0, R), (0, 1, R), (0, 2, R), (0, 3, R), (0, 4, W), (0, 5, W), (0, 6, W), (0, 7, W) ],
        # R R R R
        # W W W W
        [ (0, 0, R), (0, 1, R), (0, 2, R), (0, 3, R), (1, 0, W), (1, 1, W), (1, 2, W), (1, 3, W) ],
        # R R R W
        # R W W W
        [ (0, 0, R), (0, 1, R), (0, 2, R), (0, 3, W), (1, 0, R), (1, 1, W), (1, 2, W), (1, 3, W) ],
        # R W W W
        # R R R W
        [ (0, 0, R), (0, 1, W), (0, 2, W), (0, 3, W), (1, 0, R), (1, 1, R), (1, 2, R), (1, 3, W) ],
        # R R W W
        # R R W W
        [ (0, 0, R), (0, 1, R), (0, 2, W), (0, 3, W), (1, 0, R), (1, 1, R), (1, 2, W), (1, 3, W) ],
      ]
      
      # rotate a piece 90 degrees
      def rotate(p):
        p_ = list((c, -r, v) for (r, c, v) in p)
        r0 = min(r for (r, _, _) in p_)
        c0 = min(c for (_, c, _) in p_)
        return list((r - r0, c - c0, v) for (r, c, v) in p_)
      
      # transform the pieces array into [ <rot 0>, <rot 90>, <rot 180>, <rot 270> ]
      for i in irange(len(pieces)):
        p = pieces[i]
        ps = list()
        for n in irange(0, 3):
          ps.append(sorted(p, key=unpack(lambda r, c, v: r + c), reverse=1))
          if n == 3: break
          p = rotate(p)
        pieces[i] = ps
          
      # place p at position k in g
      def place(g, p, k):
        g = list(g)
        (c0, r0) = divmod(k, 5)
        for (r, c, v) in p:
          r += r0
          if r > 4: return
          c += c0
          if c > 7: return
          # only place R at (r=0, c=7)
          if r == 0 and c == 7 and v != R: return
          i = 5 * c + r
          if g[i] is not None: return
          g[i] = v
        return g
      
      # fit the pieces into the grid
      # return the rows
      def fit(ps, g):
        # are we done?
        if not ps:
          # create the cols and rows
          cs = list(chunk(g, 5))
          rs = list(zip(*cs))
          # row 0 has whites in (as well as red at col 7)
          if W not in rs[0]: return
          # but one of the other rows is all red
          if all(W in r for r in rs): return
          # return the rows
          yield rs
        else:
          # find an empty square
          k = g.index(None)
          # try to place a piece there (in some orientation)
          for (i, rs) in enumerate(ps):
            for p in rs:
              g_ = place(g, p, k)
              if g_ is None: continue
              # make the columns
              cs = list(chunk(g_, 5))
              if None not in cs[0]:
                # count the reds in column 0
                n = cs[0].count(R)
                # all the other columns must have fewer reds
                if not all(j == 0 or col.count(R) < n for (j, col) in enumerate(cs)): continue
              # place the remaining pieces
              yield from fit(ps[:i] + ps[i + 1:], g_)
                
      # initial grid
      g0 = [None] * 40
      
      # fit the pieces into the grid
      for rs in fit(pieces, g0):
        # output rows
        for r in rs:
          printf("[ {r} ]", r=join(('RW'[v] for v in r), sep=' '))
        printf()
      

      Solution: The wines in the second row down are: (red, white, white, white, white, white, white, red).

      There is only one possible layout, as shown:

      Like

  • Unknown's avatar

    Jim Randell 8:47 am on 29 March 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2852: When in Rome 

    From The Sunday Times, 21st May 2017 [link] [link]

    An ancient five-by-five table of letters been found in Rome. All the ten different five-letter rows across and five-letter columns down are Roman numerals, with no letter exceeding a C. The across numbers are, in order, even, even, a cube, a prime, and a power of 2. The down numbers include a cube, the rest being even or prime.

    Which two Roman numerals cross at the middle square?

    [teaser2852]

     
    • Jim Randell's avatar

      Jim Randell 8:47 am on 29 March 2022 Permalink | Reply

      This Python program constructs candidate values for the rows and the columns, and then fills out the rows, ensuring there are always available candidates for the columns.

      Once the grid is filled out the columns are checked to ensure they meet the puzzle requirements.

      It runs in 61ms.

      Run: [ @replit ]

      from enigma import (irange, inf, cb, Primes, int2roman, union, unpack, update, delete, join, match, roman2int, printf)
      
      # max number
      N = 399
      
      # 5-digit roman numerals, map: int -> roman
      rom5 = dict()
      for n in irange(1, N):
        r = int2roman(n)
        if len(r) != 5: continue
        assert not ('M' in r or 'D' in r)
        rom5[n] = r
      
      # generate roman numerals from function f
      def romans(f, a=1):
        for i in irange(a, inf):
          n = f(i)
          if n > N: break
          r = rom5.get(n)
          if r: yield r
      
      # categorise the roman numerals into sets
      evens = set(v for (k, v) in rom5.items() if k % 2 == 0)
      cubes = set(romans(cb))
      ps = Primes(N + 1)
      primes = set(v for (k, v) in rom5.items() if k in ps)
      pow2s = set(romans(lambda n: 2**n))
      
      # row/column candidates
      rcs = [evens, evens, cubes, primes, pow2s]
      ccs = [union([cubes, evens, primes])] * 5
      
      # return column candidates for rows rs
      def col_cand(rs, ccs):
        ccs_ = list()
        # consider each column
        for (j, t) in enumerate(zip(*rs)):
          # make the template
          t = join(t)
          cs = list(x for x in ccs[j] if match(x, t))
          if not cs: return
          ccs_.append(cs)
        return ccs_
      
      # fill out the grid (rows), respecting row/col candidates
      # rs = partially completed grid (rows)
      # rcs = row candidates; map <index> -> <candidates>
      # ccs = col candidates
      def solve(rs, rcs, ccs):
        # are we done?
        if not rcs:
          yield rs
        else:
          # choose a row
          (k, vs) = min(rcs.items(), key=unpack(lambda k, vs: len(vs)))
          # choose a value
          for v in vs:
            rs_ = update(rs, [k], [v])
            # check columns in the new grid match
            cs_ = col_cand(rs_, ccs)
            if cs_:
              # solve for the remaining rows
              yield from solve(rs_, delete(rcs, [k]), cs_)
      
      # initial grid
      g = ['?????'] * 5
      for rs in solve(g, dict(enumerate(rcs)), ccs):
        cs = list(map(join, zip(*rs)))
        # rows and columns are all different
        if len(union([rs, cs])) != 10: continue
        # check the columns: exactly 1 is a cube, the others are even or prime
        cbs = set()
        eps = set()
        for (i, x) in enumerate(cs):
          if x in cubes: cbs.add(i)
          if x in primes or x in evens: eps.add(i)
        if not (len(cbs) == 1 and len(eps) == 4 and (not cbs.issubset(eps))): continue
        # output solution
        for (t, vs) in zip(['rows', 'cols'], (rs, cs)):
          fmt = lambda vs: join(vs, sep=" | ", enc=("[ ", " ]"))
          printf("{t}: {vs} = {ns}", vs=fmt(vs), ns=tuple(roman2int(v) for v in vs))
        printf()
      

      Solution: The middle row is CCXVI (216). The middle column is LXXIX (79).

      The completed grid looks like this:

      Like

    • Jim Olson's avatar

      Jim Olson 6:30 pm on 29 March 2022 Permalink | Reply

      Typo on the last column. Should be “23” which is still a prime number.

      Like

  • Unknown's avatar

    Jim Randell 4:10 pm on 18 February 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3100: Crate Expectations 

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

    Pip and Estella know each other to be honest and clever. At the reading of Miss Havisham’s will they see a large crate on which are placed two envelopes addressed V+S+E and V, and a lipstick. The solicitor gives Estella the V+S+E envelope and Pip the V envelope and says, “Inside this crate Miss Havisham has left a cuboidal gold bar whose dimensions in mm are different whole numbers greater than 1, and whose volume, surface area and total edge length are V mm³, S mm² and E mm respectively. Inside your envelope, which you should now open, is the secret value of your address. Every fifteen minutes a bell will ring, and if at that point you know the other’s value, write both values on the crate with lipstick and the gold is yours”. At the first bell Pip and Estella sat still, but when the bell rang again Estella lipsticked 3177 and Pip’s value on the crate.

    What was Pip’s value?

    [teaser3100]

     
    • Jim Randell's avatar

      Jim Randell 4:49 pm on 18 February 2022 Permalink | Reply

      For a cuboid with dimensions x, y, z we have:

      V = xyz
      S = 2(xy + xz + yz)
      E = 4(x + y + z)

      V+S+E = xyz + 2(xy + xz + yz) + 4(x + y + z)
      V+S+E = (x + 2)(y + 2)(z + 2) – 8

      Estella knows that V+S+E = 3177, so we can find possible cuboids by looking at the decomposition of (3177 + 8) into three factors.

      But Pip did not respond at the first bell, so we can discard any cuboids that give a unique V+S+E value for the corresponding V value.

      I’ve added the [[ divisors_tuples() ]] function from Enigma 1062 to the enigma.py library.

      This Python program runs in 48ms.

      Run: [ @replit ]

      from enigma import (Record, divisors_tuples, printf)
      
      # determine cuboid values from (x, y, z) dimensions
      def cuboid(x, y, z):
        V = x * y * z
        S = 2 * (x * y + x * z + y * z)
        E = 4 * (x + y + z)
        return Record(x=x, y=y, z=z, V=V, S=S, E=E, VSE=V + S + E)
      
      # determine cuboid values from volume V
      def from_V(V):
        for (x, y, z) in divisors_tuples(V, 3):
          if 1 < x < y < z:
            yield cuboid(x, y, z)
      
      # determine cuboid values from V+S+E value
      def from_VSE(VSE):
        for (x, y, z) in divisors_tuples(VSE + 8, 3):
          if 3 < x < y < z:
            yield cuboid(x - 2, y - 2, z - 2)
      
      # Estella knows V+S+E = 3177
      for e in from_VSE(3177):
        # eliminate cuboids with volume V that have unique V+S+E values
        ps = set(p.VSE for p in from_V(e.V))
        if len(ps) < 2: continue
        # output solution
        printf("V={e.V} [x={e.x} y={e.y} z={e.z}] ps={ps}")
      

      Solution: Pip’s value was 1815.

      Given a V+S+E value of 3177, Estella knows the decomposition is one of:

      x=3, y=5, z=89; V=1335, S=1454, E=388; V+S+E = 3177
      x=3, y=11, z=47; V=1551, S=1382, E=244; V+S+E = 3177
      x=5, y=11, z=33; V=1815, S=1166, E=196; V+S+E = 3177

      Which means Pip has been given a value for V that is one of 1335, 1551, 1815.

      If Pip had been given 1335, he could work out:

      x=3, y=5, z=89; V=1335, S=1454, E=388; V+S+E = 3177

      If he had been given 1551, he could work out:

      x=3, y=11, z=47; V=1551, S=1382, E=244; V+S+E = 3177

      In either of these two cases Pip would have been able to declare both values at the first bell.

      He didn’t, so he must have been given 1815, which has the following decompositions:

      x=3, y=5, z=121; V=1815, S=1966, E=516; V+S+E = 4297
      x=3, y=11, z=55; V=1815, S=1606, E=276; V+S+E = 3697
      x=5, y=11, z=33; V=1815, S=1166, E=196; V+S+E = 3177

      There are multiple options, so Pip cannot declare at the first bell.

      Hence, of the three options, Estella knows Pip must have been given 1815 and can declare this at the next bell.

      Like

  • Unknown's avatar

    Jim Randell 5:53 pm on 23 December 2021 Permalink | Reply
    Tags: by: Nick MacKinnon,   

    Teaser 3092: A Christmas Carol 

    From The Sunday Times, 26th December 2021 [link] [link]

    A Christmas Carol was published on 19/12/1843, when Bob Cratchit was in his forties, almost seven years after Jacob Marley’s death on Christmas Eve. On Marley’s last Christmas Day, a working day for them all as always, Scrooge said to Cratchit and Marley: “We three will work the same dates next year as this year, except that I’ll cover Cratchit’s birthday so he can have the day off, since Marley never works on his”. On Boxing Day, Scrooge decided to allocate extra work dates for next year to anyone whose number of work dates was going to be below the average of the three of them, bringing them up to exactly that average. Daily, up to and including New Year’s Eve, Scrooge repeated this levelling-up to the new average, never needing fractions of a day.

    What was Bob Cratchit’s full date of birth?

    In the book The Sunday Times Teaser Book 2 (2023), the “levelling-up” process is described as follows:

    … Scrooge decided to allocate extra work dates for the next year to the one whose number of work dates was going to be below the average of the three of them, bringing him up to exactly that average. Daily, up to and including New Year’s Eve, Scrooge repeated this levelling-up for that person to the new average, never needing fractions of a day.

    Which is intended to indicate that only one of the three is “levelled-up” each time the process is applied.

    [teaser3092]

     
    • Jim Randell's avatar

      Jim Randell 5:55 pm on 23 December 2021 Permalink | Reply

      I’m not sure I understand how this is meant to work as a puzzle.

      If Cratchit is in his forties on 1843-12-19, then he was born sometime between 1794 and 1803.

      And, if Marley died on 1836-12-24, his last Christmas Day would be 1835-12-25, so Scrooge was planning the dates for 1836.

      All we know for sure (for the initial schedule) is that they all work every Christmas Day, Marley never works on his birthday, and Scrooge will work on Cratchit’s birthday in 1836 but Cratchit won’t.

      So the number of possible workdays initially scheduled for 1836 is: Scrooge 2-366, Cratchit 1-365, Marley 1-365.

      I found 3429 initial assignments of numbers of work days in 1836 that would allow Scrooge to perform his procedure 6 times.

      For example if the initial number of allocated work days is (99, 268, 308) we have:

      26th: (99, 268, 308), mean = 225 → (225, 268, 308)
      27th: (225, 268, 308), mean = 267 → (267, 268, 308)
      28th: (267, 268, 308), mean = 281 → (281, 281, 308)
      29th: (281, 281, 308), mean = 290 → (290, 290, 308)
      30th: (290, 290, 308), mean = 296 → (296, 296, 308)
      31st: (296, 296, 308), mean = 300 → (300, 300, 308)

      But there doesn’t seem to be any specified conditions to allow us to narrow down these numbers to a set that would allow us to determine a specific date for Cratchit’s birthday.

      However, there is a particular date that suggests itself as the only one of the candidate birthdates that has a certain property, so maybe that is the required answer. And it is the same unique date that we get if we make certain assumptions about the behaviour of Scrooge.

      Solution: [See below]

      Like

      • Jim Randell's avatar

        Jim Randell 10:22 am on 1 January 2022 Permalink | Reply

        The published puzzle text is flawed.

        Apparently the setter intended that only one of the three would be “levelled up” each time the process is applied.

        This leads to a single levelling up process:

        26th: (1, 365, 366), mean = 244 → (244, 365, 366)
        27th: (244, 365, 366), mean = 325 → (325, 365, 366)
        28th: (325, 365, 366), mean = 352 → (352, 365, 366)
        29th: (352, 365, 366), mean = 361 → (361, 365, 366)
        30th: (361, 365, 366), mean = 364 → (364, 365, 366)
        31st: (364, 365, 366), mean = 365 → (365, 365, 366)

        So, before the “levelling up” process started the work days allocated were 1, 365, 366.

        Scrooge must be 366, as he is working an extra day compared to 1835, and there were 365 days in 1835. The extra day worked is the leap day in 1836, and this must also be Cratchit’s birthday.

        Cratchit must be 365, as Marley did not work on his birthday in 1835.

        Hence, Marley must be the 1. He only worked Christmas day.

        So the intended answer was:

        Solution: Bob Cratchit was born on 29th February 1796.

        And this was the answer I suspected, as it was the only date with a certain property in the required range (i.e. the only 29th February), without worrying about the “levelling up” stuff at all.

        Like

  • Unknown's avatar

    Jim Randell 4:36 pm on 19 November 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3087: Hexagonia 

    From The Sunday Times, 21st November 2021 [link] [link]

    Hexagonia is a hexagonal republic and is divided into 24 numbered counties, as shown. The counties are to be reorganised into four departments, each composed of six edge-connected counties. No two departments will have the same shape or be reflections of each other, and the president’s residence in Department A will be built on an axis of symmetry of the department. Every sum of the county numbers in a department will be, in the prime minister’s honour, a prime number, and her mansion will be built in the middle of a county in Department B, on an axis of symmetry of the department, and as far as possible from the president’s residence.

    In what county will the Prime Minister’s mansion be built?

    This is the next puzzle after Teaser 2401 whose number has the property described in Teaser 2700.

    [teaser3087]

     
    • Jim Randell's avatar

      Jim Randell 5:30 pm on 19 November 2021 Permalink | Reply

      It looks like I implemented my polyiamonds.py module at just the right time (see Teaser 2838).

      I added the other hexiamonds to it and then used it to generate the only possible map. From this we see there are only 2 achiral shapes involved, and only one of them has an axis of symmetry that passes through the counties, so the required answer is determined directly from the map.

      The following Python program runs in 1.16s. Although by limiting the sets of pieces tested to those containing 2 chiral shapes, this can be reduced to 668ms.

      Run: [ @replit ]

      from enigma import (subsets, join, primes, printf)
      import polyiamonds
      
      # collect the possible hexiamonds
      shapes = polyiamonds.shapes("O6 I6 C6 E6 F6 G6 H6 J6 P6 S6 V6 X6".split())
      
      # map cells of the 24-hex grid to county numbers
      cells = [
        (0, 6), (0, 7), (1, 6), (1, 7), (2, 6), # 1 - 5
        (0, 4), (0, 5), (1, 4), (1, 5), (2, 4), (2, 5), (3, 4), # 6 - 12
        (0, 3), (1, 2), (1, 3), (2, 2), (2, 3), (3, 2), (3, 3), # 13 - 19
        (1, 1), (2, 0), (2, 1), (3, 0), (3, 1), # 20 - 24
      ]
      county = dict((cell, i) for (i, cell) in enumerate(cells, start=1))
      grid = set(cells)
      
      primes.extend(130)
      
      # choose 4 of the shapes
      for ps in subsets(shapes.keys(), size=4):
      
        # attempt to fit the shapes into the grid
        ss = list(shapes[p] for p in ps)
        for g in polyiamonds.fit(ss, grid):
          # the sum of the counties in each region should be prime
          t = dict((n, 0) for n in (1, 2, 3, 4))
          for (k, v) in g.items():
            t[v] += county[k]
          if not all(v in primes for v in t.values()): continue
      
          printf("{ps}\n", ps=join(ps, sep=" ", enc="[]"))
          polyiamonds.output_grid(g)
      

      Solution: The Prime Minister’s Mansion is in county 16.

      The layout of the departments is as follows:

      The departments have the following prime totals: blue (H6) = 31; red (C6) = 61; green (E6) = 101; orange (J6) = 107.

      The two departments with axes of symmetry are the red one (C6) and the green one (E6).

      The red one’s axis is along the 6/13 border, so this gives us the location of the President’s Residence (so Department A is red), and the axis in the green department (Department B) is indicated with the dotted line. This passes through counties 15 and 16, but we want to be as far from the President’s Residence as possible, so the Prime Minister’s Mansion must be in county 16.

      In fact C6 is the only achiral piece that can give a prime total, and has an axis on a border, so C6 must be involved in the map with one of E6 or V6 (which are the remaining achiral pieces with axes that pass through cells). And the remaining 2 counties must be chosen from F6, G6, H6, I6, J6, P6, S6 (chiral).

      Like

      • Jim Randell's avatar

        Jim Randell 10:28 pm on 19 November 2021 Permalink | Reply

        Here is a faster version, where we only consider combinations of 2 achiral and 2 chiral pieces, and positions of pieces in the grid are only considered if the counties involved have a prime sum.

        It runs in 67ms.

        Run: [ @replit ]

        from enigma import (subsets, join, primes, defaultdict, intersect, exact_cover, printf)
        import polyiamonds
        
        # collect the possible hexiamonds
        achiral = "O6 C6 E6 V6 X6".split()
        chiral = "I6 F6 G6 H6 J6 P6 S6".split()
        shapes = polyiamonds.shapes(achiral + chiral)
        
        # map cells of the 24-hex grid to county numbers
        cells = [
          (0, 6), (0, 7), (1, 6), (1, 7), (2, 6), # 1 - 5
          (0, 4), (0, 5), (1, 4), (1, 5), (2, 4), (2, 5), (3, 4), # 6 - 12
          (0, 3), (1, 2), (1, 3), (2, 2), (2, 3), (3, 2), (3, 3), # 13 - 19
          (1, 1), (2, 0), (2, 1), (3, 0), (3, 1), # 20 - 24
        ]
        county = dict((cell, i) for (i, cell) in enumerate(cells, start=1))
        grid = set(cells)
        
        primes.extend(130)
        
        # look for placements that give a prime total
        ss = defaultdict(list)
        for (k, vs) in shapes.items():
          for cs in polyiamonds.placements(vs, grid):
            if sum(county[c] for c in cs) in primes:
              ss[k].append(cs)
        
        achiral = sorted(intersect([achiral, ss.keys()]))
        chiral = sorted(intersect([chiral, ss.keys()]))
        
        # choose 2 achiral shapes
        for ps1 in subsets(achiral, size=2):
          # and the rest are chiral
          for ps2 in subsets(chiral, size=2):
            ps = ps1 + ps2
            # look for an exact cover using these pieces
            for rs in exact_cover(list(ss[p] for p in ps), grid):
              # output solution
              printf("{ps}\n", ps=join(ps, sep=" ", enc="[]"))
              g = dict()
              for (i, cs) in enumerate(rs, start=1):
                g.update((c, i) for c in cs)
              polyiamonds.output_grid(g)
        

        Like

      • Frits's avatar

        Frits 5:20 pm on 20 November 2021 Permalink | Reply

        @Jim, you don’t answer the question

        Like

        • Jim Randell's avatar

          Jim Randell 5:48 pm on 20 November 2021 Permalink | Reply

          @Frits: I reveal the answers to competition puzzles after the deadline for entries is closed.

          Like

          • Frits's avatar

            Frits 6:57 pm on 20 November 2021 Permalink | Reply

            @Jim, I mean that your program doesn’t seem to print in what county will the Prime Minister’s mansion be built. You normally don’t hide the answer in your program output. It doesn’t matter.

            Like

  • Unknown's avatar

    Jim Randell 9:08 am on 16 November 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2838: King Lear IV 

    From The Sunday Times, 12th February 2017 [link] [link]

    King Lear IV’s realm consisted of a regular hexagon divided into 24 counties that were equal-sized equilateral triangles. In his will he wanted to share the counties among his six daughters, each daughter’s portion having the property that, if you walked in a straight line between any two points in it, then you remained in her portion. If two daughters’ portions had the same area then they had to be of different shapes (and not the mirror image of each other). He wanted Cordelia to have a single county (his favourite county on the edge of the kingdom), he wanted Goneril to have a hexagonal-shaped portion, and he knew the number of counties he wanted to allocate to each of the other daughters, with Reagan’s portion being the largest of all. It turned out that his requirements uniquely determined Goneril’s and Reagan’s counties.

    What, in increasing order, were the numbers of counties allocated to the six daughters?

    [teaser2838]

     
    • Jim Randell's avatar

      Jim Randell 9:10 am on 16 November 2021 Permalink | Reply

      I originally solved this puzzle using the Polyform Puzzler framework [link], the problem then becomes how to interface to framework, but it allowed me to quickly produce a solution using a variation on the code I had previously written for Enigma 321.

      But having subsequently packaged my polyominoes code written for Enigma 321 into a separate module, I thought I could do the same for polyiamonds.

      So I wrote a package polyiamonds.py [@github] that knows enough about the convex shapes that fit into the 24-hex grid, and used that to solve this puzzle.

      Here is a reference for polyiamonds [link]. I used the same names, but added octiamonds “d8” (= “diamond”) and “t8” (= “trapezium”) to produce a complete set of convex polyiamonds that will fit in the required hexagonal grid. In total there are 12 possible shapes.

      The program looks for 4 additional pieces to go with “T1” and “O6” (which we are told are there), and one of these pieces must be larger than “O6”. The position of the “T1” piece is fixed along the edge of grid. The program then looks for packings where the positions of Regan’s (the largest piece) and Goneril’s piece (“O6”) are uniquely determined. It runs in 67ms.

      Run: [ @replit ]

      from enigma import (defaultdict, subsets, diff, union, irange, ordered, join, printf)
      import polyiamonds
      
      # convex polyiamonds up to size 8 that fit in the hexagon
      pieces = {
        "T1": 1,
        "D2": 2,
        "I3": 3,
        "T4": 4, "I4": 4,
        "I5": 5,
        "O6": 6, "I6": 6,
        "I7": 7, "D7": 7,
        "d8": 8, "t8": 8,
      }
      
      # collect the shapes
      shapes = polyiamonds.shapes(pieces.keys())
      
      # make the hexagonal grid
      grid = union(set((x, y) for y in irange(y1, y2)) for (x, y1, y2) in [(0, 3, 7), (1, 1, 7), (2, 0, 6), (3, 0, 4)])
      # remove (1, 1) for T1
      grid.remove((1, 1))
      
      # accumulate results by the position of G (= O6) and R (= largest)
      def key(g, ps):
        cells = lambda g, k: ordered(*(c for (c, n) in g.items() if n == k))
        return (cells(g, ps.index("O6") + 1), cells(g, len(ps)))
      
      # choose 4 pieces to go with T1 and O6
      r = defaultdict(set)
      for ps in subsets(diff(pieces.keys(), ["T1", "O6"]), size=4, fn=list):
        # check the size
        ss = list(pieces[p] for p in ps)
        m = max(ss)
        if not (m > 6 and sum(ss) == 17 and ss.count(m) == 1): continue
      
        # attempt to fit the shapes into the grid
        ps.extend(["T1", "O6"])
        ps = sorted(ps, key=lambda p: pieces[p])
        ks = tuple(pieces[p] for p in ps)
        printf("{ps} -> {ks}\n", ps=join(ps, sep=" ", enc="[]"), ks=join(ks, sep=" ", enc="()"))
        n = 0
        for g in polyiamonds.fit(list(shapes[p] for p in ps if p != "T1"), grid, start=2):
          r[ks].add(key(g, ps))
          g[(1, 1)] = 1 # place T1 as 1
          polyiamonds.output_grid(g)
          n += 1
        printf("[{n} ways]\n")
      
      # look for a unique solution
      for (k, vs) in r.items():
        if len(vs) == 1:
          printf("solution = {k}")
      

      Solution: The numbers of counties allocated to the daughters are: 1, 2, 4, 4, 6, 7.

      I found three allocations that allow the grid to be packed. They are summarised in the following diagram:

      (Click to enlarge).

      The allocation that has only one way to pack the grid gives the solution.

      Like

  • Unknown's avatar

    Jim Randell 5:04 pm on 15 October 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3082: In the swim 

    From The Sunday Times, 17th October 2021 [link] [link]

    Expert mathematicians Al Gritham, Ian Tadger, Carrie Over and Tessa Mole-Poynte have a swimming race for a whole number of lengths. At some point during each length (so not at the ends) they calculate that they have completed a fraction of the distance, and at the finish they compare their fractions (all in the lowest terms). Al says, “My fractions have prime numerators, the set of numerators and denominators has no repeats, their odd common denominator has two digits, and the sum of my fractions is a whole number”. Tessa says, “Everybody’s fractions have all the properties of Al’s, but one of mine is closer to an end of the pool than anybody else’s”.

    What were Tessa’s fractions?

    [teaser3082]

     
    • Jim Randell's avatar

      Jim Randell 10:00 pm on 15 October 2021 Permalink | Reply

      The puzzle doesn’t say anything about the sets of fractions being different, but I think we need to assume that to find a unique solution. (Otherwise 3 of the 4 sets could be the same, with the largest measure, and Tessa’s could be one of the three possible sets that have a smaller measure).

      The program finds all possible sets, collected by the number of lengths in the race, and finds which has the smallest measure.

      I turns out there is only one possible number of lengths, and there are four possible sets of fractions, so if each is assigned to one of the participants Tessa’s must be the one with the minimum measure.

      This Python program runs in 67ms.

      Run: [ @replit ]

      from enigma import (Primes, irange, fraction, mlcm, group, fdiv, Accumulator, printf)
      
      # primes up to 100
      primes = Primes(100)
      
      # select fractions with different numerators and denominators, that sum to a whole number
      def select(ss, rs=[], ns=set(), t=(0, 1)):
        # are we done?
        if not ss:
          if t[1] == 1:
            yield rs
        else:
          # choose the next fraction
          for (a, b) in ss[0]:
            if not (a in ns or b in ns):
              yield from select(ss[1:], rs + [(a, b)], ns.union((a, b)), fraction(a, b, *t))
      
      # generate sets of fractions that satisfy the conditions
      def generate():
      
        # consider the 2-digit, odd common denominator
        for d in irange(11, 99, step=2):
          # must be more than one possible denominator
          if d in primes: continue
      
          # calculate the possible fractions
          fs = list()
          for n in irange(1, d - 1):
            (a, b) = fraction(n, d)
            if a in primes:
              fs.append((a, b))
      
          # consider number of lengths (each denominator must be different)
          for k in irange(2, len(set(b for (a, b) in fs))):
      
            # put the fractions into the corresponding lengths
            lengths = list(list() for _ in irange(k))
            for (a, b) in fs:
              (i, r) = divmod(a * k, b)
              if r > 0:
                lengths[i].append((a, b))
      
            # return possible sets of fractions
            if all(lengths):
              for ss in select(lengths):
                # check the common denominator
                d = mlcm(*(b for (a, b) in ss))
                if 10 < d < 100 and d % 2 == 1:
                  yield ss
      
      # group possible sets of fractions by number of lengths
      d = group(generate(), by=len)
      
      # consider number of lengths
      for k in sorted(d.keys()):
        rs = d[k]
        if len(rs) > 1:
          printf("{k} lengths -> {n} sets", n=len(rs))
          # output sets, and measure
          Q = fdiv
          r = Accumulator(fn=min, collect=1)
          for ss in rs:
            m = min(min(Q(a, b) - Q(i, k), Q(i + 1, k) - Q(a, b)) for (i, (a, b)) in enumerate(ss))
            r.accumulate_data(m, ss)
            printf("-> {ss} [{m:.2%}]")
          # output minimum measure sets
          for ss in r.data:
            printf("min = {ss}")
          printf()
      

      Solution: Tessa’s fractions were: 13/75, 7/15, 3/5, 19/25.

      So the race was over 4 lengths.

      Tessa’s fourth fraction 19/25 = 0.76, so is 1% of the total distance after the start of the final length.

      The other participants must have the following sets of fractions:

      13/63, 3/7, 5/9, 17/21
      7/75, 11/25, 3/5, 13/15
      23/99, 3/11, 5/9, 31/33

      Each set of fractions (including Tessa’s) sum to 2.


      We can show that the number of lengths must be even:

      If we consider a k length race. Then the fractions are:

      0/k < a < 1/k
      1/k < b < 2/k
      2/k < c < 3/k

      (k − 1)/k < z < k/k

      From which we see:

      T(k − 1)/k < a + b + c + … + z < T(k)/k
      (1/2)(k − 1) < sum < (1/2)(k + 1)

      If k is even, k = 2x:

      (1/2)(2x − 1) < sum < (1/2)(2x + 1)
      x − 1/2 < sum < x + 1/2

      and as the sum is an integer we have: sum = x.

      If k is odd, k = 2x + 1:

      (1/2)(2x) < sum < (1/2)(2x + 2)
      x < sum < x + 1

      And there are no integer solutions.

      So k must be even (and the sum of the fractions must be k/2).

      And k cannot be 2, as then the fractions would need to be (a / b) and 1 − (a / b) = (b − a) / b which have the same denominator.

      We could use this observation to reduce the number of cases considered by the program, line 34 becomes:

      for k in irange(4, len(set(b for (a, b) in fs)), step=2):
      

      Then only denominators of 45, 63, 75, 81, 99 are considered, and only races consisting of 4 lengths.

      Like

    • Frits's avatar

      Frits 2:04 pm on 16 October 2021 Permalink | Reply

      Using Jim’s approach as base but trying to do it a little bit different.

      Quite a complex puzzle this week.

        
      from enigma import fraction
      from collections import defaultdict
      from itertools import compress
      
      def primesbelow(n):
        # rwh_primes1v2(n):
        """ Returns a list of primes < n for n > 2 """
        sieve = bytearray([True]) * (n // 2 + 1)
        for i in range(1, int(n ** 0.5) // 2 + 1):
          if sieve[i]:
            sieve[2 * i * (i + 1)::2 * i + 1] = \
            bytearray((n // 2 - 2 * i * (i + 1)) // (2 * i + 1) + 1)
        return [2, *compress(range(3, n, 2), sieve[1:])]
      
      P = primesbelow(100)
                                
      # select fractions with different numerators and denominators, 
      # that sum to a whole number
      def select(ss, k, rs=[], ns=set(), t=(0, 1)):
        nr_processed = len(rs)
        # are we done?
        if nr_processed == k:
          if t[1] == 1:
            yield rs
        else:
          low = nr_processed / k
          hgh = (nr_processed + 1) / k
          for i, (a, b) in enumerate(ss):
            # only select fractions in the same length 
            f = a / b
            if f <= low: continue
            if f >= hgh: break
            
            # choose the next fraction
            if not(a in ns or b in ns):
              yield from select(ss[i + 1:], k, rs + [(a, b)], ns.union((a, b)), 
                            fraction(a, b, *t))   
      
      d = defaultdict(list)
      
      # odd 2-digit common denominators
      for cd in range(11, 100, 2):
        # must be more than one possible denominator
        if cd in P: continue
        
        # generate sets of fractions with prime numerators
        fs = [f for n in range(1, cd) if (f := fraction(n, cd))[0] in P]
        
        k = 2
        while True:
          # break if there is a gap in the set of length numbers
          if len(set((k * a) // b for a, b in fs)) != k:
            break
         
          # select different fractions that sum to a whole number
          for s in select(fs, k):
            # check on 2-digit common denominator, no odd check needed
            if max(b for a, b in s) < 10: continue
            
            d[k].append(s)
          k += 1  
      
      for k, vs in d.items():
        # we need enough data for 4 mathematicians
        if len(vs) < 4: continue
        
        # calculate difference from whole number      
        diffs = [(min((diff := ((k * a) / b) % 1), 1 - diff), v) 
                  for v in vs for a, b in v]
        
        # one of Tessa's fractions is the closest to an end of the pool         
        tessa = min(diffs)
        
        print(f"Tessa's fractions are: {tessa[1]}")      
      

      Like

      • Frits's avatar

        Frits 2:18 pm on 16 October 2021 Permalink | Reply

        Probably no such 1-digit common denominator can ever happen (line 58).

        Like

    • Hugh Casement's avatar

      Hugh Casement 1:44 pm on 24 October 2021 Permalink | Reply

      I don’t understand this puzzle. In fact, I don’t believe it can be understood.
      “Their odd common denominator has two digits” implies that the denominators are all the same, which does not tally with “the set of numerators and denominators has no repeats”.

      “… have completed a fraction of the distance” means to me a fraction of the current length.
      Tessa said that one of her fractions was the closest to an end of the pool; the largest of all Jim’s fractions is 31/33, not 19/25 ! Alternatively, 7/75 is the smallest.

      Altogether the setters of puzzles should say what they mean (and mean what they say, even if that is not the same thing, as the Mad Hatter protested).

      Like

  • Unknown's avatar

    Jim Randell 9:16 am on 5 October 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2828: Return to Zenda 

    From The Sunday Times, 4th December 2016 [link] [link]

    Each postcode in Ruritania consists of ten digits, the first three showing the area, the next three showing the town, and the final four showing the house. Just twelve houses have “lucky” postcodes that have no repeated digit and which consist of two three-figure squares followed by a four-figure square. Rudolph lives in such a house and he recently met a girl called Flavia. She only told him that hers was the only house in her area with a lucky postcode, and that her area/town/house numbers were all bigger than his. He has found a postcode satisfying these conditions and sent her a Christmas card, but it has been returned as it was not her postcode.

    What is Rudolph’s postcode?

    [teaser2828]

     
    • Jim Randell's avatar

      Jim Randell 9:16 am on 5 October 2021 Permalink | Reply

      I think the wording of this puzzle could be a bit clearer. But with a reasonable assumption that leading zeros are not allowed in the squares we get a unique solution.

      This Python program runs in 54ms.

      Run: [ @replit ]

      from enigma import (irange, is_duplicate, cproduct, subsets, filter_unique, unpack, join, printf)
      
      # find n-digit squares from a^2 to b^2 with no repeated digits (as
      # strings) leading zeros are allowed
      def squares(n, a, b):
        r = list()
        for i in irange(a, b):
          s = str(i * i).zfill(n)
          if not is_duplicate(s):
            r.append(s)
        return r
      
      # 3 and 4 digit squares
      s3s = squares(3, 10, 31)
      s4s = squares(4, 32, 99)
      
      # collect "lucky" (pandigital) postcodes
      lucky = list((area, town, house)
        for (area, town, house) in cproduct([s3s, s3s, s4s])
          if len(set(area + town + house)) == 10)
      printf("[{n} lucky postcodes]", n=len(lucky))
      
      # choose the 12 lucky postcodes
      for postcodes in subsets(lucky, size=12):
      
        # flavia's postcode is the only (possible) lucky postcode in the area
        fs = filter_unique(postcodes, unpack(lambda a, t, h: a)).unique
      
        # consider rudolph's postcode
        for (ra, rt, rh) in postcodes:
          # find possible postcodes for flavia
          f = list((a, t, h) for (a, t, h) in fs if a > ra and t > rt and h > rh)
          # and there must be more than one possibility
          if len(f) > 1:
            fmt = lambda s: join(s, sep=":", enc="()")
            printf("{r} -> {f}",r=fmt((ra, rt, rh)), f=join(map(fmt, f), sep=' '))
      

      Solution: Rudolph’s postcode is (324:576:1089).

      And there are two possible postcodes for Flavia: (961:784:3025) and (361:784:9025). So Rudolph chose the wrong one.

      We can allow the squares to have leading zeros by changing the value of the [[ a ]] parameter in the calls to [[ squares() ]]. But if we do this we find that are many possible solutions.


      The 12 “lucky” postcodes (area:town:house) are:

      (169:784:3025)
      (196:784:3025)
      (324:576:1089)
      (324:576:9801)
      (361:784:9025)
      (576:324:1089)
      (576:324:9801)
      (784:169:3025)
      (784:196:3025)
      (784:361:9025)
      (784:961:3025)
      (961:784:3025)

      Flavia’s postcode is unique by area:

      (169:784:3025)
      (196:784:3025)
      (361:784:9025)
      (961:784:3025)

      And Rudolph’s postcode must be one of the 12 lucky code above, that has two candidate postcodes for Flavia that are larger in each component. There is only one possibility for Rudolph’s postcode:

      (324:576:1089) → (361:784:9025) or (961:784:3025)


      Without leading zeros there are exactly 12 lucky postcodes, so we know all possible lucky postcodes are in use. (And this is probably the scenario the setter had in mind).

      However, if we allow leading zeros in the squares we can find 34 lucky postcodes, but we know only 12 of them are in use. But how does Flavia know that hers is the only lucky postcode in her area? Is it the only possible lucky postcode in her area? Or just the only one currently in use? In my program I have implemented the latter.

      And using this interpretation we find there are many thousands of situations that lead to a solution, with 6 possible values for Rudolph’s postcode. So I think it is probably safe to assume that the setter intended there to be no leading zeros in the squares.

      Like

      • Frits's avatar

        Frits 11:49 am on 9 June 2025 Permalink | Reply

        or more efficient

        # collect "lucky" (pandigital) postcodes
        lucky = list()
        for (area, house) in product(s3s, s4s):
          if len(ah := set(area + house)) == 7:
            for town in s3s:
              if ah.isdisjoint(town):
                lucky.append((area, town, house))
        

        Like

    • Frits's avatar

      Frits 11:53 am on 5 October 2021 Permalink | Reply

      Checking lucky postcodes outside “SubstitutedExpression” would have been easier and faster but this was more challenging.

        
      from enigma import SubstitutedExpression
      
      # the alphametic puzzle 
      p = SubstitutedExpression(
        [# Rudolph
         "is_square(ABC)",
         "is_square(DEF)",
         "is_square(GHIJ)",
         
         # does more than one Flavia candidate exist for Rudolph (ABC:DEF:GHIJ) ? 
         # Flavia's postcode (a:b:c) is unique by area
         "sum(1 for a in range(ABC + 1, 1000) if is_square(a) and \
                   sum(1 for t in range(100, 1000) if is_square(t) \
                           if len(set(str(a) + str(t))) == 6 \
                         for u in range(1000, 10000) if is_square(u) and \
                         len(set(str(a) + str(t) + str(u))) == 10 \
                       ) == 1 \
                for b in range(DEF + 1, 1000) if is_square(b) \
                  if len(set(str(a) + str(b))) == 6 \
                for c in range(GHIJ + 1, 10000) if is_square(c) and \
                len(set(str(a) + str(b) + str(c))) == 10 \
             ) > 1",
         
        ],
        answer="(str(ABC) + ':' + str(DEF) + ':' + str(GHIJ))",
        distinct=("ABCDEFGHIJ"),
        verbose=0,
      )
      
      # print answer
      for (_, ans) in p.solve():
        print(f"Rudolph's postcode is {ans}")
      

      Like

    • Frits's avatar

      Frits 1:33 pm on 5 October 2021 Permalink | Reply

      Similar.

        
      import sqlite3
      
      sqs = [str(i**2) for i in range(10, 100)]
      
      luckies = []
      for a in [x for x in sqs if len(x) == 3]:
        for b in [x for x in sqs if len(x) == 3]:
          ab = a + b
          if len(set(ab)) != 6: continue
          for c in [x for x in sqs if len(x) == 4]:
            if len(set(ab + c)) != 10: continue
            luckies.append([a, b, c])
      
      # connect to SQLite database that resides in the memory
      sqlite_Connection = sqlite3.connect('temp.db')
      c1 = sqlite3.connect(':memory:')
      
      c1.execute("CREATE TABLE postcodes ( \
                          area INT, \
                          town INT, \
                          house INT \
                  )")
      
      # insert entries into table
      for (x, y, z) in luckies:
        stmnt = "INSERT INTO postcodes VALUES ("
        stmnt += x + ", " + y + ", " + z + ")"
        c1.execute(stmnt)
      
      c1.commit()
      
      stmnt = "SELECT area, town, house, " 
      stmnt += "(SELECT COUNT(1) FROM postcodes B" 
      stmnt += " WHERE B.AREA > A.AREA and" 
      stmnt += "       B.TOWN > A.TOWN and" 
      stmnt += "       B.HOUSE > A.HOUSE and"
      stmnt += "       NOT EXISTS (SELECT 1"  # Flavia's postcode is unique by area
      stmnt += "         FROM postcodes C"
      stmnt += "          WHERE C.AREA = B.AREA and" 
      stmnt += "          (C.TOWN != B.TOWN or"
      stmnt += "           C.HOUSE != B.HOUSE)" 
      stmnt += "         )"
      stmnt += "       ) as nFlavia "
      stmnt += "FROM postcodes A " 
      stmnt += "WHERE nFlavia > 1;"
      
      for ans in c1.execute(stmnt):
        print(f"Rudolph's postcode is {ans[:-1]}")
        
      c1.close()  
      
      if (sqlite_Connection):
        sqlite_Connection.close()
      

      Like

    • GeoffR's avatar

      GeoffR 3:41 pm on 5 October 2021 Permalink | Reply

      A good variety of solutions for this teaser.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: sq3 = {x*x | x in 10..31};  
      set of int: sq4 = {x*x | x in 32..99};
       
      % Rudolph's digits in 3:3:4 digits for area:town:house numbers
      var 1..9:A; var 0..9:B; var 0..9:C; 
      var 1..9:D; var 0..9:E; var 0..9:F; 
      var 1..9:G; var 0..9:H; var 0..9:I; var 0..9:J;
      constraint all_different ([A,B,C,D,E,F,G,H,I,J]);
      
      % Flavia's digits - 1st set are abc:def:ghij
      var 1..9:a; var 0..9:b; var 0..9:c; 
      var 1..9:d; var 0..9:e; var 0..9:f; 
      var 1..9:g; var 0..9:h; var 0..9:i; var 0..9:j;
      constraint all_different([a,b,c,d,e,f,g,h,i,j]);
      
      % Flavia's digits - 2nd set are mno:pqr:stuv
      var 1..9:m; var 0..9:n; var 0..9:o; 
      var 1..9:p; var 0..9:q; var 0..9:r; 
      var 1..9:s; var 0..9:t; var 0..9:u; var 0..9:v;
      constraint all_different ([m,n,o,p,q,r,s,t,u,v]);
      
      % Rudolph's numbers
      var 100..999: ABC = 100*A + 10*B + C;
      var 100..999: DEF = 100*D + 10*E + F;
      var 1000..9999: GHIJ = 1000*G + 100*H + 10*I + J;
      
      % Flavia's numbers - 1st set
      var 100..999: abc = 100*a + 10*b + c;
      var 100..999: def = 100*d + 10*e + f;
      var 1000..9999: ghij = 1000*g + 100*h + 10*i + j;
      
      % Flavia's numbers - 2nd set
      var 100..999: mno = 100*m + 10*n + o;
      var 100..999: pqr = 100*p + 10*q + r;
      var 1000..9999: stuv = 1000*s + 100*t + 10*u + v;
      
      % Square constraints for Rudolph's and Flavia's numbers
      constraint ABC in sq3 /\ DEF in sq3 /\ GHIJ in sq4;
      constraint abc  in sq3 /\ def in sq3 /\ ghij in sq4;
      constraint mno in sq3 /\ pqr in sq3 /\ stuv in sq4;
      
      % Flavia's area/town/house numbers were all bigger than Rodolph's numbers
      % We conclude she has two numbers as Rudolph made a mistake on the first.
      constraint abc > ABC /\ def > DEF /\ ghij > GHIJ;
      constraint mno > ABC /\ pqr > DEF /\ stuv > GHIJ;
      
      % Flavia's two sets of numbers are different areas/ house numbers
      % ... but make Flavia's town the same in both sets of numbers
      constraint mno > abc /\ pqr == def /\ ghij > stuv;
      
      solve satisfy;
      
      output ["Rudolph's numbers : " ++ show([ABC, DEF, GHIJ])
      ++ "\n" ++ "Flavia's numbers (1st set): " ++ show([abc, def,ghij])
      ++ "\n" ++ "Flavia's numbers (2nd set): " ++ show([mno, pqr, stuv]) ];
      
      % Rudolph's numbers : [324, 576, 1089]
      % Flavia's numbers (1st set): [361, 784, 9025]
      % Flavia's numbers (2nd set): [961, 784, 3025]
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:33 pm on 3 September 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3076: Bee lines 

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

    Three bees are each trapped inside an empty cuboidal box, each box of a different size, none of whose faces are squares. The lengths of the internal edges of each box in centimetres are whole numbers, and the volume of each box is no more than a litre. Starting at a corner, each bee moves only in straight lines, from corner to corner, until it has moved along every edge of its box. The only points a bee visits more than once are corners of its box, and the total distance moved by each bee is a whole number of centimetres. Given the above, the sum of these three distances is as small as it could be.

    What is the sum of the distances that the bees moved?

    [teaser3076]

     
    • Jim Randell's avatar

      Jim Randell 5:40 pm on 3 September 2021 Permalink | Reply

      After a couple of false starts I think I have found the answer.

      We assume the bees are points of negligible size travelling along the inside edges and faces of the boxes, which have internal integer dimensions. [Note: This may not be what the setter intends, see my additional comment below].

      If the cuboid has (increasing, internal) dimensions x, y, z, then in order to traverse all 12 edges (distance = 4x + 4y + 4z)) we need to also traverse the diagonals of 3 of the faces (as we do not need to form a closed path). So we need the diagonals of (at least) two of the three different face shapes to have integer values.

      This Python program looks for viable cuboid shapes, and then finds three with the shortest distances traversed. It runs in 62ms.

      Run: [ @replit ]

      from enigma import (irange, is_square, subsets, group, unpack, printf)
      
      # integer diagonal?
      diag = lambda *vs: is_square(sum(v * v for v in vs))
      
      # find possible cuboid dimensions (x, y, z) and distance traversed d
      # return (x, y, z, d)
      def generate():
        for z in irange(3, 499):
          for y in irange(2, z - 1):
            if not (y * z < 1000): break
            dyz = diag(y, z)
            for x in irange(1, y - 1):
              if not (x * y * z < 1000): break
              dxy = diag(x, y)
              dxz = diag(x, z)
              # calculate paths
              d = 4 * (x + y + z)
              for (a, b) in subsets(filter(None, (dxy, dxz, dyz)), size=2):
                dx = 2 * a + b
                yield (x, y, z, d + dx)
      
      # group cuboids by distance
      g = group(generate(), by=unpack(lambda x, y, z, d: d))
       
      # find 3 different cuboids with the smallest distances
      ss = set()
      t = 0
      for k in sorted(g.keys()):
        for (x, y, z, d) in g[k]:
          if (x, y, z) not in ss:
            ss.add((x, y, z))
            t += d
            printf("[{n}: ({x}, {y}, {z}) -> {d}]", n=len(ss))
            if len(ss) == 3: printf("total distance = {t}")
        if len(ss) >= 3: break
      

      The program above finds minimal paths where the bee is constrained to the interior surface of the box, but if the bee is permitted to fly between diametrically opposite corners of the box we get a different answer (see comment below for modified program).

      Solution: The sum of the distances is 397 cm.


      The net of a cuboid has 8 vertices, each with three edges to other vertices. In order to make a continuous path we need there to be only two vertices of odd order (for the start and finish of the path), or all vertices to be even (for a path that starts and finishes at the same vertex).

      Adding a diagonal link between two vertices, can reduce the number of even vertices by 2. Hence by adding in three diagonals between different vertices we can make a net with two vertices of odd order, which can be traversed (with these vertices as the start and finish of the path).

      We need to traverse all 12 edges, and an additional 3 diagonals, giving a minimum path of 15 lines. We can’t use diagonals that have points in common (e.g. we can only cross a face once), as the only points the bee is allowed to revisit are the vertices.

      Here is a diagram showing the traversal of the edges of a cuboid in 15 lines, using three “face” diagonals:

      (The path could be closed by traversing the diagonal cross the front of the box, but this is not required by the puzzle, and wouldn’t provided a minimal length path).

      To minimise the length of the path we want the paths that cross opposite faces (i.e. 6 and 14) to be the shortest available diagonal, and the remaining diagonal (i.e. 8) to be the next shortest.

      We find there are three viable boxes for this method:

      5 × 9 × 12: volume = 540; distance = 145
      6 × 8 × 15: volume = 720; distance = 153
      5 × 12 × 16: volume = 960; distance = 178

      Together these constitute the answer if the bee is restricted to crossing the interior surface of the box, and give a total of 476 cm.

      However, if we suppose the bee is permitted to fly (in a straight line) between diametrically opposite corners of the box (the puzzle text isn’t explicit on whether this is allowed or not), then there is another potential family of paths:

      Here the “space” diagonal through the interior space of the box is shown as a curved line, but the bee travels in a straight line between opposite vertices. And all four of the “space” diagonals meet at the central point of the cuboid, so we can only use one of them.

      (In this case closing the path would require traversing another “space” diagonal, so is not possible).

      This method provides another two viable boxes:

      3 × 4 × 12: volume = 144; distance = 99
      8 × 9 × 12: volume = 864; distance = 165

      Using the smallest of these, along with the smallest 2 previously found boxes gives a total distance of 397 cm.

      This latter distance is the published solution, so the bee must be allowed to fly (in a straight line) between diametrically opposite corners.

      Like

      • Frits's avatar

        Frits 10:53 pm on 3 September 2021 Permalink | Reply

        @Jim, I don’t know what you mean by “three edges”. I have the feeling your first solution was better (as I can visualize the route of the bee and it has a smaller distance).

        “no more than a litre” can be coded more accurately.

        Like

        • Jim Randell's avatar

          Jim Randell 9:05 am on 4 September 2021 Permalink | Reply

          @Frits: I realised that the path my previous solution was based on didn’t meet all the necessary requirements of the puzzle, so I had to revise my approach. (Which gave a longer, but more correct answer).

          Once a viable path for traversing the edges of the cuboid is worked out, you can proceed to look for paths which have integer lengths. (I’m going to put the path I worked out up with the solution, but if you want to see it now it is [here]).

          The only slightly worrying thing is that the part about minimising the sum. With my current approach there are only three possible boxes, so only one possible sum.

          Like

          • Jim Randell's avatar

            Jim Randell 11:15 am on 4 September 2021 Permalink | Reply

            I think the minimal sum comes about because there are longer possible paths on the same cube. (We can generate these by adding a [[ select="P" ]] parameter to the [[ subsets() ]] call on line 15. Without this it only generates the shorter variants).

            Like

      • Jim Randell's avatar

        Jim Randell 10:13 pm on 4 September 2021 Permalink | Reply

        If the bee is allowed to travel (fly) along a path through the centre of the cube to the opposite corner (something my original approach did not allow), we can adapt the program to include this diagonal as follows:

        Run: [ @replit ]

        from enigma import (irange, is_square, group, unpack, printf)
        
        # integer diagonal?
        diag = lambda *vs: is_square(sum(v * v for v in vs))
        
        # find possible cuboid dimensions (x, y, z) and minimal distance traversed d
        # return (x, y, z, d)
        def generate():
          for z in irange(3, 499):
            for y in irange(2, z - 1):
              if not (y * z < 1000): break
              dyz = diag(y, z)
              for x in irange(1, y - 1):
                if not (x * y * z < 1000): break
                dxy = diag(x, y)
                dxz = diag(x, z)
                dxyz = diag(x, y, z)
                # calculate minimal path
                ds = list(x for x in (dxy, dxz, dyz, dxyz) if x is not None)
                if len(ds) > 1:
                  (a, b) = ds[:2]
                  yield (x, y, z, 4 * (x + y + z) + 2 * a + b)
        
        # group cuboids by distance
        g = group(generate(), by=unpack(lambda x, y, z, d: d))
         
        # find 3 different cuboids with the smallest distances
        n = t = 0
        for k in sorted(g.keys()):
          for (x, y, z, d) in g[k]:
            n += 1
            t += d
            printf("[{n}: ({x}, {y}, {z}) -> {d}]")
            if n == 3: printf("total distance = {t}")
          if n >= 3: break
        

        This brings 2 more cuboids in to play, so there are now 5 to choose from.

        This is probably the scenario intended by the setter.

        (Although maybe the bee could cross a face diagonally in one direction by walking, and cross it in the other direction by flying, and thereby avoid visiting the centre of the face twice. This would bring me full circle back to my original (abandoned) approach where the bee crosses one face by 2 different diagonals).

        Like

        • Jim Randell's avatar

          Jim Randell 1:02 pm on 5 September 2021 Permalink | Reply

          I also wrote a program that performs an exhaustive search for the minimal path on each cuboid [@replit], which verifies that the programs above do find the minimal length paths in the cases where the space diagonal isn’t or is allowed.

          from enigma import (irange, inf, Accumulator, is_square, group, unpack, printf)
          
          # vertices are labelled: 0 - 8
          # moves are labelled by midpoints 0-19 (0 = n/a, +12 edges, +6 faces, +1 centre)
          
          # matrix of vertex to vertex moves, values = midpoints
          move = [
            #  0   1   2   3   4   5   6   7
            [  0,  1, 13,  4, 15, 19, 16,  9 ], # 0
            [  1,  0,  2, 13, 19, 17, 10, 16 ], # 1
            [ 13,  2,  0,  3, 18, 11, 17, 19 ], # 2
            [  4, 13,  3,  0, 12, 18, 19, 15 ], # 3
            [ 15, 19, 18, 12,  0,  5, 14,  8 ], # 4
            [ 19, 17, 11, 18,  5,  0,  6, 14 ], # 5
            [ 16, 10, 17, 19, 14,  6,  0,  7 ], # 6
            [  9, 16, 19, 15,  8,  14, 7,  0 ], # 7
          ]
          
          # find minimal path, visiting tgt midpoints
          # ws = weights for each midpoint 0-19, (0 for invalid midpoints)
          # tgt = target midpoints to collect
          # vs = vertices visited so far
          # r = accumulator for minimal path
          # ms = midpoints visited (so far)
          # t = total weight of path (so far)
          def path(ws, tgt, vs, r, ms=set(), t=0):
            # are we done?
            if tgt.issubset(ms):
              r.accumulate_data(t, vs)
            else:
              # extend the path
              for (v, m) in enumerate(move[vs[-1]]):
                if (not m) or (not ws[m]) or m in ms: continue
                t_ = t + ws[m]
                if not (t_ > r.value):
                  path(ws, tgt, vs + [v], r, ms.union([m]), t_)
          
          # integer diagonal?
          diag = lambda *vs: is_square(sum(v * v for v in vs))
          
          # find possible cuboid dimensions (x, y, z) and minimal distance traversed d
          # return (x, y, z, d)
          def generate():
            for z in irange(3, 499):
              for y in irange(2, z - 1):
                if not (y * z < 1000): break
                dyz = diag(y, z)
                for x in irange(1, y - 1):
                  if not (x * y * z < 1000): break
                  dxy = diag(x, y)
                  dxz = diag(x, z)
                  dxyz = diag(x, y, z)
                  # calculate minimal weight path, visiting all edges of the cuboid
                  ws = [0, x, y, x, y, x, y, x, y, z, z, z, z, dxy, dxy, dyz, dxz, dyz, dxz, dxyz]
                  tgt = set(irange(1, 12))
                  r = Accumulator(fn=min, value=inf)
                  path(ws, tgt, [0], r)
                  if r.value < inf:
                    printf("({x}, {y}, {z}) -> {r.value}")
                    yield (x, y, z, r.value)
          
          # group cuboids by distance
          g = group(generate(), by=unpack(lambda x, y, z, d: d))
           
          # find 3 different cuboids with the smallest distances
          n = t = 0
          for k in sorted(g.keys()):
            for (x, y, z, d) in g[k]:
              n += 1
              t += d
              printf("[{n}: ({x}, {y}, {z}) -> {d}]")
              if n == 3: printf("total distance = {t}")
            if n >= 3: break
          

          (To disallow the space diagonal replace [[ dxyz ]] with [[ 0 ]] on line 54).

          Like

    • Brian Gladman's avatar

      Brian Gladman 8:37 am on 4 September 2021 Permalink | Reply

      @Frits I don’t know what Jim’s earlier answer was but my current answer agrees with Jim’s current one. However, I must admit that I got there after a lot of path sketching to find the form of the shortest path and it is quite possible that I have missed a shorter arrangement.

      Like

    • Frits's avatar

      Frits 1:42 pm on 4 September 2021 Permalink | Reply

      If we need to traverse the diagonals of (at least) two of the three different face shapes we can look for Pythagorean triples that share a non-hypotenuse side.

        
      from enigma import pythagorean_triples
      
      pt = list(pythagorean_triples(200))
      
      s = set()
      # check if pythogorean triples share a non-hypotenuse side
      for p1 in pt:
        for p2 in pt:
          if p2 == p1: continue
          
          if p1[0] in p2[:2]:
            match = p1[0]
          elif p1[1] in p2[:2]:
            match = p1[1]
          else:
            continue   
         
          n3 = p2[0] if p2[1] == match else p2[1]  
          
          if p1[0] * p1[1] * n3 <= 1000:
            d = 4 * (p1[0] + p1[1] + n3) + 2 * (min(p1[2], p2[2])) +  max(p1[2], p2[2]) 
            s.add((d, ) + tuple(sorted([p1[0], p1[1], n3])))
            
      # more than 3 boxes?      
      if len(s) > 2:
        sol = sorted(s)[:3]
        print("answer:", sum(x[0] for x in sol))
        for x in sol: 
          print(x[1:], "with distance", x[0])
      

      Like

    • GeoffR's avatar

      GeoffR 7:25 am on 7 September 2021 Permalink | Reply

      I realise that my code can probably be shortened, but I wanted to show the full workings of this solution and therefore code optimisation was of secondary importance to me. My solution finds the five cuboids mentioned and the three minimum cuboids for the triple flight path solution.

      from itertools import combinations
      
      C = []  # list of cuboid flight path totals
      
      from math import isqrt
      
      def is_sq(x):
         return isqrt(x) ** 2 == x 
       
      # consider possible cuboid dimensions 
      for a, b, c in combinations(range(3, 50), 3):
        # each cuboid is less than one litre in volume
        if a * b * c <= 1000:
           
          # identify types of squares from dimensions a, b and c
          s1 = a*a + b*b
          s2 = a*a + c*c
          s3 = b*b + c*c
          s4 = a*a + b*b + c*c
          
          # separate research showed there were just two squares for each path
          if sum([is_sq(s1), is_sq(s2), is_sq(s3), is_sq(s4)]) == 2:
            # Type s1 and s2 squares contribute
            if is_sq(s1) and is_sq(s2):
              path = 4 *(a + b + c) + 2 * isqrt(a*a + b*b) + isqrt(a*a + c*c)
              C.append(path)
            # Type s1 and s3 squares contribute
            if is_sq(s1) and is_sq(s3):
              path = 4 *(a + b + c) + 2 * isqrt(a*a + b*b) + isqrt(b*b + c*c)
              C.append(path)
            # Type s1 and s4 squares contribute
            if is_sq(s1) and is_sq(s4):
              path = 4 *(a + b + c) + 2 * isqrt(a*a + b*b) + isqrt(a*a + b*b + c*c)
              C.append(path)
            # Type s2 and s3 squares contribute
            if is_sq(s2) and is_sq(s3):
              path = 4 *(a + b + c) + 2 * isqrt(a*a + c*c) + isqrt(b*b + c*c)
              C.append(path)
            # Type s2 and s4 squares contribute 
            if is_sq(s2) and is_sq(s4):
              path = 4 *(a + b + c) + 2 * isqrt(a*a + c*c) + isqrt(a*a + b*b + c*c)
              C.append(path)
            # Type s3 and s4 squares contribute
            if is_sq(s3) and is_sq(s4):
              path = 4 *(a + b + c) + 2 * isqrt(b*b + c*c) + isqrt(a*a + b*b + c*c)
              C.append(path)
      
      # A sorted list of flight path totals
      C = sorted(C)
      # find minimum sum of three flight paths
      Min_FP = C[0] + C[1] + C[2]
      
      print(f"All possible flight path lengths = {C} cm.")
      print(f"Length of three shortest flight paths = {Min_FP} cm.")
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:12 am on 31 August 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2815: PIN-in-the-middle 

    From The Sunday Times, 4th September 2016 [link] [link]

    For Max Stout’s various accounts and cards he has nine different 4-digit PINs to remember. For security reasons none of them is the multiple of another, and none is an anagram of another. He has written these PINs in a 3-by-3 array with one PIN in each place in the array. It turns out that the product of the three PINs in any row, column or main diagonal is the same. In fact there are just two different prime numbers that divide into this product.

    What is the PIN in the middle position of the array?

    [teaser2815]

     
    • Jim Randell's avatar

      Jim Randell 10:13 am on 31 August 2021 Permalink | Reply

      There are two primes p and q and each square of the grid contains some product of the two primes, and the whole forms a multiplicative magic square.

      Each square has a value: (p^m)(q^n) so the squares formed by the exponents (the “m-square” and the “n-square”) are both additive
      magic squares.

      If the PIN in the central square is (p^j)(q^k) then the magic constant of the multiplicative square is (p^3j)(q^3k) and the magic constants for the two additive magic squares are 3j and 3k.

      This Python program runs in 59ms.

      Run: [ @replit ]

      from enigma import (Primes, irange, inf, subsets, div, seq_all_different, nsplit, ordered, printf)
      
      primes = Primes(expandable=1)
      
      # generate multiplicative magic squares formed from two primes
      def solve():
      
        # consider values for p and q
        for q in primes.range(3, inf):
          for p in primes.range(2, q):
      
            # find 4 digit combinations of p and q
            ns = set()
            for k in irange(0, inf):
              qk = q ** k
              if qk > 9999: break
              for j in irange(0, inf):
                n = (p ** j) * qk
                if n > 9999: break
                if n > 3: ns.add(n)
      
            # choose E (the central number)
            for E in ns:
              # the magic constant is ...
              K = E ** 3
      
              # choose A (the smallest corner)
              for A in ns:
                if not (A < E): continue
      
                # compute I
                I = div(K, A * E)
                if I is None or I not in ns: continue
      
                # choose B (less than D)
                for B in ns:
                  # compute the remaining values
                  C = div(K, A * B)
                  if C is None or C < A or C not in ns: continue
                  F = div(K, C * I)
                  if F is None or F not in ns: continue
                  D = div(K, E * F)
                  if D is None or D < B or D not in ns: continue
                  G = div(K, A * D)
                  if G is None or G < A or C * E * G != K or G not in ns: continue
                  H = div(K, B * E)
                  if H is None or G * H * I != K or H not in ns: continue
      
                  # return the magic square
                  sq = (A, B, C, D, E, F, G, H, I)
                  if len(set(sq)) == 9:
                    yield sq
      
      
      for sq in solve():
        # check no numbers have the same digits
        if not seq_all_different(ordered(*nsplit(x, 4)) for x in sq): continue
        # check none of the numbers is a factor of another
        if any(b % a == 0 for (a, b) in subsets(sorted(sq), size=2)): continue
      
        # output the square
        (A, B, C, D, E, F, G, H, I) = sq
        printf("[ {A} {B} {C} | {D} {E} {F} | {G} {H} {I} ]")
        break # we only need the first solution
      

      Solution: The PIN in the middle is 2592.

      And here is a complete grid:

      The magic constant is: 2592^3 = 17414258688 = (2^15)(3^12).

      The numbers in brackets show the powers of 2 and 3, which can be seen to form additive magic squares with magic constants of 15 and 12.

      Like

    • Frits's avatar

      Frits 6:15 pm on 16 September 2021 Permalink | Reply

      See also [https://brg.me.uk/?page_id=4071] for 2 different approaches.

      Like

  • Unknown's avatar

    Jim Randell 4:45 pm on 13 August 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3073: Snookered 

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

    The playing surface of a snooker table is a twelve-foot by six-foot rectangle. A ball is placed at P on the bottom cushion (which is six feet wide) and hit so it bounces off the left cushion, right cushion and into the top-left pocket.

    Now the ball is replaced at P and hit so it bounces off the left cushion, top cushion and into the bottom right pocket, after travelling 30% further than the first shot took. The ball always comes away from the cushion at the same angle that it hits the cushion.

    How far did the ball travel on the second shot?

    [teaser3073]

     
    • Jim Randell's avatar

      Jim Randell 5:07 pm on 13 August 2021 Permalink | Reply

      (See also: Teaser 1917).

      With a suitable diagram this puzzle reduces to solving a quadratic equation.

      By reflecting the table along its sides we can convert the paths of the ball into straight lines, whose length can be easily calculated. (For the purposes of this puzzle the ball and the pockets can be considered to be points of negligible size).

      We see that the 1st distance A is given by:

      A² = (12)² + (12 + x)²

      And the second distance B by:

      B² = (24)² + (6 + x)²

      And they are related by:

      B = (13/10)A
      10B = 13A
      100B² = 169A²

      This gives us a quadratic equation in x, which we can solve to find the required answer (= B).

      Run: [ @replit ]

      from enigma import (quadratic, sq, hypot, printf)
      
      # dimensions of the table
      (X, Y) = (6, 12)
      
      # B = (P/Q) A
      # (P^2)(A^2) = (Q^2)(B^2)
      (P2, Q2) = (sq(13), sq(10))
      
      # calculate the coefficients of the quadratic: a.x^2 + b.x + c = 0
      a = P2 - Q2
      b = P2 * 2 * Y - Q2 * 2 * X
      c = P2 * 2 * sq(Y) - Q2 * (sq(2 * Y) + sq(X))
      printf("[({a:+d})x^2 ({b:+d})x ({c:+d})]")
      
      # and solve the equation to determine x
      for x in quadratic(a, b, c, domain='F'):
        if 0 < x < X:
          # calculate the paths
          A = hypot(12, 12 + x)
          B = hypot(24, 6 + x)
          printf("A={A:g} B={B:g} [x={x:g}]")
      

      Solution: On the second shot the ball travelled 26 feet.

      The required equation is:

      69x² + 2856x − 12528 = 0

      This factorises as:

      3(x − 4)(23x + 1044) = 0

      From which the value of x is obvious, and the value of B can then be calculated.

      We find:

      x = 4
      A = 20
      B = 26

      Like

      • Frits's avatar

        Frits 6:25 pm on 13 August 2021 Permalink | Reply

        @Jim, I verified your quadratic solution. It directly follows from the diagram.

        Like

      • Jim Randell's avatar

        Jim Randell 11:51 am on 16 August 2021 Permalink | Reply

        Or finding the answer numerically:

        Run: [ @replit ]

        from enigma import (hypot, fdiv, find_value, printf)
        
        # calculate paths A and B
        A = lambda x: hypot(12, 12 + x)
        B = lambda x: hypot(24, 6 + x)
        
        # find an x value that gives B/A = 1.3
        x = find_value((lambda x: fdiv(B(x), A(x))), 1.3, 0, 6).v
        printf("A={A:g} B={B:g} [x={x:g}]", A=A(x), B=B(x))
        

        Like

  • Unknown's avatar

    Jim Randell 9:20 am on 3 August 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2807: Pentagons 

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

    I have taken five identical straight rods and joined each to the next by a hinge to make a flexible ring. With this ring I can make lots of different pentagonal shapes, and in particular I can make lots of pentagons with area equal to a whole number of square centimetres. The largest whole-numbered area achievable is a two-figure number, and the smallest whole-numbered area achievable is another two-figure number. In fact these two numbers use the same two digits but in the reverse order.

    What is the largest whole-numbered area achievable?

    [teaser2807]

     
    • Jim Randell's avatar

      Jim Randell 9:21 am on 3 August 2021 Permalink | Reply

      The maximal area is achieved with a cyclic pentagon. If the sides have length x the area is given by:

      Amax(x) = (x² / 4) √(5(5 + 2√5))

      If this value is not an integer, the maximum integer-valued area will be achieved by a slight deformation of the pentagon to give the floor of this value.

      I believe the minimal area is achieved with a degenerate pentagon where two of the sides are collapsed to give an equilateral triangle with a protruding spike.

      Amin(x) = (x² / 4) √3

      Again, if this value is not an integer, the minimum integer-valued area will be achieved by opening up the collapsed spike slightly (to give an actual pentagon), and increase the area to the ceiling of this value.

      This Python program considers possible Amax and Amin values (each 2 digits, one the reverse of the other), and calculates the range of x values that would give these values. If there are any common values then we have a viable solution. It runs in 50ms.

      Run: [ @replit ]

      from enigma import (irange, nreverse, sqrt, fdiv, printf)
      
      # area multipliers
      maxA = fdiv(sqrt(5 * (5 + 2 * sqrt(5))), 4)
      minA = fdiv(sqrt(3), 4)
      
      # consider 2 digit maximal area
      for M in irange(10, 99):
        m = nreverse(M)
        if not (9 < m < M): continue
      
        # calculate range of x values based on M
        xmin1 = sqrt(M, maxA)
        xmax1 = sqrt(M + 1, maxA)
      
        # calculate range of x value based on m
        xmin2 = sqrt(m - 1, minA)
        xmax2 = sqrt(m, minA)
      
        # intersect the intervals
        xmin = max(xmin1, xmin2)
        xmax = min(xmax1, xmax2)
        if not (xmin < xmax): continue
        printf("Amax={M} Amin={m} -> x = [{xmin}, {xmax}]")
      

      Solution: The largest whole-number area achievable is 61 cm².

      And the smallest whole-number area achievable is 16 cm².

      The length of the rods is between 5.995 cm and 6.003 cm, so (although not mentioned in the puzzle) an integer length (of 6 cm) for the rods could be intended.

      For 6 cm rods we have:

      Amax ≈ 61.937 cm²
      Amin ≈ 15.588 cm²

      Like

    • Frits's avatar

      Frits 5:03 pm on 4 August 2021 Permalink | Reply

      Using Jim’s code as a base but avoiding square root calculations.
      The internal run time is 400 nanoseconds (with PyPy).

       
      # "x^2" related multipliers
      bigMult = 4 / (5 * (5 + 2 * 5**.5))**.5
      smallMult = 4 / 3**.5
      
      # consider 2 digit maximal area
      for t in range(2, 10):
        for u in range(1, t):
          M = 10 * t + u
          m = u * 10 + t
        
          # calculate range of x^2 values
          x2max1 = (M + 1) * bigMult
          x2min2 = (m - 1) * smallMult
      
          # leave inner loop if already a max is smaller than a min
          # as x2min2 grows faster than x2max1 for subsequent entries
          if x2max1 < x2min2: break
      
          x2min1 = x2max1 - bigMult
          x2max2 = x2min2 + smallMult
      
          # intersect the intervals
          x2min = max(x2min1, x2min2)
          x2max = min(x2max1, x2max2)
          if not(x2min < x2max): continue
          print(f"answer: {M} cm2")
      

      Like

  • Unknown's avatar

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

    Teaser 2795: No mean feat 

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

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

    What were my blue numbers?

    [teaser2795]

     
    • Jim Randell's avatar

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

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

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

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

      This Python program runs in 530ms.

      Run: [ @replit ]

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

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

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

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

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

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

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

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

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

      Then the sequence:

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

      also has this property.

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

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

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

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

      Replacing line 26 with the following does the trick:

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

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

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

      Giving a total of 6 possible red/blue colourings.

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

      Like

      • Jim Randell's avatar

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

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

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

        Then the sequence is:

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

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

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

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

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

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

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

        Like

  • Unknown's avatar

    Jim Randell 8:41 am on 29 April 2021 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2788: Red-letter days 

    From The Sunday Times, 28th February 2016 [link] [link]

    The months of my 2016 calendar are headed M, T, W, Th, F, Sa, Su, with the dates 1, 2, 3… in a grid of squares underneath. I have shaded all the days on which I have meetings planned – all of them being on weekdays. For example, in February the days 1, 2, 3, 4, 8 and 9 are shaded: these shaded squares form a connected piece and the product of the numbers is a perfect cube. The same is true of the shaded squares in another month – and in fact it’s the largest possible such cube in the calendar. My last meeting in that month is on my aunt’s birthday.

    What is her birthday?

    [teaser2788]

     
    • Jim Randell's avatar

      Jim Randell 8:42 am on 29 April 2021 Permalink | Reply

      This Python program examines each month to find regions that give the maximum possible cube.

      For each month the grid is filled out, and then we calculate the total number of each possible prime factor available, and any date that contributes a prime factor that occurs fewer than 3 times is removed. We then examine subsets of the remaining numbers that have a product which is a cube, and form a connected region.

      It runs in 421ms.

      Run: [ @replit ]

      from datetime import (date, timedelta)
      from enigma import (grid_adjacency, irange, factor, multiset, call, subsets, multiply, is_cube, union, Accumulator, printf)
      
      # enable internal caching for is_cube
      is_cube.cache_enabled = 1
      
      # prime factors of numbers from 1 to 31
      factors = dict((n, factor(n)) for n in irange(1, 31))
      
      # the adjacency matrix for a 5x5 grid
      adj = grid_adjacency(5, 5)
      
      # is a region of the grid connected?
      def is_connected(js):
        # start with one of the cells, and try to connect the rest in
        rs = set(js[1:])
        js = js[:1]
        while js and rs:
          js = union(adj[i] for i in js).intersection(rs)
          rs.difference_update(js)
        return not rs
      
      # 1 day increment
      day = timedelta(days=1)
      
      # find regions with maximal product
      r = Accumulator(fn=max, collect=1)
      
      # consider possible months (although we could skip Feb)
      seen = dict()
      for m in irange(1, 12):
      
        # fill out the grid for a particular month
        grid = dict()
        (d, i) = (date(2016, m, 1), None)
        while d.month == m:
          j = d.weekday()
          if j < 5:
            i = (j if i is None else i + 1)
            grid[i] = d.day
          d += day
      
        # remove dates that cannot be used
        while 1:
          # accumulate all the prime factors of numbers in the grid
          fs = call(multiset.from_seq, (factors[n] for n in grid.values()))
          # remove any numbers with a factor with a count < 3
          ks = list(k for (k, n) in grid.items() if any(fs[f] < 3 for f in factors[n]))
          if not ks: break
          for k in ks: del grid[k]
      
        # have we already done this month
        k = tuple(sorted(grid.items()))
        if k in seen:
          printf("[month {m} duplicates month {x}]", x=seen[k])
          continue
        seen[k] = m
      
        printf("[month {m}: {grid}", grid=sorted(grid.values()))
      
        # find subsets whose product is a cube
        for ks in subsets(grid.keys(), min_size=1):
          p = multiply(grid[k] for k in ks)
          if not (is_cube(p) and is_connected(ks)): continue
          vs = sorted(grid[k] for k in ks)
          r.accumulate_data(p, (m, vs))
          printf("-> {vs} -> {p}^3", p=is_cube(p))
      
      printf("max product = {x}^3", x=is_cube(r.value))
      for (m, vs) in r.data:
        printf("month {m} -> {vs}")
      

      Solution: Her birthday is 27th June.

      Which is a Monday in 2016.

      The largest cube achievable is 15120³ = 3456649728000.

      It occurs in June using dates (1), 3, 6, 7, 8, 9, 10, 14, 15, 16, 20, 21, 27. (Using 1 is optional).

      The maximal cubes found for each month are:

      Jan: 2520³ = 16003008000; (1), 4, 5, 6, 7, 8, 14, 15, 20, 21, 27
      Feb: 120³ = 1728000; 1, 2, 3, 4, 5, 8, 10, 12, 15
      Mar: 168³ = 4741632; (1), 2, 7, 8, 9, 14, 16, 21
      Apr: = Jan
      May: 6³ = 216; 2, 3, 4, 9
      Jun: 15120³ = 3456649728000; (1), 3, 6, 7, 8, 9, 10, 14, 15, 16, 20, 21, 27
      Jul: = Jan
      Aug: 120³ = 1728000; 1, 2, 3, 4, 5, 8, 10, 12, 15 [= Feb]
      Sep: 7560³ = 432081216000; (1), 5, 6, 7, 8, 9, 12, 14, 15, 20, 21, 27
      Oct: 3³ = 27; 27
      Nov: = Mar
      Dec: = Sep

      The following diagram shows the regions that give the maximum cube for each month:

      (1 is optional, except where it is required to keep the region connected, i.e. Aug, Feb).

      Like

    • Frits's avatar

      Frits 6:49 pm on 30 April 2021 Permalink | Reply

      Based on Jim’s program with the idea to find the maximum product as soon as possible.

      from datetime import date, timedelta
      from itertools import combinations
      from enigma import multiply, is_cube
      
      # exclude days where a prime factor can not occur 3 times per month
      ex_days = [11, 13, 17, 19, 22, 23, 26, 29, 31]
      # also exclude days on small islands
      ex_days += [18, 24, 25, 30]
      
      zeroes = [[0 for x in range(5)] for y in range(5)] 
      
      # calculate minimal number of <s> entries to exceed <mprod>  
      def min_entries(s, lim):
        p = 1  
        for i, v in enumerate(s):
          p *= v
          if p > lim: break
        return len(s) - i
      
      # chain as many cells of sequence <s> as possible
      def chain_cells(i, j, s, done):
        if i < 0 or i > 4 or j < 0 or j > 4: return []
       
        if done[i][j] == 1: return []
        
        ind = i * 5 + j 
       
        if ind not in s: return []
        
        chain = [ind]
        done[i][j] = 1
        
        # check adjacent cells
        chain += chain_cells(i - 1, j, s, done)
        chain += chain_cells(i + 1, j, s, done)
        chain += chain_cells(i, j - 1, s, done)
        chain += chain_cells(i, j + 1, s, done)
        
        return chain
      
      # 1 day increment
      day = timedelta(days=1)
       
      (mprod, mentries, bday) = (0, 1, "")
      
      # consider possible months (although we could skip Feb)
      seen = dict()
      grids = dict()
      
      # build and store grids
      for m in range(1, 13):
        # fill out the grid for a particular month
        grid = dict()
        (d, i) = (date(2016, m, 1), None)
        while d.month == m:
          j = d.weekday()
          if j < 5:
            d1 = d.day
            i = (j if i is None else i + 1)
            if d1 not in ex_days:
              grid[i] = d1
          d += day
        grids[m] = grid
        
      # start with the month with the most entries  
      for m in sorted(grids, key=lambda m: len(grids[m]), reverse=True): 
        grid = grids[m]
        # have we already done this month
        k = tuple(sorted(grid.items()))
        if k in seen:
          continue
        seen[k] = m
       
        allprod = multiply(grid[k] for k in grid)
        
        if mprod:
          d1 = allprod / mprod
          # not enough numbers in grid to exceed mprod
          if d1 < 1:
            continue
          # calculate minimal number of entries to exceed <mprod>  
          mentries = min_entries(grid.values(), d1)
        
        # find subsets whose product is a cube
        for k in range(len(grid), 0, -1):
          if k < mentries: break
          
          for ks in combinations(grid.keys(), k):
            p = multiply(grid[k] for k in ks)
            
            if not(p >= mprod and is_cube(p)): continue
            
            (i, j) = (ks[0] // 5, ks[0] % 5)
            # days have to be connected
            if len(ks) > len(chain_cells(i, j, ks, [x.copy() for x in zeroes])): 
              continue
              
            if p > mprod: 
              mprod = p
              # calculate minimal number of entries to exceed <mprod>  
              mentries = min_entries(grid.values(), allprod / mprod)
              vs = [grid[k] for k in ks]
              bday = str(vs[-1]) + "-" + str(m) + "-2016"
              print(f"month {m}: -> {vs} -> {is_cube(p)}^3")
       
      print(f"max product = {is_cube(mprod)}^3")
      print(f"birthday {bday}")
      

      Like

  • Unknown's avatar

    Jim Randell 8:07 am on 17 December 2020 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2767: Cutting corners 

    From The Sunday Times, 4th October 2015 [link] [link]

    To make an unusual paperweight a craftsman started with a cuboidal block of marble whose sides were whole numbers of centimetres, the smallest sides being 5cm and 10cm long. From this block he cut off a corner to create a triangular face; in fact each side of this triangle was the diagonal of a face of the original block. The area of the triangle was a whole number of square centimetres.

    What was the length of the longest side of the original block?

    [teaser2767]

     
    • Jim Randell's avatar

      Jim Randell 8:08 am on 17 December 2020 Permalink | Reply

      Assuming the block is a rectangular cuboid (like a brick), lets us calculate the diagonals of the faces using Pythoagoras’ Theorem. (But this isn’t the only type of cuboid, see: [ @wikipedia ]).

      Suppose the sides of the block are (a, b, c).

      Then the lengths of the diagonals are: (x, y, z) = (√(a² + b²), √(a² + c²), √(b² + c²)).

      Using the following variant of Heron’s Formula [ @wikipedia ] for the area of a triangle with sides (x, y, z)

      4A = √(4x²y² − (x² + y² − z²)²)

      We can simplify this to calculate the area of a triangle formed by the diagonals as:

      4A = √(4(a² + b²)(a² + c²) − (2a²)²)
      2A = √(a²b² + a²c² + b²c²)

      And we are interested in when the area A is an integer, for given values of a and b.

      A = (1/2)√(a²b² + a²c² + b²c²)

      This Python program looks for the smallest solution for side c. It runs in 44ms.

      Run: [ @replit ]

      from enigma import (irange, inf, is_square, div, printf)
      
      # the smallest 2 sides are given
      (a, b) = (5, 10)
      (a2, b2) = (a * a, b * b)
      
      # consider values for the longest side
      for c in irange(b + 1, inf):
        c2 = c * c
      
        # calculate the area of the triangle
        r = is_square(a2 * b2 + a2 * c2 + b2 * c2)
        if r is None: continue
        A = div(r, 2)
        if A is None: continue
      
        # output solution
        printf("a={a} b={b} c={c}; A={A}")
        break # only need the first solution
      

      Solution: The longest side of the original block was 40 cm.


      In this case we are given a = 5, b= 10, so we have:

      4A² = 2500 + 125c²
      4A² = 125(20 + c²)
      c² = 4A²/125 − 4.5
      c = √(4A²/125 − 20)

      For c to be an integer, it follows the 4A² is divisible by 125 = 5³. So A is some multiple of 25, larger than 25.

      And we quickly find a solution, either manually, or using a program:

      from enigma import (irange, inf, is_square, div, printf)
      
      # consider values for the longest side
      for A in irange(50, inf, step=25):
        c = is_square(div(4 * A * A, 125) - 20)
        if c is not None:
          # output solution
          printf("a={a} b={b} c={c}; A={A}", a=5, b=10)
          break # only need the first solution
      

      However, this is not the only solution. If we leave the program running we find larger solutions:

      a=5 b=10 c=40; A=225
      a=5 b=10 c=720; A=4025
      a=5 b=10 c=12920; A=72225
      a=5 b=10 c=231840; A=1296025
      a=5 b=10 c=4160200; A=23256225
      a=5 b=10 c=74651760; A=417316025
      a=5 b=10 c=1339571480; A=7488432225

      These further solutions could have been eliminated by putting a limit on the size of the block (e.g. if the longest side was less than 7m long, which it probably would be for a desk paperweight).

      But we can generate these larger solutions more efficiently by noting that we have a form of Pell’s equation:

      4A² = a²b² + a²c² + b²c²
      (2A)² − (a² + b²)c² = a²b²

      writing:

      X = 2A
      D = a² + b²
      Y = c
      N = a²b²

      we get:

      X² − D.Y² = N

      And we are interested in solutions to the equation where X is even, and Y > b.

      We can use the pells.py solver from the py-enigma-plus repository to efficiently generate solutions:

      from enigma import (arg, ifirst, printf)
      import pells
      
      (a, b) = (5, 10)
      
      # given (a, b) generate solutions for (c, A) in c order
      def solve(a, b):
        (a2, b2) = (a * a, b * b)
        # solve: (a^2 + b^2).c^2 - 4.A^2 = -(a^2.b^2)
        for (c, A) in pells.diop_quad(a2 + b2, -4, -a2 * b2):
          if c > b:
            yield (c, A)
      
      # output the first N solutions
      N = arg(20, 0, int)
      for (c, A) in ifirst(solve(a, b), N):
        printf("a={a} b={b} c={c}; A={A}")
      

      Which gives the following output:

      a=5 b=10 c=40; A=225
      a=5 b=10 c=720; A=4025
      a=5 b=10 c=12920; A=72225
      a=5 b=10 c=231840; A=1296025
      a=5 b=10 c=4160200; A=23256225
      a=5 b=10 c=74651760; A=417316025
      a=5 b=10 c=1339571480; A=7488432225
      a=5 b=10 c=24037634880; A=134374464025
      a=5 b=10 c=431337856360; A=2411251920225
      a=5 b=10 c=7740043779600; A=43268160100025
      a=5 b=10 c=138889450176440; A=776415629880225
      a=5 b=10 c=2492270059396320; A=13932213177744025
      a=5 b=10 c=44721971618957320; A=250003421569512225
      a=5 b=10 c=802503219081835440; A=4486129375073476025
      a=5 b=10 c=14400335971854080600; A=80500325329753056225
      a=5 b=10 c=258403544274291615360; A=1444519726560481536025
      a=5 b=10 c=4636863460965394995880; A=25920854752758914592225
      a=5 b=10 c=83205138753102818310480; A=465130865823099981124025
      a=5 b=10 c=1493055634094885334592760; A=8346434730063040745640225
      a=5 b=10 c=26791796274954833204359200; A=149770694275311633440400025
      ...
      

      Solutions for the longest side (c) and area (A) can be calculated by the following recurrence relation:

      (c, A) → (40, 225)
      (c, A) → (9c + 8A/5, 50c + 9A)

      Like

  • Unknown's avatar

    Jim Randell 9:27 am on 29 November 2020 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 1958: Pent up 

    From The Sunday Times, 26th March 2000 [link]

    The schoolchildren run around in a walled regular pentagonal playground, with sides of 20 metres and with an orange spot painted at its centre. When the whistle blows each child has to run from wherever they are to touch each of the five walls, returning each time to their starting point, and finishing back at the same point.

    Brian is clever but lazy and notices that he can minimize the distance he has to run provided that his starting point is within a certain region. Therefore he has chalked the boundary of this region and he stays within in throughout playtime.

    (a) How many sides does Brian’s region have?
    (b) What is the shortest distance from the orange spot to Brian’s chalk line?

    This puzzle is included in the book Brainteasers (2002). The puzzle text above is taken from the book.

    [teaser1958]

     
    • Jim Randell's avatar

      Jim Randell 9:28 am on 29 November 2020 Permalink | Reply

      I wrote a program to plot points with a path distance that is the same as that of the central point, and you get a plot like this:

      This makes sense, as the shortest distance from a point to the line representing any particular side is a line through the point, perpendicular to the side. So for each side, if we sweep it towards the opposite vertex, we cover a “house shaped” region of the pentagon, that excludes two “wings” on each side. (The regions that overhang the base).

      Sweeping each of the five sides towards the opposite vertex, the intersection of these five “house shaped” regions is the shaded decagon:

      A line from the centre of one of the sides of this decagon, through the orange spot to the centre of the opposite side, is a minimal diameter of the decagon, and we see that this distance is the same as the length of one of the sides of the original pentagon. (It corresponds to the side swept towards the opposite vertex, in the position when it passes through the exact centre of the pentagon).

      So the minimum distance from the centre to the perimeter of the decagon is half this distance.

      Solution: (a) The region has 10 sides; (b) The shortest distance is 10 metres.


      If the playground had been a regular 4-gon (square) or 3-gon (equilateral triangle), then every interior point would correspond to a minimal path.

      For a 6-gon (hexagon) we get a smaller 6-gon inside as the minimal area, and for a 7-gon the area is the shape of a 14-gon.

      In general for a k-gon we get an area in the shape of a smaller k-gon (when k is even) or 2k-gon (when k is odd).

      The areas get smaller and smaller as k increases. In the limit we have a circular playground with a single point at the centre corresponding to the minimal path.

      Like

    • Hugh Casement's avatar

      Hugh Casement 1:23 pm on 29 November 2020 Permalink | Reply

      I think he chalks a line that runs (for example) from the north vertex to a point roughly two thirds of the way down the ESE wall, then to the midpoint of the S wall, to a point roughly a third of the way up the WSW wall, and back to the N vertex. He then stays on that line rather than within it. When the whistles blows he can run right round the line, clockwise or anticlockwise, touching two walls at the N vertex. His total distance is about 81.75 m.

      Like

      • Jim Randell's avatar

        Jim Randell 2:39 pm on 29 November 2020 Permalink | Reply

        @Hugh: Except that after touching each wall you have to return to your starting point before you can set off to touch the next wall.

        Like

        • Hugh Casement's avatar

          Hugh Casement 3:17 pm on 29 November 2020 Permalink | Reply

          Oh, I misread it, thinking he just has to return to the start point at the end.
          Sorry to introduce a red herring.
          Anyway in my version I was wrong that the intermediate points are about a third of the way along the walls from the south: they’re precisely two thirds of the way.

          Like

          • Hugh Casement's avatar

            Hugh Casement 4:50 pm on 29 November 2020 Permalink | Reply

            Oops! Got it wrong again. Precisely one third, six and 2/3 metres.
            I’m really not awake today. Feel free to delete all my posts on this subject.

            Like

    • Hugh Casement's avatar

      Hugh Casement 2:31 pm on 29 November 2020 Permalink | Reply

      Put t = tan(72°) = sqrt{5 + 2 sqrt(5)}.
      Then if the midpoint of the south wall has coordinates (0,0) and the north vertex is at (0, 10t),
      the lower right wall is y = t(x – 10) and the lower left wall is y = t(10 – x).

      Then a bit of calculus — or some trial & error by program — shows that the intermediate points that he touches are at roughly (±12.060113, 6.340375) and the length of his path is about 81.75134 metres. If he starts at a point off the line (on either side) his overall path is longer.

      Of course his chalk line could start at any vertex and go via the midpoint of the opposite side. He could draw five such overlapping quadrilaterals, and stick to any of them, swapping between them at will, so long as when the whistle blows he is not confused as to which line he is on.

      Like

  • Unknown's avatar

    Jim Randell 11:19 am on 10 November 2020 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 1942: Eurosceptics 

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

    Ruritania is reluctant to adopt the euro as it has a sensible currency of its own. The mint issues the coins in four denominations, the value of each being proportional to its radius. The total value of the four, in euros, is 28.

    The four coins are available in a clever presentation pack. It consists of a triangular box of sides 13 cm, 14 cm and 15 cm. The largest coin just fits into the box, touching each of its sides, roughly as shown:

    Then there are three straight pieces of thin card inside the box. Each touches the large coin and is parallel to a side of the box. This creates three smaller triangles in the corners of the box. The three remaining coins just fit into the box, with one in each of these small triangles. Each coin touches all three sides of the triangle.

    Unfortunately I have lost the smallest coin from my presentation pack.

    What, in euros, is its value?

    This puzzle is included in the book Brainteasers (2002). The puzzle text above is taken from the book.

    [teaser1942]

     
    • Jim Randell's avatar

      Jim Randell 11:20 am on 10 November 2020 Permalink | Reply

      See: [ @Wikipedia ] for more details on incircles and excircles.

      Suppose a, b, c, d are the radii of the four coins (from largest to smallest).

      The inradius of a triangle can be calculated as the area of the triangle divided by the semi-perimeter.

      So for the large triangle we have:

      a = A / S

      where:

      S = semi-perimeter = (13 + 14 + 15) / 2 = 21
      A = area = sqrt(21.8.7.6) = 84

      So:

      a = 84/21 = 4

      Then considering the triangle containing the next smallest coin (radius = b), this is a version of the large triangle scaled down by a factor of (say) q.

      So it has a semi-perimeter of 21q, and an area of 84q² so:

      b = 84q² / 21q = 4q
      q = b / 4

      But the largest coin is an excircle of this smaller triangle and so its radius is given by:

      a = b.21q / (21q − 13q)
      a = 21b / (21 − 13) = b(21/8)
      b = (8/21)a = 32/21

      Similarly, for coins in the other corners:

      c = (7/21)a = 4/3
      d = (6/21)a = 8/7

      Now, if the radii are multiplied by factor f to get the value in euros we have:

      (4 + 32/21 + 4/3 + 8/7)f = 28
      8f = 28
      f = 7/2

      So the coins are worth:

      f.a = 14 euros
      f.b = 5 + 1/3 euros
      f.c = 4 + 2/3 euros
      f.d = 4 euros

      Which do indeed give a total of 28 euros.

      So, we have worked out the radii and value of each of the four coins.

      Solution: The smallest coin is worth 4 euros.

      The four coins are:

      radius = 40.0 mm; value = 14.00 euro
      radius = 15.2 mm; value = 5.33 euro
      radius = 13.3 mm; value = 4.67 euro
      radius = 11.4 mm; value = 4.00 euro

      Here’s a program that performs the same calculations:

      Run: [ @repl.it ]

      from enigma import fdiv, sqrt, multiply, printf
      
      # total value of the coins
      T = 28
      
      # the sides of the large triangle
      sides = (13, 14, 15)
      
      # calculate the radius of the largest coin
      S = fdiv(sum(sides), 2)
      A = sqrt(S * multiply(S - x for x in sides))
      a = fdiv(A, S)
      
      # and the three coins in the corners
      (b, c, d) = (fdiv(a * (S - x), S) for x in sides)
      
      # multiplier f: radius -> value
      f = fdiv(T, a + b + c + d)
      
      # output the values of the coins
      printf("[S={S} A={A} f={f}]")
      for (r, n) in zip((a, b, c, d), "abcd"):
        printf("{n}: {r:.1f} mm -> {v:.2f} euro", r= r * 10, v=r * f)
      

      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