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

  • Unknown's avatar

    Jim Randell 8:55 am on 2 December 2025 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2469: [Football league] 

    From The Sunday Times, 17th January 2010 [link]

    Our football league has six teams, A, B, C, D, E and F, who play each other once, earning 3 points for a win, 1 for a draw. Of last season’s 20 goals, the best were in Primadona’s hat-trick in C’s derby against D. At one stage, when C and D had played all their games, but A and B each still had two in hand, the league order was A, B, C, D, E, F (goal differences separate teams tying on points). But A’s morale was shattered because, in the end, C won the league (the number of goals scored also being needed for the final decision).

    What were the scores in C’s matches? (C v A, C v B, C v D, C v E, C v F)?

    This puzzle was originally published with no title.

    [teaser2469]

     
    • Jim Randell's avatar

      Jim Randell 8:55 am on 2 December 2025 Permalink | Reply

      The wording of the puzzle suggests that in the final order C had the same points and goal difference as (at least) one of the other teams, and was declared winner based on goals scored.

      I think this puzzle (and probably Teaser 2516) are more easily dealt with manually. But there is a certain challenge in constructing a programmatic solution.

      My program adopts the following strategy:

      [1] Allocate match outcomes for the “one stage”, when C & D have played all their matches, A & B have 2 to play, the league order is A, B, C, D, E, F.

      [2] Allocate the remaining match outcomes such that C is at the top of the table.

      [3] Allocate goals to the outcomes such that 3 goals are scored by (at least) one side in the CvD match.

      [4] Allocate any remaining goals, so that 20 goals are allocated in total.

      [5] Check C wins the league (but has the same points and goal difference as another team) and that the ordering of “one stage” is valid.

      It runs in 5.3s (using PyPy).

      from enigma import (Football, subsets, update, cproduct, peek, is_decreasing, zip_eq, join, printf)
      
      # scoring system
      football = Football(games="wdlx", points=dict(w=3, d=1))
      
      teams = "ABCDEF"
      keys = sorted(x + y for (x, y) in subsets(teams, size=2))
      
      # find keys for each of the teams
      dks = dict((t, list(k for k in keys if t in k)) for t in teams)
      
      # allocate match outcomes for the "one stage"
      def stage1():
        # C and D have played all their games
        for (AD, BD, CD, DE, DF) in football.games("wdl", repeat=5):
          D = football.table([AD, BD, CD, DE, DF], [1, 1, 1, 0, 0])
          for (AC, BC, CE, CF) in football.games("wdl", repeat=4):
            C = football.table([AC, BC, CD, CE, CF], [1, 1, 0, 0, 0])
            if not (C.points >= D.points): continue
      
            # A and B each have two games unplayed (x)
            for (AB, BE, BF) in football.games("wdlx", repeat=3):
              B = football.table([AB, BC, BD, BE, BF], [1, 0, 0, 0, 0])
              if not (B.x == 2 and B.points >= C.points): continue
              for (AE, AF) in football.games("wdlx", repeat=2):
                A = football.table([AB, AC, AD, AE, AF], [0, 0, 0, 0, 0])
                if not (A.x == 2 and A.points >= B.points): continue
      
                # the remaining game (may be unplayed)
                for EF in football.games("wdlx"):
                  E = football.table([AE, BE, CE, DE, EF], [1, 1, 1, 1, 0])
                  if not (D.points >= E.points): continue
                  F = football.table([AF, BF, CF, DF, EF], [1, 1, 1, 1, 1])
                  if not (E.points >= F.points): continue
      
                  # return match outcomes (at "one stage")
                  yield dict(zip(keys, [AB, AC, AD, AE, AF, BC, BD, BE, BF, CD, CE, CF, DE, DF, EF]))
      
      # allocate remaining matches
      def stage2(ms):
        # find unplayed matches
        xs = list(k for (k, v) in ms.items() if v == 'x')
        # allocate match outcomes
        for vs in football.games("wdl", repeat=len(xs)):
          ms1 = update(ms, xs, vs)
          # calculate the new table
          (A, B, C, D, E, F) = (football.extract_table(ms1, t) for t in teams)
          # C is now at the top of the table
          if not (C.points >= max(A.points, B.points, D.points, E.points, F.points)): continue
      
          # return match outcomes (final) and unplayed games (at "one stage")
          yield (ms1, xs)
      
      # allocate goals to the matches; return scorelines
      def stage3(ms):
        # minimum goals
        gs = dict(w=(1, 0), d=(0, 0), l=(0, 1))
        # allocate minimum goals
        ss = dict((k, gs[v]) for (k, v) in ms.items())
        # one side scores 3 goals in the CvD match
        gs = dict(w=[(3, 0), (4, 3)], d=[(3, 3)], l=[(3, 4), (0, 3)])
        k = 'CD'
        for v in gs[ms[k]]:
          yield update(ss, [(k, v)])
      
      # allocate remaining goals (to make 20 in total); return scorelines
      def stage4(ms, ss):
        # calculate number of goals remaining
        r = 20 - sum(sum(x) for x in ss.values())
        if r == 0:
          yield ss
        elif r > 0:
          # chose matches and teams for the remaining goals
          for kts in subsets(cproduct([keys, (0, 1)]), size=r, select='R'):
            # make an updated set of scores
            (ks, ss_) = (set(), ss)
            for (k, t) in kts:
              ks.add(k)
              v = ss_[k]
              ss_ = update(ss_, [(k, update(v, [(t, v[t] + 1)]))])
            # check we haven't changed any outcomes
            if all(peek(football.outcomes([ss_[k]], [0])) == ms[k] for k in ks):
              yield ss_
      
      # calculate (<points>, <goal-difference>, <goals-for>) score for team <t>
      def score(t, ms, ss):
        T = football.extract_table(ms, t)
        (gf, ga) = football.extract_goals(ss, t)
        return (T.points, gf - ga, gf)
      
      # check a scenario
      def check(ms, ss, xs):
        # check C wins the league
        (A, B, C, D, E, F) = (score(t, ms, ss) for t in teams)
        # C is at the top of the table
        others = (A, B, D, E, F)
        if not C > max(others): return
        # but has the same number of points _and_ goal difference as another team
        if not any(zip_eq(C, X, first=2) for X in others): return
      
        # check the positions with the matches in <xs> unplayed
        ms = update(ms, xs, ['x'] * len(xs))
        ss = update(ss, xs, [None] * len(xs))
        scores = tuple(score(t, ms, ss) for t in teams)
        # check the order is A, B, C, D, E, F
        if not is_decreasing(scores, strict=1): return
      
        # this is a valid scenario
        return scores
      
      # put it all together ...
      for ms1 in stage1():
        for (ms, xs) in stage2(ms1):
          for ss3 in stage3(ms):
            for ss in stage4(ms, ss3):
              # extract (<pts>, <diff>, <for>) scores for a valid scenario
              scores = check(ms, ss, xs)
              if scores:
                printf("unplayed = {xs}", xs=join(xs, sep=", ", sort=1))
                printf("scores = {scores}")  # scores at "one stage"
                football.output_matches(ms, ss)  # final results
      

      Solution: The scores in C’s matches are: C v A = 0-1; C v B = 0-1; C v D = 3-3; C v E = 1-0; C v F = 2-0.

      The full set of results is:

      A vs B = (d) 0-0
      A vs C = (w) 1-0
      A vs D = (w) 2-0
      A vs E = (l) 0-1
      A vs F = (l) 0-1
      B vs C = (w) 1-0
      B vs D = (w) 1-0
      B vs E = (l) 0-1
      B vs F = (l) 0-1
      C vs D = (d) 3-3
      C vs E = (w) 1-0
      C vs F = (w) 2-0
      D vs E = (w) 1-0
      D vs F = (w) 1-0
      E vs F = (d) 0-0

      The final order was:

      C: 2w1d = 7 points; for = 6; against = 5; difference = +1
      A: 2w1d = 7 points; for = 3; against = 2; difference = +1
      B: 2w1d = 7 points; for = 2; against = 2; difference = 0
      E: 2w1d = 7 points; for = 2; against = 2; difference = 0
      D: 2w1d = 7 points; for = 5; against = 6; difference = −1
      F: 2w1d = 7 points; for = 2; against = 3; difference = −1

      i.e. C > A > B = E > D > F.

      Note that B and E cannot be separated in the final order by points, goal difference, or goals scored.

      At the intermediate stage the following games were unplayed:

      A v E; A v F; B v E; B v F; and maybe E v F

      Giving the following orders:

      A: 3 played; 2w1d = 7 points; for = 3; against = 0; difference = +3
      B: 3 played; 2w1d = 7 points; for = 2; against = 0; difference = +2
      C: 5 played; 2w1d = 7 points; for = 6; against = 5; difference = +1
      D: 5 played; 2w1d = 7 points; for = 5; against = 6; difference = −1

      E: 3 played; 1d = 1 point; for = 0; against = 2; difference = −2
      F: 3 played; 1d = 1 point; for = 0; against = 3; difference = −3

      or (if E v F is unplayed):

      E: 2 played; 0 points; for = 0; against = 2; difference = −2
      F: 2 played; 0 points; for = 0; against = 3; difference = −3

      In either case the order is: A > B > C > D > E > F.

      Like

  • Unknown's avatar

    Jim Randell 7:42 am on 25 November 2025 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2516: [Football league] 

    From The Sunday Times, 12th December 2010 [link] [link]

    Six teams — Ayes, Bees, Cees, Dees, Ease and Fees — played each other once in a football league, with three points for a win and one for a draw. Ayes finished top, with the order of the teams being alphabetical. The difference in points between each pair of adjacent teams was the same. The 13 goals scored included hat-tricks from the Dees striker in two matches. In one game, the Cees goalkeeper let in two before being substituted.

    What were Cees’ five results (in the order CvA, CvB, CvD, CvE, CvF)?

    This puzzle was originally published with no title.

    [teaser2516]

     
    • Jim Randell's avatar

      Jim Randell 7:43 am on 25 November 2025 Permalink | Reply

      The points form an arithmetic sequence, from low (F) to high (A) = (f, f + x, f + 2x, f + 3x, f + 4x, f + 5x).

      So the total number of points is: 6f + 15x.

      There are 6 teams, and 15 matches in all. And there are 13 goals scored in total.

      We know 6 of the goals are involved in two of D’s matches, which leaves just 7 goals remaining to be allocated. So the maximum number of games that are won/lost is 9, meaning there must be at least 6 matches that are drawn. And in order for the 5 teams to have different points totals there can be no more than 11 drawn matches.

      The following Python program considers the possible numbers for matches that are drawn (and the rest must be won/lost), calculates the total number of points allocated, and constructs possible arithmetic sequences corresponding to this total. It then uses the [[ Football() ]] helper class from the enigma.py library to allocate match outcomes that give the required points totals, and then allocates the 13 goals accordingly.

      It runs in 239ms. (Internal runtime is 173ms).

      from enigma import (Football, irange, div, rev, seq_get, subsets, ordered, diff, update, fail, sprintf, printf)
      
      # scoring system
      football = Football(games="wdl", points=dict(w=3, d=1))
      
      # allocate match outcomes for teams <ts>, points <pts>,
      # with <w> total wins and <d> total draws
      def matches(ts, pts, w, d, i=0, ms=dict()):
        # look for the next team
        t = seq_get(ts, i)
        # are we done?
        if t is None:
          if w == 0 and d == 0:
            yield ms
        else:
          # find unallocated keys involving this team
          ks = list(ordered(t, x) for x in ts if x != t)
          ks = diff(ks, ms)
          # consider match outcomes for the unallocated games
          for vs in football.games(repeat=len(ks)):
            ms_ = update(ms, ks, vs)
            # extract the table and check we have achieved the required points
            T = football.extract_table(ms_, t)
            if T.points != pts[i]: continue
            # and have not exceeded the number of wins/draws
            (w_, d_) = (w - T.w, d - T.d)
            if (w_ < 0 or d_ < 0): continue
            # solve for the remaining teams
            yield from matches(ts, pts, w_, d_, i + 1, ms_)
      
      # solve for points <pts> with <w> matches won/lost and <d> matches drawn
      def solve(pts, w, d):
        # fill out matches (note: each draw is counted by both sides)
        for ms in matches("ABCDEF", pts, w, 2 * d):
          # extract keys for C and D
          (Cs, cs) = football.extract_keys(ms, 'C')
          (Ds, ds) = football.extract_keys(ms, 'D')
      
          # allocate minimum possible goals for the matches
          gs = dict(w=(1, 0), d=(0, 0), l=(0, 1))
          ss0 = dict((k, gs[v]) for (k, v) in ms.items())
      
          # choose 2 matches for D to have hat-tricks
          gs = [dict(w=(3, 0), d=(3, 3), l=(3, 4)), dict(w=(4, 3), d=(3, 3), l=(0, 3))]
          for kts in subsets(zip(Ds, ds), size=2):
            ss = update(ss0, ((k, gs[t][ms[k]]) for (k, t) in kts))
            # find the number of remaining goals to allocate
            r = 13 - sum(sum(v) for v in ss.values())
            if r < 0: continue
            if r > 0: fail(msg=sprintf("distribute {r} remaining goals"))
      
            # C concedes (at least) 2 goals in (at least) one match
            if not any(ss[k][1 - t] >= 2 for (k, t) in zip(Cs, cs)): continue
      
            # output scenario
            football.output_matches(ms, ss)
      
      # consider number of drawn matches (between 6 and 11)
      for d in irange(6, 11):
        # the remaining matches are won/lost
        w = 15 - d
        # total points allocated
        t = 2*d + 3*w
        # consider points difference between teams
        for x in [1, 2, 3]:
          f = div(t - 15*x, 6)
          if f is None or f < 0: continue
          pts = rev(f + i*x for i in irange(6))
          # attempt to solve for this sequence of points
          printf("[d={d}: w={w} -> t={t}; x={x} f={f} -> pts = {pts}]")
          solve(pts, w, d)
      

      Fortunately it turns out that after the minimum possible goals have been allocated to the matches, there are no “discretionary” goals remaining to allocate, so I didn’t bother to write that section of code.

      Solution: The scores in Cee’s matches were: CvA = 0-1; CvB = 1-0; CvD = 0-3; CvE = 1-0; CvF = 0-0.

      The full list of match outcomes and scores is:

      A vs B = (d) 0-0
      A vs C = (w) 1-0
      A vs D = (w) 1-0
      A vs E = (d) 0-0
      A vs F = (d) 0-0
      B vs C = (l) 0-1
      B vs D = (w) 1-0
      B vs E = (w) 1-0
      B vs F = (d) 0-0
      C vs D = (l) 0-3
      C vs E = (w) 1-0
      C vs F = (d) 0-0
      D vs E = (l) 0-1
      D vs F = (w) 3-0
      E vs F = (d) 0-0

      And the points are: A = 9, B = 8, C = 7, D = 6, E = 5, F = 4.

      The program actually runs fine considering the full range of possible draws (d = 0..15), so we don’t need to do any pre-analysis.

      Like

    • Frits's avatar

      Frits 4:58 am on 30 November 2025 Permalink | Reply

      rev = lambda s: s[::-1]
      opp = lambda n: 1 if n == 1 else 3 - n
      # return sorted tuple
      stup = lambda x, y: (x, y) if x < y else (y, x)
      tri = lambda n: n * (n + 1) // 2
      
      # remove elements from <s2> from <s1>
      # eg diff_lists([1,3,1,2], [2,0,1]) results in [1,3]
      def diff_lists(s1, s2):
        s = s1.copy()
        for x in s2:
          if x in s1:
            s.remove(x)
        return s 
      
      # return a permutation of sequence <s> where s may have duplicate elements   
      def permutations_non_unique(s, k, ss=()):
        if k == 0:
          yield ss
        else:
          for v in set(s):
            i = s.index(v)
            # list without first occurrence of <v> 
            s_ = s[:i] + s[i + 1:]
            yield from permutations_non_unique(s_, k - 1, ss + (v, ))
      
      # generate a list of 15 wins/losses/draws
      def gen_games(k, s, ps, ss=[]):
        if k == -1:
          yield ss
        else:
          # reverse the games that are already known
          r = tuple(opp(x[4 - k]) for x in ss)
          sumr = sum(r)
          # get <k> more games
          for p in permutations_non_unique(s, k):
            # check total points per team
            if sumr + sum(p) == ps[5 - k]:
              yield from gen_games(k - 1, diff_lists(s, p), ps, ss + [r + p])
      
      # generate the scores of a team
      def solve_team(gs, r, k, mxNotD, mxD, ngs, sss, ss=[]):
        if k == 0:
          yield ss, ngs
        else:
          g, i = gs[0], 5 - k
          mn = 1 if g == 3 else 0
          mx = mxD if r == 3 or i == 3 else mxNotD
          if i < r: # we know the opponents score
            o = sss[i][r - 1]
            if g == 3:
              mn = o + 1
            elif g == 0:
              mx = min(mx, o - 1)
            else:
              mn, mx = o, o 
          
          for sc in range(mn, mx + 1):
            if sc > ngs: break
            yield from solve_team(gs[1:], r, k - 1, mxNotD, mxD, 
                                  ngs - sc, sss, ss + [sc])   
        
      # generate the scores of all teams    
      def solve(gss, k, mxNotD, mxD, ngs=0, ss=[]):
        if k == 6:
          if ngs == 0:
            yield ss
        else:
          gs = gss[0] 
          for sc, ngs_ in solve_team(gs, k, 5, mxNotD, mxD, ngs, ss):
            if k == 3:
              # The Dees had 2 hat-trick games
              if sum(x > 2 for x in sc) < 2: continue 
            yield from solve(gss[1:], k + 1, mxNotD, mxD, ngs_, ss + [sc])
      
      sols = set()        
      # two matches have at least 3 goals so we can have 9 wins at most
      for w in range(1, 10):
        d = 15 - w
        tp = w + 30   # total points: 3 * w + 2 * d
        # possible increments of the arithmetic sequence (ap, bp ... fp)
        for inc in range(1, 4):
          fp, r = divmod(tp - 15 * inc, 6)
          # we have 13 - 6 = 7 goals to divide
          # for games without the Dees determine the maximum goals per win
          maxNonD = 7 - (w - 3) 
          if r or fp < 0 or maxNonD < 1: continue
          # the Dees can have 5 wins at most, leaving (w - 5) wins for other teams
          # so for D we have 13 - 3 (other hat-trick) - 3 (non-hat-trick) - (w - 5) 
          # or 12 - w maximum goals per win
          maxD = 12 - w
          # check if both hat-trick games must be both wins
          if (ht_wins := 13 - 3 * 3 < w - 2):
            if fp + 2 * inc < 6: continue
          # points for the six teams
          ps = [fp + x * inc for x in range(5, -1, -1)]
          ws, ds = [0, 3] * w, [1] * d
          
          # generate a list of 15 wins/losses/draws
          for gs in gen_games(5, ws + ds, ps):
            # check hat-trick games
            if ht_wins and sum(x == 3 for x in gs[3]) < 2: continue
            # assign scores to games
            for goals in solve(gs, 0, maxNonD, maxD, 13, []):
              vsC = [gs[2] for i, gs in enumerate(goals) if i != 2]
              # in one game the Cees goalkeeper let in two before being substituted
              if max(vsC) < 2: continue
              sols.add(tuple(zip(goals[2], vsC)))
              
      print("answer:", ' or '.join(str(s)[1:-1] for s in sols)) 
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:17 am on 30 November 2025 Permalink | Reply

        @Frits: Your code gives a result of 1-1 in the CvB match. But the correct result is 1-0. (See my comment above for the complete set of results).

        Like

        • Frits's avatar

          Frits 3:51 am on 1 December 2025 Permalink | Reply

          @Jim,

          I found that out myself today (when comparing it to Brian’s output).

          Please use:

          vsC = [gs[2 if i > 2 else 1] for i, gs in enumerate(goals) if i != 2]
          

          Like

  • Unknown's avatar

    Jim Randell 7:32 am on 22 August 2025 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2485: [Circular field] 

    From The Sunday Times, 9th May 2010 [link] [link]

    Jack and Katy have inherited a circular field, with North Gate (N) at the northernmost point and East Gate (E), South Gate (S) and West Gate (W) at the appropriate points.

    Jack’s share of the field is one hectare in area. For each point P in his share, [exactly] three of the angles NPE, EPS, SPW and WPN are acute. For each point in Katy’s share, however, fewer than three of those angles are acute.

    How far is it between North Gate and East Gate?

    This puzzle was originally published with no title.

    [teaser2485]

     
    • Jim Randell's avatar

      Jim Randell 7:32 am on 22 August 2025 Permalink | Reply

      If we draw a circle and consider the angle subtended by the diameter with any point P, then:

      If P is on the circumference of the circle, then the angle is 90°.

      If P is inside the circle, then the angle is >90°.

      If P is outside the circle, then the angle is <90° (i.e. actute).

      So we can draw circles with diameters of NE, ES, SW, WN, and then Jack’s region is any point that is outside exactly 3 of the circles, and Katy’s region is any point that is outside fewer than 3 of the circles.

      The situation is this:

      Jack’s area (blue) consists of points that are outside exactly 3 of the circles, and Katy’s area (green) consists of points that are outside exactly 2 of the circles.


      If we suppose the distance we are interested in is 2d:

      Then the area of the semi-circle with diameter NE, radius = d is: (1/2) 𝛑 d^2.

      And this area is the same as that of the triangle NOE and two half-petals (= 1 petal):

      (1/2) 𝛑 d^2 = d^2 + P

      2P = (𝛑 − 2) d^2

      And Katy’s area (K) consists of 4 petals:

      K = 4P = 2 (𝛑 − 2) d^2

      The radius of the entire circular field is: OE = d √2.

      And so the total area of the field is then: 2 𝛑 d^2.

      And so Jack’s area (J) is:

      J = 2 𝛑 d^2 − 2 (𝛑 − 2) d^2

      J = 4 d^2

      And this is equal to 1 hectare = 10_000 sq m.

      4 d^2 = 10_000 sq m
      d^2 = 2500 sq m
      d = 50 m

      And the distance we are interested in is NE = 2d, hence:

      Solution: The distance between the N gate and E gate is 100 m.

      And Katy’s area is:

      K = 2 (𝛑 − 2) d^2
      K ≈ 5708 sq m

      So, only 57% the size of Jack’s area.

      Like

  • Unknown's avatar

    Jim Randell 8:59 am on 2 April 2025 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2561: Winning the double 

    From The Sunday Times, 23rd October 2011 [link] [link]

    Four teams, A, B, C and D, played each other once in a league, then played a knockout competition. They scored a total of 54 goals in the nine games, with a different score in each game; no team scored more than five goals in any game. For each team the average number of goals per game was a different whole number. Team B beat D by a margin of more than one goal in the knockout competition, but team A took the double, winning all their games.

    What was the score in the match CvD?

    [teaser2561]

     
    • Jim Randell's avatar

      Jim Randell 9:00 am on 2 April 2025 Permalink | Reply

      In the tournament the 6 matches are:

      AvB, AvC, AvD, BvC, BvD, CvD

      In the knockout there is a BvD match (which B won), but A won the knockout. So the matches in the knockout must be:

      1st round: AvC (A wins); BvD (B wins)
      Final: AvB (A wins)

      So A and B have played 5 matches, and C and D have played 4.

      A won all their matches, so their goal average must be at least 3.

      B won at least one match, so their goal average must be at least 1.

      The following run file finds possible scorelines in each match. It isn’t very fast though, but it does run in 10.3s under PyPy.

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # tournament:
      #
      #     A B C D
      #  A: - H I J  (A wins all matches)
      #  B: K - L M
      #  C: N P - Q
      #  D: R S T -
      #
      # knockout:
      #
      #  round 1: A v C = U - V (A wins); B v D = W - X (B wins)
      #  final: A v B = Y - Z (A wins)
      #
      # no team scored more than 5 goals, so goals (and goal averages = A, B, C, D) are all 0-5
      --base=6  # allows digits 0-5
      --distinct="ABCD"  # goal averages are all different
      
      # total goals scored by each team
      --macro="@goalsA = (H + I + J + U + Y)"
      --macro="@goalsB = (K + L + M + W + Z)"
      --macro="@goalsC = (N + P + Q + V)"
      --macro="@goalsD = (R + S + T + X)"
      
      # total of all the goals scored is 54
      "@goalsA + @goalsB + @goalsC + @goalsD == 54"
      
      # each game has a different score line
      "seq_all_different(ordered(x, y) for (x, y) in zip([H, I, J, L, M, Q, U, W, Y], [K, N, R, P, S, T, V, X, Z]))"
      
      # goal averages (are all different by --distinct)
      "A * 5 == @goalsA"
      "B * 5 == @goalsB"
      "C * 4 == @goalsC"
      "D * 4 == @goalsD"
      "A > 2"  # A won all their matches
      "B > 0"  # B won at least one match
      # and the total of all goals scored is 54
      "5 * (A + B) + 4 * (C + D) == 54"
      
      # B beat D by more than 1 goal in the knockout
      "W > X + 1"
      
      # A won all their games
      "H > K" "I > N" "J > R" "U > V" "Y > Z"
      
      --template="(H-K I-N J-R L-P M-S Q-T) (U-V W-X; Y-Z) (A B C D)"
      --header="(AvB AvC AvD BvC BvD CvD) (AvC BvD; AvB) (A B C D)"
      --solution=""
      --answer="(Q, T)"  # answer is the score in C vs D match
      --denest=-1  # CPython workaround
      

      Solution: The score in the C vs D match was 5 – 5.

      The program finds 48 ways to assign the scores in each match.

      For example, one of the assignments is:

      Tournament:
      A vs B = 5-1
      A vs C = 5-3
      A vs D = 5-0
      B vs C = 0-4
      B vs D = 0-3
      C vs D = 5-5

      Knockout:
      A vs C = 5-4
      B vs D = 2-0
      A vs B = 5-2

      A wins all their matches by scoring 5 goals, so their goal average is 5. And the goal averages for B, C, D are B = 1 (from 5 goals), C = 4 (from 16 goals), D = 2 (from 8 goals).

      Like

      • Frits's avatar

        Frits 1:20 pm on 2 April 2025 Permalink | Reply

        @Jim, typo in line 48: “J > R”.
        It makes the program slower (and 48 ways to assign the scores in each match)

        Like

      • Frits's avatar

        Frits 2:17 pm on 2 April 2025 Permalink | Reply

        Easy performance enhancement (from Brian’s analysis):

        --invalid="5,BCD"  # teams other than A cannot score 5 goals in all their games
        

        Like

        • Jim Randell's avatar

          Jim Randell 2:27 pm on 2 April 2025 Permalink | Reply

          Yes. With a little analysis there are some easy additional restrictions we can deduce.

          These bring the run time down to a much more reasonable 196ms.

          # more restrictions we can deduce
          --invalid="0,ABHIJUWY"
          --invalid="1,AW"
          --invalid="2,A"
          --invalid="4,X"
          --invalid="5,BCDKNRVXZ"
          

          And with more analysis we can go further. But I prefer to let the computer do the majority of the work.

          Like

    • Frits's avatar

      Frits 1:15 pm on 3 April 2025 Permalink | Reply

      The program loops over the requested score so we can early return as soon as a solution is found (and not report 47 similar solutions).

      The runtime approaches the 10 ms with CPython (as is feasible for most teasers).

      from itertools import product
      
      # Analysis: (credit B. Gladman)
      
      # If the average goals per game for A, B, C, D are a, b, c and d we then have
      #
      #     54 == 5.(a + b) + 4.(c + d) 
      #
      # (since A and B play five games whereas C and D play four). Here the possible
      # solutions (a + b, c + d) = (10, 1), (6, 6) or (2, 11).
       
      # Since the maximum goals a team can score in any game is 5, all the averages
      # are five or less. And, since the averages are all different, a + b and c + d
      # are both less than 10 which means that a + b == c + d == 6. Moreover, since
      # A wins all their games, other teams cannot score 5 goals in all their games
      # and must have goals per match averages of less than five.
      #
      # Hence the possibilities are:
      #
      #      (a, b) = (5, 1), (4, 2), (2, 4)
      #      (c, d) = (4, 2), (2, 4)
      #
      # And since the values are all different 
      #
      #      (a, b, c, d) =  (5, 1, 4, 2) or (5, 1, 2, 4)
      #
      #
      # Hence in goal totals (A, B, C, D) = (25, 5, 16, 8) or (25, 5, 8, 16)
      
      (H, I, J, U, Y, A, B) = (5, 5, 5, 5, 5, 5, 1)
      
      # tournament:
      #
      #     A B C D
      #  A: - H I J  (A wins all matches)
      #  B: K - L M
      #  C: N P - Q
      #  D: R S T -
      #
      # knockout:
      #
      #  round 1: A v C = U - V (A wins); B v D = W - X (B wins)
      #  final: A v B = Y - Z (A wins)
      #
      
      def inner(Q, T):
        for C in [2, 4]:
          D = 6 - C
          # A wins all their games with 5 goals  
          if 5 in {Q, T} and (Q, T) != (5, 5): continue
          for X in range(3):                        # X != 3 as W < 5
            for W in range(X + 2, 5):               # B beat D by more than one goal
              for R in range(5):
                S = D * 4 - (R + T + X)             # team D
                if not (0 <= S <= 4): continue      # S != 5 as M <= 3
                for V in range(5):                  # as W >= 2 then K,L,M,Z <= 3
                  if V == R: continue
                  for N in range(5):
                    if N in {V, R}: continue
                    P = C * 4 - (N + Q + V)         # team C
                    if not (0 <= P <= 4): continue  # P != 5 als L <= 3
                    # as W >= 2 then K,L,M,Z <= 3
                    for L, M in product(range(4), repeat=2): 
                      # different games
                      if (len(set(s := list(zip([L, M, Q, W], [P, S, T, X])))) !=
                          len(s)): continue
                      for Z in range(4):            # as W >= 2 then K,L,M,Z <= 3
                        if Z in {V, R, N}: continue
                        K = B * 5 - (L + M + W + Z) # team B
                        if not (0 <= K <= 3) or K in {V, R, N, Z}: continue
                        if H+I+J+K+L+M+N+P+Q+R+S+T+U+V+W+X+Y+Z != 54: continue
                        return str((Q, T))
      
      sols = []
      # Process scores CvD  (Q, T)
      for C, D in product(range(6), repeat=2):
        if (s := inner(C, D)):
          sols.append(s)
      
      print("answer(s):", ' or '.join(sols))  
      

      Like

    • Frits's avatar

      Frits 6:20 pm on 3 April 2025 Permalink | Reply

      Another one using more analysis. Internal runtime 22 microseconds (under PyPy).

      from itertools import product, permutations
      
      stup = lambda x, y: (x, y) if x < y else (y, x)
        
      def diffgames(a, b):
        return len(set(s := list(stup(x, y) for x, y in zip(a, b)))) == len(s)
        
      # Analysis: (credit B. Gladman)
      
      # If the average goals per game for A, B, C, D are a, b, c and d we then have
      #
      #     54 == 5.(a + b) + 4.(c + d) 
      #
      # (since A and B play five games whereas C and D play four). Here the possible
      # solutions (a + b, c + d) = (10, 1), (6, 6) or (2, 11).
       
      # Since the maximum goals a team can score in any game is 5, all the averages
      # are five or less. And, since the averages are all different, a + b and c + d
      # are both less than 10 which means that a + b == c + d == 6. Moreover, since
      # A wins all their games, other teams cannot score 5 goals in all their games
      # and must have goals per match averages of less thsn five.
      #
      # Hence the possibilities are:
      #
      #      (a, b) = (5, 1), (4, 2), (2, 4)
      #      (c, d) = (4, 2), (2, 4)
      #
      # And since the values are all different 
      #
      #      (a, b, c, d) =  (5, 1, 4, 2) or (5, 1, 2, 4)
      #
      #
      # Hence in goal totals (A, B, C, D) = (25, 5, 16, 8) or (25, 5, 8, 16)
      #
      #
      # In the knockout 1st round game BvD team B can score a maximum of 4 goals
      # and we have B - D >= 2. Team D can only score a maximum of 2 goals in this 
      # knockout round.
      # This means the average of team D cannot be 4 (as it would require at least 
      # 2 wins with 5 goals). So the averages of team C and D are 2 resp. 4.
      # In all games of team C they score at least 3 goals.
      
      (H, I, J, U, Y, A, B, C, D) = (5, 5, 5, 5, 5, 5, 1, 4, 2)
      (B5, C4, D4) = (5 * B, 4 * C, 4 * D)
      
      # tournament:
      #
      #     A B C D
      #  A: - H I J  (A wins all matches)
      #  B: K - L M
      #  C: N P - Q
      #  D: R S T -
      #
      # knockout:
      #
      #  round 1: A v C = U - V (A wins); B v D = W - X (B wins)
      #  final: A v B = Y - Z (A wins)
      #
      
      # inner loops
      def inner(Q, T):
        # team A wins all their games scoring 5 goals in each game
        if 5 in {Q, T} and (Q, T) != (5, 5): return
        for X in range(3):                      # X != 3 as W < 5
          for R in range(3):                    # as N and V (team C) use 3 and 4
            S = D4 - (R + T + X)             # team D
            if not (0 <= S <= 4): continue      # S != 5 as M <= 3
            for N, V in permutations(range(3, 5), 2): # team C goals
              P = C4 - (N + Q + V)           # team C
              if not (3 <= P <= 4): continue    # P != 5 als L <= 3
              for W in range(X + 2, 5):         # B beat D by more than one goal
                # K + N + R + V + Z = 10  and  K + L + M + W + Z = 5
                for L in range((LM := (V + N + R) - W - 5) + 1):       
                  M = LM - L
                  
                  # different games
                  if not diffgames([L, M, Q, W], [P, S, T, X]): continue
      
                  for Z in range(6 - W - L - M):  # K,L,M,Z <= 3 as W >= 2
                    if Z in {V, R, N}: continue
                    K = B5 - (L + M + W + Z) # team B
                    if not (0 <= K <= 3) or K in {V, R, N, Z}: continue
      
                    if H+I+J+K+L+M+N+P+Q+R+S+T+U+V+W+X+Y+Z != 54: continue
                    return str((Q, T))
      
      sols = []
      # process scores CvD, team C scores at least 3 goals in each game
      for Q, T in product(range(3, 6), range(6)):
        if (s := inner(Q, T)):
          sols.append(s)
      
      print("answer(s):", ' or '.join(sols))   
      

      Like

  • Unknown's avatar

    Jim Randell 7:57 am on 18 March 2025 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2528: [Football league] 

    From The Sunday Times, 6th March 2011 [link] [link]

    Some teams, A, B, C and so on, play each other once in a football league, earning three points for a win and one for a draw. So far, each team has played two games and scored the same total number of goals. At the moment, they stand in alphabetical order, with each team having a different number of points. The result of the game B v E was 3-2, and indeed all the wins so far have been by a one-goal margin.

    If you knew how many teams there were in the league, then it would be possible to work out all the results so far.

    What are those results?

    This puzzle was originally published with no title.

    [teaser2528]

     
    • Jim Randell's avatar

      Jim Randell 7:57 am on 18 March 2025 Permalink | Reply

      Each team has played 2 matches, so the possible outcomes are:

      w+w = 6 points
      w+d = 4 points
      w+l = 3 points
      d+d = 2 points
      d+l = 1 point
      l+l = 0 points

      So, if each of the teams has a different number of points, there can be no more than 6 teams in the league. And we are told there are at least 5 teams.

      There were quite a lot of conditions to keep track of, so I opted to use a MiniZinc model, and then look for a unique solution given the number of teams in the league (using the minizinc.py wrapper).

      The following Python/MiniZinc program runs in 649ms.

      from enigma import (irange, subsets, str_upper, sprintf, printf)
      from minizinc import MiniZinc
      
      # find solutions for <n> teams
      def solve(n):
        model = sprintf(r"""
      
        include "globals.mzn";
      
        set of int: Teams = 1..{n};
      
        % record goals scored in the matches
        % 0 = unplayed
        % >0 = goals scored + 1
        array [Teams, Teams] of var 0..10: x;
      
        % no team plays itself
        constraint forall (i in Teams) (x[i, i] = 0);
      
        % unplayed games are unplayed by both sides
        constraint forall (i, j in Teams where i < j) (x[i, j] = 0 <-> x[j, i] = 0);
      
        % each team has played exactly 2 games (= positive values)
        constraint forall (i in Teams) (sum (j in Teams) (x[i, j] > 0) = 2);
      
        % each team has scored the same number of goals (over both games)
        function var int: goal(var int: X) = if X > 0 then X - 1 else 0 endif;
        function var int: goals(var Teams: i) = sum (j in Teams) (goal(x[i, j]));
        constraint all_equal([goals(i) | i in Teams]);
      
        % points are in alphabetical order
        function var int: pt(var int: X, var int: Y) = if X > 0 /\ Y > 0 then 3 * (X > Y) + (X = Y) else 0 endif;
        function var int: pts(var Teams: i) = sum (j in Teams where j != i) (pt(x[i, j], x[j, i]));
        constraint forall (i in Teams where i > 1) (pts(i) < pts(i - 1));
      
        % score in B vs E match was 3-2 (so values are 4, 3)
        constraint x[2, 5] = 4 /\ x[5, 2] = 3;
      
        % all winning goal margins are 1
        constraint forall (i, j in Teams where i != j) (x[i, j] > x[j, i] -> x[i, j] = x[j, i] + 1);
      
        solve satisfy;
        """)
      
        m = MiniZinc(model, solver="minizinc -a --solver chuffed")
        for s in m.solve():
          yield s['x']
      
      # output matches for <n> teams, solution <x>
      def output(n, x):
        for i in irange(0, n - 1):
          for j in irange(i + 1, n - 1):
            (a, b) = (x[i][j] - 1, x[j][i] - 1)
            if a == b == -1: continue
            printf("{i} vs {j} = {a} - {b}", i=str_upper[i], j=str_upper[j])
        printf()
      
      # find possible points totals for 2 matches scoring 0, 1, 3.
      tots = set(x + y for (x, y) in subsets([0, 1, 3], size=2, select='R'))
      printf("tots = {tots}")
      
      # consider leagues with at least 5 teams
      for n in irange(5, len(tots)):
        ss = list(solve(n))
        k = len(ss)
        printf("[{n} teams -> {k} solutions]")
        if k == 1:
          for x in ss:
            output(n, x)
      

      Solution: The results are: A vs C = 2-1; A vs F = 3-2; B vs D = 2-2; B vs E = 3-2; C vs F = 4-3; D vs E = 3-3.

      The points are:

      A = 6 points (2 wins)
      B = 4 points (1 win + 1 draw)
      C = 3 points (1 win)
      D = 2 points (2 draws)
      E = 1 point (1 draw)
      F = 0 points

      With 5 teams there are 3 possible sets of matches, so this does not provide a solution:

      A vs C = 1-0; A vs E = 3-2; B vs D = 1-1; B vs E = 3-2; C vs D = 4-3;
      A vs C = 2-1; A vs D = 4-3; B vs D = 3-3; B vs E = 3-2; C vs E = 5-4
      A vs D = 1-0; A vs E = 2-1; B vs C = 0-0; B vs E = 3-2; C vs D = 3-3

      Like

  • Unknown's avatar

    Jim Randell 4:43 pm on 31 December 2024 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2596: Unusual factors 

    From The Sunday Times, 24th June 2012 [link] [link]

    There are no neat tests for divisibility by 7 or 13, so it’s unusual for these factors to feature in Teasers. Today we put that right.

    If you start with any three different non-zero digits, then you can make six different three-digit numbers using precisely those digits. For example, with 2, 5 and 7 you get 257, 275, 527, 572, 725 and 752. Here, one of their differences (572 − 257) is divisible by 7, and another (725
    − 257) is divisible by 13.

    Your task today is to find three different digits so that none of the six possible three-digit numbers and none of the differences between any pair of them is a multiple of either 7 or 13.

    What are the three digits?

    My review of puzzles posted in 2024 is at Enigmatic Code: 2024 in review.

    [teaser2596]

     
    • Jim Randell's avatar

      Jim Randell 4:44 pm on 31 December 2024 Permalink | Reply

      This Python program looks at possible sets of three digits, and then uses the [[ generate() ]] function to generate possible arrangements and differences. This means as soon as we find a number that fails the divisibility tests we can move on to the next set of digits.

      It runs in 65ms. (Internal runtime is 491µs).

      from enigma import (irange, subsets, nconcat, printf)
      
      # generate numbers to test from digits <ds>
      def generate(ds):
        seen = list()
        # consider all arrangements of the digits
        for n in subsets(ds, size=len, select='P', fn=nconcat):
          # return the arrangement
          yield n
          # return the differences
          for x in seen:
            yield abs(n - x)
          seen.append(n)
      
      # choose 3 digits
      for ds in subsets(irange(1, 9), size=3):
        # check numbers are not divisible by 7 or 13
        if any(n % 7 == 0 or n % 13 == 0 for n in generate(ds)): continue
        # output solution
        printf("digits = {ds}")
      

      Solution: The three digits are: 1, 4, 9.

      See also: A video by Matt Parker on divisibility tests [@youtube].

      Like

    • GeoffR's avatar

      GeoffR 7:34 pm on 1 January 2025 Permalink | Reply

      from itertools import permutations
      from enigma import nsplit
      
      N = []
      
      for a, b, c in permutations(range(1, 10), 3): 
          n1, n2 = 100*a + 10*b + c, 100*a + 10*c + b
          n3, n4 = 100*b + 10*a + c, 100*b + 10*c + a
          n5, n6 = 100*c + 10*a + b, 100*c + 10*b + a
          # Check three-digit numbers are not divisible by 7 or 13
          if any(x % 7 == 0 for x in (n1 ,n2, n3, n4, n5, n6)):
              continue
          if any(x % 13 == 0 for x in (n1, n2, n3, n4, n5, n6)):
              continue
          N.append(n1)
      
      # check differences for each number in the list
      # of potential candidates
      for m in N:
          x, y, z = nsplit(m)
          m1, m2 = m, 100*x + 10*z + y
          m3, m4 = 100*y + 10*x + z, 100*y + 10*z + x
          m5, m6 = 100*z + 10*x + y, 100*z + 10*y + x
          # a manual check of the 15 differences of each 6-digit number
          if abs(m1-m2) % 7 == 0 or abs(m1-m2) % 13 == 0: continue
          if abs(m1-m3) % 7 == 0 or abs(m1-m3) % 13 == 0: continue
          if abs(m1-m4) % 7 == 0 or abs(m1-m4) % 13 == 0: continue
          if abs(m1-m6) % 7 == 0 or abs(m1-m5) % 13 == 0: continue
          if abs(m1-m6) % 7 == 0 or abs(m1-m6) % 13 == 0: continue
          #
          if abs(m2-m3) % 7 == 0 or abs(m2-m3) % 13 == 0: continue
          if abs(m2-m4) % 7 == 0 or abs(m2-m4) % 13 == 0: continue
          if abs(m2-m5) % 7 == 0 or abs(m2-m5) % 13 == 0: continue
          if abs(m2-m6) % 7 == 0 or abs(m2-m6) % 13 == 0: continue
          #
          if abs(m3-m4) % 7 == 0 or abs(m3-m4) % 13 == 0: continue
          if abs(m3-m5) % 7 == 0 or abs(m3-m5) % 13 == 0: continue
          if abs(m3-m6) % 7 == 0 or abs(m3-m6) % 13 == 0: continue
          #
          if abs(m4-m5) % 7 == 0 or abs(m4-m5) % 13 == 0: continue
          if abs(m4-m6) % 7 == 0 or abs(m4-m6) % 13 == 0: continue
          if abs(m5-m6) % 7 == 0 or abs(m5-m6) % 13 == 0: continue
          if x < y < z:
              print(f"The three digit numbers are {x}, {y} and {z}.")
      
      # The three digit numbers are 1, 4 and 9.
      

      I resorted to a manual check of the differences of the 6 numbers arising from each 3 digit candidate, as it was not coming out easily in code.

      Like

  • Unknown's avatar

    Jim Randell 11:16 am on 19 November 2024 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2613: Anagrammatical 

    From The Sunday Times, 21st October 2012 [link] [link]

    The number 258 is special in the following way. If you write down its six different anagrams (258, 285, 528, etc.), then calculate the fifteen differences between any two of those six (27, 270, 243, etc.) and then add up those fifteen differences, you get 4644, which is a multiple of the number 258 that you started with!

    Which higher three-figure number has the same property?

    [teaser2613]

     
    • Jim Randell's avatar

      Jim Randell 11:17 am on 19 November 2024 Permalink | Reply

      Here is a constructive solution.

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

      from enigma import (irange, nsplit, subsets, nconcat, printf)
      
      # check a 3 digit number
      def check(n):
        # find different rearrangements of the digits
        rs = sorted(subsets(nsplit(n), size=len, select='mP', fn=nconcat))
        if len(rs) < 2: return False
        # calculate the total sum of the pairwise differences
        t = sum(y - x for (x, y) in subsets(rs, size=2))
        if t % n != 0: return False
        printf("{n} -> {t}")
        return True
      
      # check the given value
      assert check(258)
      
      # find the next 3-digit number after 258 that works
      for n in irange(259, 999):
        if check(n): break
      

      Solution: The next 3-digit number with the same property is 387.

      And there are no higher 3-digit numbers with the property, but there are lower ones.

      The complete list of 3-digit numbers (and the sum of the differences) with this property is:

      129 → 6192
      172 → 4644
      258 → 4644
      387 → 3870

      Like

      • Frits's avatar

        Frits 5:03 pm on 21 November 2024 Permalink | Reply

        The differences between any two of these four 3-digit numbers are multiples of 43.

        See [https://brg.me.uk/?page_id=300].

        Like

    • Ruud's avatar

      Ruud 7:18 pm on 19 November 2024 Permalink | Reply

      This code finds all three digit numbers that meet the conditions given:

      import istr
      
      for i in istr.range(100, 1000):
          if i.all_distinct() and sum(abs(x - y) for x, y in istr.combinations(istr.concat(istr.permutations(i)), 2)).is_divisible_by(i):
              print(i)
      

      Like

    • GeoffR's avatar

      GeoffR 2:56 pm on 21 November 2024 Permalink | Reply

      from enigma import nsplit
      
      for n in range(258, 999):
          diff = 0
          a, b, c = nsplit(n)
          if 0 in (a, b, c):
              continue
          # Form the six numbers
          n1, n2 = 100*a + 10*b + c, 100*a + 10*c + b
          n3, n4 = 100*b + 10*a + c, 100*b + 10*c + a
          n5, n6 = 100*c + 10*a + b, 100*c + 10*b + a
          if not len(set((n1, n2, n3, n4, n5, n6))) == 6:
              continue
          # Find sum of fifteen differences
          diff = abs(n1-n2) + abs(n1-n3) + abs(n1-n4) + abs(n1-n5) + abs(n1-n6)\
                  + abs(n2-n3) + abs(n2-n4) + abs(n2-n5) + abs(n2-n6)\
                  + abs(n3-n4) + abs(n3-n5) + abs(n3-n6)\
                  + abs(n4-n5) + abs(n4-n6)\
                  + abs(n5-n6)
          #  Sum of fifteen differences is a multiple of the number 
          if diff % n == 0:
              print(f"Number = {n}, Differences sum = {diff}")
      
      # Number = 258, Differences sum = 4644
      # Number = 387, Differences sum = 3870
      
      

      Interesting that the differences sum is ten times the number.

      Like

  • Unknown's avatar

    Jim Randell 10:43 am on 17 September 2024 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2604: Foreign friends 

    From The Sunday Times, 19th August 2012 [link] [link]

    The telephone number of my Japanese friend is a ten-digit number written as a group of five two-digit numbers. The phone number does not start with a 0, and no digit is used more than once. The numbers in the group are in increasing order, no two of them have a common factor greater than 1, and one of them is a fourth power. Also, the product of the numbers in the group is divisible by four of the first five primes.

    The telephone number of my Russian friend is also a ten-digit number but it is written as a group of two five-digit numbers. The number and group have the same properties as those stated above. The second digit of the Russian number is double the second digit of the Japanese number.

    What are the two telephone numbers?

    [teaser2604]

     
    • Jim Randell's avatar

      Jim Randell 10:44 am on 17 September 2024 Permalink | Reply

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It runs in 772ms. (Internal runtime is 582ms, although this can be reduced to 377ms by splitting out the [[ is_coprime(@jps) ]] check into 10 separate coprime tests (one for each pair)).

      Note: An up-to-date version of enigma.py is required for the multiple argument version of is_coprime().

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # Japanese phone number = AB CD EF GH IJ
      # Russian phone number = KLMNP QRSTU
      --distinct="ABCDEFGHIJ,KLMNPQRSTU"
      --macro="@jps = AB, CD, EF, GH, IJ"
      --macro="@rus = KLMNP, QRSTU"
      
      # neither phone number starts with 0
      --invalid="0,AK"
      
      # in the groups ...
      # the numbers are in increasing order
      "A < C" "C < E" "E < G" "G < I"
      "K < Q"
      
      # the numbers are pairwise co-prime
      "is_coprime(@jps)"
      "is_coprime(@rus)"
      
      # use "at least" or "exactly" for counting?
      --code="count = icount_at_least"  # or: icount_exactly
      
      # one of them is a fourth power
      --code="pow4s = set(i**4 for i in irange(0, 17))"
      --code="pow4 = lambda ns: count(ns, is_in(pow4s), 1)"
      "pow4([@jps])"
      "pow4([@rus])"
      
      # the product is divisible by four of {2, 3, 5, 7, 11}
      --code="_divs = (lambda n, ds, k: count(ds, (lambda d: n % d == 0), k))"
      --code="divs = (lambda ns: _divs(multiply(ns), [2, 3, 5, 7, 11], 4))"
      "divs([@jps])"
      "divs([@rus])"
      
      # 2nd digit of the Russian number is double the 2nd digit of the Japanese number
      "2 * B = L"
      --invalid="5|6|7|8|9,B"  # so, B < 5
      
      --template="(AB CD EF GH IJ) (KLMNP QRSTU)"
      --solution=""
      --denest=-1
      

      Solution: The numbers are: (23 49 50 67 81) and (46970 83521).

      The individual parts factorise as:

      (23 49 50 67 81) = ([23] [7×7] [2×5×5] [67] [3×3×3×3])
      (46970 83521) = ([2×5×7×11×61] [17×17×17×17])

      From which we see in each number there are no prime factors common to any two of the parts, so each is pairwise coprime.

      And the final part in each is the fourth power (of a prime).

      The top number has divisors of: 2, 3, 5, 7 (but not 11).

      The bottom number has divisors of: 2, 5, 7, 11 (but not 3).

      Like

      • Ruud's avatar

        Ruud 12:22 pm on 19 September 2024 Permalink | Reply

        Brute force:

        from istr import istr
        import math
        import functools
        import operator
        
        istr.repr_mode("str")
        solutions = {2: set(), 5: set()}
        
        for s in istr.permutations(istr.range(10)):
            if s[0] != 0:
                for k in (2, 5):
                    numbers = tuple(istr("".join(s[i : i + k])) for i in range(0, len(s), k))
                    if all(number0 < number1 for number0, number1 in zip(numbers, numbers[1:])):
                        if sum((float(number) ** (1 / 4)).is_integer() for number in numbers) == 1:
                            if sum(functools.reduce(operator.mul, numbers) % prime == 0 for prime in (2, 3, 5, 7, 11)) == 4:
                                if all(math.gcd(int(number0), int(number1)) == 1 for number0, number1 in istr.combinations(numbers, 2)):
                                    solutions[k].add(istr(" ").join(numbers))
        for japanese in solutions[2]:
            for russian in solutions[5]:
                if japanese[1] * 2 == russian[1]:
                    print(f"{japanese=} {russian=}")
        

        Like

    • GeoffR's avatar

      GeoffR 4:41 pm on 19 September 2024 Permalink | Reply

      # Japanese telephone number is AB CD EF GH IJ
      # Russian telephone number is KLMNP QRSTU
      
      from itertools import permutations
      from enigma import is_coprime
      
      # max 5 digits for the 4th power
      pow4 = [n * n * n * n for n in range(2, 18)]
      
      dgts = set(range(10))
      
      # Japanese Telephone Number
      for p1 in permutations(dgts, 4):
        jp_found = 0
        A, B, C, D = p1
        if 0 in (A, C): continue
        # Constraint on 2nd digit of 1st Tel No. i.e. L = 2 *  B
        if B == 0 or B > 4: continue
        if not A < C: continue
        AB, CD = 10*A + B, 10*C + D
        if not is_coprime(AB, CD): continue
      
        # Find remaining digits in Japanese Telephone Number
        q1 = dgts.difference({A, B, C, D})
        for p2 in permutations(q1):
          E, F, G, H, I, J = p2
          if 0 in (E, G, I): continue
          if not A < C < E < G < I: continue
          EF, GH, IJ = 10*E + F, 10*G + H, 10*I + J
          
          # check ten co-prime conditions 
          if is_coprime(CD, EF) and is_coprime(EF, GH) and is_coprime(GH, IJ) \
            and is_coprime(AB, EF) and is_coprime(AB, GH) and is_coprime(AB, IJ) \
            and is_coprime(CD, GH ) and is_coprime(CD, IJ) and is_coprime(EF, IJ):
              
            # Check for a fourth power
            if any (x in pow4 for x in (AB, CD, EF, GH, IJ)):
              # Find number of divisors of Japanese Tel. No.
              cnt = 0
              for num in (AB, CD, EF, GH, IJ):
                for p3 in (2, 3, 5, 7, 11):
                  if num % p3 == 0:
                    cnt += 1
                if cnt == 4:
                  print(f"Japanese No. is {AB} {CD} {EF} {GH} {IJ}.")
                  jp_found = 1
      
                # Russian Telephone Number - find first half of Tel. No.
                for p5 in permutations(dgts,5):
                  K, L, M, N, P = p5
                  if K == 0: continue
                  if L != 2 * B: continue
                  KLMNP = 10000*K + 1000*L + 100*M + 10*N + P
                  # Assume 4th power is QRSTU  
                  if KLMNP not in pow4:
                    cnt2 = 0
                    for p5 in (2, 3, 5, 7, 11):
                      if KLMNP % p5 == 0:
                        cnt2 += 1
                  if cnt2 == 4:
                      
                    # Find second half of Russian Tel. No.
                    q6 = dgts.difference({K, L, M, N, P})
                    for p7 in permutations(q6):
                      Q, R, S, T, U = p7
                      QRSTU = 10000*Q + 1000*R + 100*S + 10*T + U
                      if Q == 0: continue
                      if not KLMNP < QRSTU: continue
                      if not is_coprime(KLMNP, QRSTU): continue
                      if QRSTU in pow4 and jp_found == 1:
                        print(f"Russian No. is {KLMNP} {QRSTU}.")
      
      #Japanese No. 23 49 50 67 81
      # Russian No. is 46970 83521
      
      
      
      

      Like

  • Unknown's avatar

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

    Teaser 2673: Footprints 

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

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

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

    [teaser2673]

     
    • Jim Randell's avatar

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

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

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

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

      Run: [ @replit ]

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

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

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

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


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

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

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

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

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

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

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

      Run: [ @replit ]

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

      Like

  • Unknown's avatar

    Jim Randell 8:46 am on 22 August 2023 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2635: Three sisters 

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

    In Shakespeare’s less well-known prequel, King Lear shared all of his estate amongst his three daughters, each daughter getting a fraction of the estate. The three fractions, each in their simplest form, had numerators less than 100 and had the same denominators. Cordelia got the least, with Regan getting more, and Goneril the most (but her share was less than three times Cordelia’s). Each of the three fractions gave a decimal which recurred after three places (as in 0.abcabc…) and each digit from 1 to 9 occurred in one of the three decimals.

    What were the three fractions?

    [teaser2635]

     
    • Jim Randell's avatar

      Jim Randell 8:46 am on 22 August 2023 Permalink | Reply

      The recurring decimal 0.(abc)… is a representation of the fraction abc/999.

      But the three fractions we are dealing with can all be reduced to fractions with a 2-digit numerator, and a common denominator.

      So the common denominator must be a divisor of 999.

      This Python program runs in 54ms. (Internal runtime is 2ms)

      Run: [ @replit ]

      from enigma import (divisors, decompose, gcd, recurring, join, printf)
      
      # return viable repetends
      def repetend(n, d):
        r = recurring(n, d)
        return (None if r.nr else r.rr)
      
      # consider possible denominator values 3 < d < 100
      for d in divisors(999):
        if d < 4: continue
        if d > 297: break
      
        # calculate ascending splits of the denominator
        for (C, R, G) in decompose(d, 3, increasing=1, sep=1, min_v=1, max_v=99):
          # G's share is less than 3 times C's
          if not (G < 3 * C): continue
          # check fractions are in lowest terms
          if any(gcd(n, d) != 1 for n in (C, R, G)): continue
      
          # find the repetends
          reps = list(repetend(n, d) for n in (C, R, G))
          if None in reps: continue
          # check the digits used
          ds = join(reps)
          if '0' in ds or len(set(ds)) < 9: continue
      
          # output solution
          (rC, rR, rG) = reps
          printf("C = {C}/{d} = 0.({rC})...; R = {R}/{d} = 0.({rR})...; G = {G}/{d} = 0.({rG})...")
      

      Solution: The fractions are: Cordelia = 6/37, Reagan = 14/37, Goneril = 17/37.

      As decimals:

      C = 0.(162)…
      R = 0.(378)…
      G = 0.(459)…

      Like

  • Unknown's avatar

    Jim Randell 10:43 am on 4 July 2023 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2625: Mind the gap 

    From The Sunday Times, 13th January 2013 [link] [link]

    If you list in increasing order all ten-figure numbers with no repeat digit, then the first is 1023456789 and the last is 9876543210. The differences between consecutive numbers in the list vary wildly but no difference is ever equal to the average of all the differences between the consecutive numbers.

    What difference between consecutive numbers comes closest to the average?

    [teaser2625]

     
    • Jim Randell's avatar

      Jim Randell 10:44 am on 4 July 2023 Permalink | Reply

      The way [[ subsets(..., select='P') ]] works the numbers will be produced in numerical order, so we don’t need to order them.

      The distances between consecutive numbers are recorded in a [[ multiset() ]], so we don’t have to bother processing duplicate values.

      But there are a lot of numbers to process, so the program is quite slow.

      This Python program runs in 871ms.

      Run: [ @replit ]

      from enigma import (irange, subsets, nconcat, multiset, tuples, rdiv, Accumulator, printf)
      
      # construct pandigitals in numerical order
      def generate():
        for ds in subsets(irange(0, 9), size=10, select='P'):
          if ds[0] != 0:
            yield nconcat(ds)
      
      # record the collection of differences between consecutive pandigitals
      ds = multiset.from_seq(b - a for (a, b) in tuples(generate(), 2))
      # calculate the mean difference
      m = ds.avg(div=rdiv)
      printf("mean difference = {m}", m=float(m))
      
      # find the closest difference
      r = Accumulator(fn=min, collect=1)
      for d in ds.keys():
        r.accumulate_data(abs(m - d), d)
      # output solution(s)
      for d in r.data:
        printf("closest difference = {d} [{n} times]", n=ds[d])
      

      Solution: The difference that comes closest to the mean is 2727.

      Which occurs 1872 times, for example in the following pairs:

      (1034679852, 1034682579)

      (9865317420, 9865320147)

      There are 500 different difference values.

      The smallest difference is 9 (which happens 322560 times):

      (1023456789, 1023456798)

      (9876543201, 9876543210)

      And the largest difference is 104691357 (which happens 2 times):

      (1098765432, 1203456789)
      (8796543210, 8901234567)


      With some analysis we can get a faster program.

      We can calculate the mean difference fairly easily:

      In an ordered sequence (a, b, c, …, x, y, z) the sequence of consecutive distances is:

      (b − a, c − b, …, y − x, z − y)

      and this has one fewer element than the original sequence.

      And the sum of this sequence is simply:

      sum = z − a

      i.e. the difference between the largest pandigital and the smallest pandigital.

      So, if we knew the number of pandigitals in the original sequence we can calculate the mean.

      There are factorial(10) possible digit sequences, but factorial(9) of them will have a leading zero, so are rejected.

      Hence, there are 3265920 pandigitals (and the number of distances is 1 less than this).

      And so the mean distance is:

      mean distance = (9876543210 − 1023456789) / 3265919 = 2710.7489…

      So we see that no individual difference is equal to the mean because the mean is not an integer and all individual differences are. (And in fact they all must be multiples of 9).

      Now, if we consider two consecutive pandigitals that are at a distance apart that is close to the mean, then they are going to look something like:

      abcde|vwxyz
      abcde|xzyvw

      Where there is some shared prefix, and then the remaining digits appear in a different arrangement for the two numbers. And when calculating the difference between them the common prefix disappears.

      So the difference for the numbers in the example is then:

      xzyvw − vwxyz

      And we can calculate the differences by just looking at sets of numbers that are not part of the common prefix, and calculating the closest difference using those set.

      We are looking for a difference close to 2711, so we can examine sets of digits that have 5 elements.

      This Python program runs in 123ms.

      Run: [ @replit ]

      from enigma import (
        factorial, rdiv, ndigits, Accumulator,
        irange, subsets, nconcat, tuples, printf
      )
      
      # calculate the mean distance
      m = rdiv(9876543210 - 1023456789, 9 * factorial(9) - 1)
      printf("mean difference = {m}", m=float(m))
      
      # record the closest difference to the mean
      r = Accumulator(fn=min)
      
      # consider possible sets of digits
      k = ndigits(int(m)) + 1
      for ds in subsets(irange(0, 9), size=k):
        # find consecutive numbers using this set of digits
        ns = subsets(ds, size=len, select='P', fn=nconcat)
        # find differences between consecutive numbers closest to the mean
        for (a, b) in tuples(ns, 2):
          d = b - a
          r.accumulate_data(abs(m - d), d)
      
      # output solution
      printf("closest difference = {r.data}")
      

      Like

    • Frits's avatar

      Frits 5:22 pm on 4 July 2023 Permalink | Reply

        
      from enigma import factorial
      from itertools import permutations, combinations
      
      # calculate the mean distance
      m = (9876543210 - 1023456789) / (factorial(10) - factorial(9) - 1)
      print(f"mean difference = {m}")
      
      mn, md = 9999999, 9999999
      
      # abcde with c > d > e 
      # choose last three descending digits
      for c in combinations('9876543210', 3):
        # choose first 2 digits
        for p in permutations(set('0123456789').difference(c), 2):
          s = ''.join(p + c)
      
          # jump within same 10000:  like 32510 to 35012
          if s[1] < '7':
            # jump 3 higher for a difference of 2xxx
            s2 = str(int(s[1]) + 3)
            if s2 not in s[1:]: continue
            new = int(s[0] + s2 + ''.join(sorted(set(s[1:]).difference(s2))))
          else:  
            ints = [int(x) for x in s]
            # we must be able to jump to a higher 10000
            a1 = str(ints[0] + 1)
            if s[0] == '9' or a1 not in s: continue
            # digit 3 higher then 2nd digit must be present
            if str(ints[1] - 7) not in s: continue
            # digits higher than 2nd digit may not occur in last three positions
            if any(str(j) in s[2:] for j in range(ints[1] + 1, 10)): continue
            
            # jump to higher 10000:   like 17420 to 20147
            new = int(a1 + ''.join(sorted(set(s).difference(a1))))
        
          i = int(s)
          # check for closeness to target
          if abs(m - new + i) <= mn:
            mn, md = abs(m - new + i), new - i
      
      print("answer:", md)
      

      Like

      • Frits's avatar

        Frits 1:27 pm on 5 July 2023 Permalink | Reply

        With the abcde|vwxyz method we might miss other combinations that can come relatively close to 2710.

        like 2345698710, 2345701689 with difference 2979

        Like

        • Jim Randell's avatar

          Jim Randell 1:37 pm on 5 July 2023 Permalink | Reply

          We can reduce the size of the prefix considered at line 14:

          k = ndigits(int(m)) + 2
          

          Although it turns out the common prefix is size 5 in all 1872 cases, so we are OK.

          Originally I allowed 1 digit more than the length of the mean for the suffix, but this does assume that the closest value is going to be reasonably close to the mean value.

          Like

  • Unknown's avatar

    Jim Randell 10:33 am on 25 May 2023 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2183: The punishment fits the crime 

    From The Sunday Times, 18th July 2004 [link]

    Recently I was teaching the class about Pascal’s triangle, which starts:

    1
    1  1
    1  2  1
    1  3  3  1
    1  4  6  4  1

    Subsequent rows have a 1 at each end of each row, with any other number in the row obtained by adding the two numbers diagonally above it. Then a row gives, for example:

    (1 + x)⁴  = 1 + 4x + 6x² + 4x³ + x

    Six pupils disrupted the lesson, and so I kept them in after school. I gave each of them a different prime number and told them to work out its 10th power without a calculator.

    “That’s too easy”, said Blaise, whose prime was 3. “I’ll work out the product of everyone else’s answers instead”. Then she read out the 41-digit answer and left!

    What was her 41-digit answer?

    [teaser2183]

     
    • Jim Randell's avatar

      Jim Randell 10:34 am on 25 May 2023 Permalink | Reply

      In order for the 10th power to be a 41 digit number the product of the 5 primes must be in the range:

      min = ceil(10^(40/10)) = ceil(10^4) = 10000
      max = floor(10^(41/10)) = floor(10^4.1) = 12589

      Excluding 3, the five smallest primes are (2, 5, 7, 11, 13) which have a product of 10010, which is in the range we require.

      And:

      10010^10 = 10100451202102522101200450100010000000000

      We can calculate this number using Pascal’s Triangle as follows:

      10010^10 = (10(1000 + 1))^10
      = (10^10)(1000 + 1)^10

      And row 10 (numbering from 0) of Pascal’s triangle is:

      10: (1  10  45  120  210  252  210  120  45  10  1)

      So we can calculate (1000 + 1)^10, using the polynomial derived from this row with x = 1000).

      The terms are separated by 1000, and each value in the row is 3 digits or less, so we can just write the result out by padding the values in the row to 3 digits:

      001 010 045 120 210 252 210 120 045 010 001

      Multiplying this by 10^10 just involves adding 10 zeros to the end, to give the 41 digit number:

      10100451202102522101200450100010000000000


      Or using Python.

      The following Python program runs in 57ms. (Internal runtime is 228µs).

      Run: [ @replit ]

      from enigma import (intc, intf, fdiv, primes, inf, subsets, diff, multiply, ndigits, printf)
      
      def solve(n, k):
        # min and max products for an n digit number
        m = intc(pow(10, fdiv(n - 1, k)))
        M = intf(pow(10, fdiv(n, k)))
        printf("[n={n} -> [{m}, {M}]]")
      
        # consider the largest prime
        for p in primes.irange(2, inf):
          if p == 3: continue
          # choose 4 smaller primes (excluding 3)
          f = 0
          for ps in subsets(diff(primes.between(2, p - 1), [3]), size=4, fn=list):
            ps.append(p)
            x = multiply(ps)
            if x > M:
              if f == 0: return
              break
            if not (x < m or x > M):
              x10 = pow(x, 10)
              printf("{ps} -> {x} -> {x10} [{d} digits]", d=ndigits(x10))
            f = 1
      
      # solve for 41 digit numbers
      solve(41, 10)
      

      Like

    • GeoffR's avatar

      GeoffR 4:45 pm on 26 May 2023 Permalink | Reply

      primes = (2, 5, 7, 11, 13, 17, 19)  # prime 3 excluded from description
      
      # The highest product of primes to power of 10 in the tuple below
      # .. i.e. (7,11,13,17,19) gives a 56 digit number
      # .. so a more than an adequate range to find a 41 digit product.
      
      from itertools import combinations
      
      for P1 in combinations(primes, 5):
          # choose 5 primes from a tuple of 7 primes
          p1, p2, p3, p4, p5 = P1
      
          num = p1**10 * p2**10 * p3**10 * p4**10 * p5**10
          if len(str(num)) == 41:
             print(f"41 digit number is {num}")
             exit()  # only one answer needed
      
      # 41 digit number is 10100451202102522101200450100010000000000
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:17 am on 7 March 2023 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2714: Group of death 

    From The Sunday Times, 28th September 2014 [link] [link]

    In a Champions League group four teams play each other twice with the top two qualifying for the next stage. If two teams are level on points then their head-to-head results determine their position. In one group, after five games each, the teams in order were A, B, C, D, each with a different number of points. At that stage team A was bound to qualify and teams B and C could still top the group. But the final two games were draws. All games had different results, no game had more than five goals, and team D had the same number of “goals for” as “goals against”. Team A scored fewer goals than any other team whilst D scored more than any other.

    What were the results of the two games B v C?

    [teaser2714]

     
    • Jim Randell's avatar

      Jim Randell 10:18 am on 7 March 2023 Permalink | Reply

      I am assuming the point are allocated as: “3 points for a win, 1 point for a draw”. (Although the puzzle also seems to work if “2 points for a win” is used instead).

      Each team plays their three opponents twice, so there are 12 matches in total.

      There are not more than 5 goals scored in any match, so possible scores are:

      drawn = (0, 0) (1, 1) (2, 2)
      won/list = (1, 0) (2, 0) (3, 0) (4, 0) (5, 0) (2, 1) (3, 1) (4, 1) (3, 2)

      This accounts for all 12 matches, so each score must be used.

      And the final 2 matches are drawn, so there is exactly 1 draw in the first 10 matches.

      Programming a constructive solution for this puzzle is quite involved. I hope that a manual solution is more fun. I used the [[ Football() ]] helper class from the enigma.py library to save on some of the coding, but things are complicated a bit by the fact each pair of teams meet twice, and there is nothing to distinguish the two games, so to avoid duplicated solutions I store the results for the two games in order with the “best” game (from the first team’s point of view) given first.

      This Python program runs in 253ms.

      Run: [ @replit ]

      from enigma import (Football, subsets, partitions, update, compare, ordered, rev, join, map2str, printf)
      
      football = Football(points=dict(w=3, d=1))
      
      # possible scores, no game had more than 5 goals
      scores = dict()
      scores['d'] = [(0, 0), (1, 1), (2, 2)]
      scores['w'] = [(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (5, 0)]
      scores['l'] = list(map(rev, scores['w']))
      
      # label the matches (each label corresponds to 2 matches)
      teams = "ABCD"
      matches = list(x + y for (x, y) in subsets(teams, size=2))
      
      # complete possible match pairs (additional win/lose outcomes)
      pairs = {
        '-': ['ww', 'wl', 'll'],
        'x': ['wx', 'lx'],
        'd': ['wd', 'dl'],
      }
      
      # swap pairs into canonical order
      _pair = { 'dw': 'wd', 'lw': 'wl', 'ld': 'dl' }
      pair = lambda x: _pair.get(x, x)
      
      # allocate win/lose outcomes to matches
      def allocate_matches(ks, ms):
        # are we done?
        if not ks:
          yield ms
        else:
          k = ks[0]
          v = ms.get(k, '-')
          if len(v) > 1:
            # skip this key
            yield from allocate_matches(ks[1:], ms)
          else:
            # allocate both outcomes
            for x in pairs[v]:
              yield from allocate_matches(ks[1:], update(ms, [(k, x)]))
      
      # allocate scores to matches
      def allocate_scores(ks, ms, used=set(), ss=dict()):
        # are we done?
        if not ks:
          yield (ss, used)
        else:
          k = ks[0]
          if k in ss:
            # skip this key
            yield from allocate_scores(ks[1:], ms, used, ss)
          else:
            # allocate both scores
            (a, b) = ms[k]
            for x in scores[a]:
              x_ = ordered(*x)
              if x_ in used: continue
              for y in scores[b]:
                y_ = ordered(*y)
                # remove duplicate solutions
                if (a == b and not x_ > y_) or y_ in used: continue
                yield from allocate_scores(ks[1:], ms, used.union({x_, y_}), update(ss, [(k, (x, y))]))
      
      # extract match/teams values from dict d
      def extract(d, t, fn):
        (gs, ts) = (list(), list())
        for (k, v) in d.items():
          if t in k:
            gs.extend(v)
            ts.extend([(0 if k[0] == t else 1)] * len(v))
        return fn(gs, ts)
      
      # construct the table for team t, using matches in ms
      table = lambda ms, t: extract(ms, t, football.table)
      
      # goals for/against team t, using scores in ss
      goals = lambda ss, t: extract(ss, t, football.goals)
      
      # output the matches and scores
      def output_matches(ms, ss, **kw):
        def _process(d, fn):
          d_ = dict()
          for (k, (a, b)) in d.items():
            d_[k] = a
            d_[rev(k)] = fn(b)
          return d_
        football.output_matches(_process(ms, football.swap), _process(ss, rev), **kw)
      
      # does team <x> beat team <y> in the table <tbl> and match outcomes <ms>?
      def beats(x, y, tbl, ms):
        # a team does not beat itself
        if x == y: return 0
        # can it be decided on points?
        r = compare(tbl[x].points, tbl[y].points)
        if r == 1: return 1
        if r == -1: return 0
        # can it be decided on the outcome of head-to-head games?
        (k, a, b) = ((x + y, 0, 1) if x < y else (y + x, 1, 0))
        gs = ms[k]
        (X, Y) = (football.table(gs, [a, a]), football.table(gs, [b, b]))
        r = compare(X.points, Y.points)
        if r == 1: return 1
        if r == -1: return 0
        # there may be other ways to order the teams (e.g. goal difference
        # or goals scored), but these are not mentioned
        return 0
      
      # check all possible final 2 games for required scenario
      def check(ms, f1, f2):
        # check conditions for teams A, B, C
        fB = fC = 0
        for (g1, g2) in football.games('wdl', repeat=2):
          ms_ = update(ms, [(f1, pair(ms[f1][0] + g1)), (f2, pair(ms[f2][0] + g2))])
          # calculate the final table
          tbl = dict((t, table(ms_, t)) for t in teams)
          # can A fail to qualify? (i.e. fail to beat at least 2 of the other teams)
          if not (sum(beats('A', t, tbl, ms_) for t in teams) >= 2): return False
          # can B come top? (i.e. beat the other 3 teams)
          if fB == 0 and sum(beats('B', t, tbl, ms_) for t in teams) == 3: fB = 1
          # can C come top? (i.e. beat the other 3 teams)
          if fC == 0 and sum(beats('C', t, tbl, ms_) for t in teams) == 3: fC = 1
        return (fB and fC)
      
      # find viable scenarios
      def generate():
        # choose the final 2 matches (which are draws)
        for ((t1, t2), (t3, t4)) in partitions(teams, 2):
          (f1, f2) = (t1 + t2, t3 + t4)
          ms0 = { f1: 'x', f2: 'x' }
          # of the remaining matches exactly 1 of them is a draw
          for d in matches:
            ms1 = update(ms0, [(d, ('dx' if d in ms0 else 'd'))])
            # choose win/lose outcomes for the remaining matches
            for ms2 in allocate_matches(matches, ms1):
              # calculate the table before the final 2 matches are played
              (A, B, C, D) = (table(ms2, t) for t in teams)
              if not (A.points > B.points > C.points > D.points): continue
              # in the final 2 games B or C _could_ gain the top spot
              # so A cannot be more than 3 points ahead of C
              if A.points > C.points + 3: continue
              # check all possible final 2 games
              if not check(ms2, f1, f2): continue
      
              # now include the final games (both are, in fact, drawn)
              ms3 = update(ms2, list((k, pair(ms2[k][0] + 'd')) for k in (f1, f2)))
              (A, B, C, D) = (table(ms3, t) for t in teams)
      
              # allocate scores for D
              (Ds, As) = (list(k for k in matches if t in k) for t in "DA")
              for (ss1, us1) in allocate_scores(Ds, ms3):
                # D has equal for and against (and scored more goals than any other team)
                (fD, aD) = goals(ss1, 'D')
                if fD != aD: continue
                # allocate remaining scores for A
                for (ss2, us2) in allocate_scores(As, ms3, us1, ss1):
                  # A scored fewer goals than any other team
                  (fA, aA) = goals(ss2, 'A')
                  if not (fA < fD): continue
                  # allocate remaining scores (B and C)
                  for (ss3, us3) in allocate_scores(matches, ms3, us2, ss2):
                    ((fB, aB), (fC, aC)) = (goals(ss3, t) for t in "BC")
                    if not (fA < fB < fD and fA < fC < fD): continue
      
                    # yield scenario (matches, scores, final2, goals for/against)
                    yield (ms3, ss3, (f1, f2), ((fA, aA), (fB, aB), (fC, aC), (fD, aD)))
      
      
      # look for viable scenarios
      for (ms, ss, fs, gfa) in generate():
        # output scenarios as we find them
        output_matches(ms, ss, end=None)
        printf("final games = {fs}", fs=join(fs, sep=", "))
        printf("goals for/against = {gfa}", gfa=map2str(zip(teams, gfa), arr=": ", enc=""))
        BCs = ss['BC']
        # answer is the scores in the B vs C matches
        printf("-> B vs C = {BCs}", BCs=join(BCs, sep=" "))
        printf()
      

      Solution: The scores in the B vs. C games were: 4-1 and 1-1.

      And the B vs C 1-1 draw was one of the two final games.


      There are two possible scenarios. Here is one of them:

      A vs B: 3-0, 2-0
      A vs C: 0-0, 0-4
      A vs D: 1-0, (2-2)
      B vs C: 4-1, (1-1)
      B vs D: 3-1, 2-1
      C vs D: 3-2, 0-5

      (with the two final draws indicated in brackets).

      Before the final 2 games (A vs D, B vs C) are the points are:

      A: 10 points
      B: 9 points
      C: 7 points
      D: 3 points

      If B wins their final game then they will have 12 points, and top the table (providing A does not win their final game).

      If C wins their final game then they will have 10 points, and if A loses their final game they will also have 10 points, so the top position comes down to the result of the A vs C games. And these are a draw and a win for C, so C comes out on top.

      A is guaranteed to finish higher than D, and unless C wins (and A loses) their final game also higher than C. But if C does win, then B must lose, and A will finish higher than B. So A is guaranteed to finish in the top 2 teams (and qualify for the next round).

      When the final 2 games are played they are both drawn, and the results are:

      A: 11 points; goals = 8 for, 6 against
      B: 10 points; goals = 10 for, 9 against
      C: 8 points; goals = 9 for, 12 against
      D: 4 points; goals = 11 for, 11 against

      So A and B go through to the next round.

      The other scenario plays out similarly, but the games won 3-1 and 3-2 are swapped with each other. Although this second scenario could be eliminated if the puzzle text stated that D were the only team with the same number of goals for as goals against. (As B ends up with 10 goals for and 10 goals against).

      We can use these 2 scenarios to construct further solutions where the order of the two matches played by pairs of teams are swapped over.

      Like

  • Unknown's avatar

    Jim Randell 9:01 am on 8 November 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2695: Powers behind the thrones 

    From The Sunday Times, 18th May 2014 [link] [link]

    Gold sovereigns were minted in London for most years from the great recoinage in 1817 until Britain left the gold standard in 1917. I have a collection of eight sovereigns from different years during that period, the most recent being an Edward VII sovereign (he reigned from 1902 until 1910). I noticed that the year on one of the coins is a perfect square and this set me thinking about other powers. Surprisingly, it turns out that the product of the eight years on my coins is a perfect cube.

    What (in increasing order) are those eight years?

    [teaser2695]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 8 November 2022 Permalink | Reply

      The values were are interested in lie in the interval [1817, 1910].

      And the only square in that range is 1849 (= 43²), so that must be one of the 8 values.

      And the largest value is in the interval [1902, 1910].

      This Python program finds the prime factors for numbers in the required range, and then constructs sets of 8 values whose prime factors, when combined, all appear a multiple of 3 times, and so the product of the values is a cube.

      It runs in 164ms.

      Run: [ @replit ]

      from enigma import (irange, prime_factor, multiset, delete, append, printf)
      
      # find values whose product is a cube
      # k = remaining values for find (int)
      # vs = values so far (list)
      # fs = prime factors so far (multiset)
      # d = map of value -> prime factors (multiset)
      # ks = candidate keys in d (list)
      def solve(n, vs, fs, d, ks):
        if n == 0:
          # check all prime exponents are multiples of 3
          if all(x % 3 == 0 for x in fs.values()):
            yield vs
        elif not len(ks) < n:
          # add in a new candidate
          for (i, k) in enumerate(ks):
            # and solve for the remaining candidates
            yield from solve(n - 1, append(vs, k), fs.combine(d[k]), d, ks[i + 1:])
      
      # collect candidate numbers and their prime factors (as a multiset)
      d = dict((n, multiset.from_pairs(prime_factor(n))) for n in irange(1817, 1910))
      
      # find factors that appear fewer than 3 times in total
      while True:
        # count the factors (by combining values from each of the numbers)
        m = multiset.from_dict(*d.values())
        fs = set(k for (k, v) in m.items() if v < 3)
        if not fs: break
        # and remove any numbers which involve any of these factors
        d = delete(d, (k for (k, v) in d.items() if any(p in fs for p in v)))
      
      # 1849 is one of the values in the set
      (vs1, fs1) = ([1849], d[1849])
      
      # and the max value is between 1902 and 1910
      for v in irange(1902, 1910):
        if v not in d: continue
        vs2 = append(vs1, v)
        fs2 = fs1.union(d[v])
      
        # solve for the remaining 6 values
        ks = sorted(k for k in d.keys() if k < v and k not in vs2)
        for vs in solve(6, vs2, fs2, d, ks):
          # output solution
          printf("years = {vs}", vs=sorted(vs))
      

      Solution: The eight years are: 1820, 1836, 1849, 1859, 1870, 1890, 1892, 1904.

      So we have:

      numbers = (1820, 1836, 1849, 1859, 1870, 1890, 1892, 1904)
      factors = (2^12, 3^6, 5^3, 7^3, 11^3, 13^3, 17^3, 43^3)
      factors = (2^4, 3^2, 5, 7, 11, 13, 17, 43)^3

      And the product of the numbers is: (2^4, 3^2, 5, 7, 11, 13, 17, 43)^3 = 526846320^3.

      Like

    • Hugh Casement's avatar

      Hugh Casement 8:31 am on 10 November 2022 Permalink | Reply

      We can get part of the way by analysis.  We need another factor 43, so 1892 must be one of the years.
      The only integer in the range 1902 to 1910 with a small enough largest prime factor to allow us to find two more is 1904 = 2^4 × 7 × 17.
      After that I think trial and error (i.e. by computer) is needed.

      Like

      • Jim Randell's avatar

        Jim Randell 11:03 am on 10 November 2022 Permalink | Reply

        @Hugh: My manual solution proceeded as follows:

        Armed with the prime factorisations of the numbers between 1817 and 1910 (which I did not compute manually), we quickly find 1849, 1892, 1904 are on the list.

        Then we need more factors of 17, and there are only 2 candidates: 1836 and 1870. Both of which must be included. So we now have 5 of the 8 numbers on the list.

        We can eliminate numbers with a factor of 29 (1827, 1856, 1885).

        Considering numbers with factors of 13 (1820, 1859, 1872), if any of these are included it must be 1859 and just one of the others.

        Adding 1859 and 1820 to the list leaves factors of (2, 5, 7) to find, and the only candidate is 1890, and this does indeed give a viable list.

        And I didn’t look for further solutions.

        Like

  • Unknown's avatar

    Jim Randell 8:50 am on 6 September 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2752: Best before 

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

    Peter likes to note “pandigital” times, such as 15.46, 29/03/78, that use all ten digits. Here the five individual numbers (15, 46, 29, 3 and 78) have a product that is divisible by the perfect square 36, and they also have a sum that is two more than a perfect square.

    He has been watching for pandigital times ever since and remembers one where the product of the five numbers was not divisible by any perfect square (apart from 1): this has never happened since! He is also looking out for a pandigital time where the sum of the five numbers is a perfect square:

    (a) When was that last “non-square” pandigital time?
    (b) When will that first “square-sum” pandigital time be?

    [teaser2752]

     
    • Jim Randell's avatar

      Jim Randell 8:51 am on 6 September 2022 Permalink | Reply

      We generate possible pandigital times, and then check to find the last (before the puzzle was set) with a square-free product, and the first (after the puzzle was set) with a perfect square sum.

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

      Run: [ @replit ]

      from datetime import datetime
      from enigma import (
        irange, choose, implies, nconcat, catch,
        multiply, is_square_free, is_square, printf
      )
      
      # find pandigital (y, m, d, H, M) values
      def generate():
        # possible digits
        digits = set(irange(0, 9))
      
        # selection functions
        fns = [
          # month first digit; is 0-1
          lambda m1: m1 < 2,
          # hour first digit; is 0-2
          lambda m1, H1: H1 < 3,
          # day first digit; is 0-3
          lambda m1, H1, d1: d1 < 4,
          # minute first digit; is 0-5
          lambda m1, H1, d1, M1: M1 < 6,
          # month second digit; m2 = 1 -> 0, 2
          lambda m1, H1, d1, M1, m2: implies(m1 == 1, m2 < 3),
          # hour second digit; H1 = 2 -> 0, 1, 2
          lambda m1, H1, d1, M1, m2, H2: implies(H1 == 2, H2 < 3),
          # day second digit; d1 = 3 -> 0, 1
          lambda m1, H1, d1, M1, m2, H2, d2: implies(d1 == 3, d2 < 2),
          # remaining digits (M2, y1, y2) = any
          None, None, None
        ]
      
        # assign digits
        for (m1, H1, d1, M1, m2, H2, d2, M2, y1, y2) in choose(digits, fns, distinct=1):
          (y, m, d, H, M) = (nconcat(*x) for x in [(y1, y2), (m1, m2), (d1, d2), (H1, H2), (M1, M2)])
          y += (2000 if y < 78 else 1900)
          t = catch(datetime, y, m, d, H, M)
          if t is not None:
            yield (t, (H, M, d, m, y % 100))
      
      # date of the puzzle
      now = datetime(2015, 6, 21)
      
      # record answers
      a = b = None
      
      # look for pandigital times
      for (t, ns) in generate():
        # (a) latest time before now with a square-free product
        if t < now and (a is None or t > a) and is_square_free(multiply(ns)):
          a = t
        # (b) earliest time after now with a perfect square sum
        if t > now and (b is None or t < b) and is_square(sum(ns)):
          b = t
      
      # output solution
      fmt = lambda t: t.strftime("%H:%M, %d/%m/%y")
      printf("(a) {a}", a=fmt(a))
      printf("(b) {b}", b=fmt(b))
      

      Solution: (a) 15:43, 26/07/89; (b) 18:59, 27/06/34.

      Like

    • Frits's avatar

      Frits 6:06 pm on 6 September 2022 Permalink | Reply

      Two programs:

         
      from itertools import permutations
      
      # small numbers which are square free
      nums = [n for n in range(1, 32) if n % 11 and all(n % y for y in [4, 9, 25])]
      days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      mx_dt = 0
      
      # check day/month/hour permutations
      for d, m, h in permutations(nums, 3):
        if m > 12 or h > 24: continue
        
        s = "".join(str(x).zfill(2) for x in (d, m, h))
        if len(set(s)) != 6: continue          # different digits
        
        p1 = d * m * h                         # product
        # not divisible by any perfect square
        if not all(p1 % (n * n) 
          for n in [2] + list(range(3, int(p1**.5) + 1, 2))): continue
        
        # check day number
        if d > 28 and m == 2:
          if year % 4 == 0 and (year % 100 or year % 400 == 0):
            days[1] = 29
          else:
            days[1] = 28
        if d > days[m - 1]:  continue  
        
        rest = [int(x) for x in range(9, -1, -1) if str(x) not in s]
        mdh = str(m).zfill(2) + str(d).zfill(2) + str(h).zfill(2)
        
        # check year/minutes permutations
        for p in permutations(rest):
          if p[2] > 5: continue                # minutes < 60
          
          year = 10 * p[0] + p[1]
          if 15 < year < 78: continue          # year between 1978 and 2015
          mins = 10 * p[2] + p[3]
          
          # built date string
          dt = int(("19" if year > 77 else "20") + str(year) +  mdh + str(mins))
                   
          if dt < mx_dt: continue              # skip if earlier year than maximum
          
          # not divisible by any perfect square for new numbers ...
          p2 = mins * year
          if not all(p2 % (n * n) 
                     for n in [2] + list(range(3, int(p2**.5) + 1, 2))): continue
          # ... and for all five 2-digit numbers 
          p2 *= p1 
          if not all(p2 % (n * n) 
                     for n in [2] + list(range(3, int(p2**.5) + 1, 2))): continue
         
          mx_dt = dt                           # new maximum
          break                                # as p is decreasing
          
      s = str(mx_dt) 
      print(f"last 'non-square' pandigital time: "
            f"{s[6:8]}/{s[4:6]}/{s[:4]} {s[8:10]}:{s[10:]}") 
      

      and

        
      days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      
      # pick one value from each entry of a <k>-dimensional list <ns>
      # so that all digits in the <k> values are different
      def pickOneFromEach(k, ns, s=[], dgts=set()):
        if k == 0:
          yield s
        else:
          for n in ns[k - 1]:
            sn = str(n).zfill(2)
            if all(x not in dgts for x in sn):
              yield from pickOneFromEach(k - 1, ns, s + [sn], dgts | set(sn))
      
      # months, days and hours have to use the digits 0, 1 and 2 
      # months 10 and 12 are invalid as they use two of the digits 0, 1 and 2
      # and leave no room for day and hour so month < 10
      ys = list(n for n in range(34, 100) 
                if n % 11 and all(y not in str(n) for y in "012"))
      ms = list(range(1, 10))
      ds = list(n for n in range(13, 32) if n % 11 and n % 10)
      hs = list(n for n in range(24) if n % 11 and n % 10)
      mis = list(n for n in range(34, 60) 
                 if n % 11 and all(y not in str(n) for y in "012"))
      
      rev_lst = [mis, hs, ds, ms, ys]
      
      for p in pickOneFromEach(5, rev_lst):
        s = "".join(p)
        # sum has to be a perfect square
        if sum([int(x) for x in p]) not in {144, 169, 196}: continue
        
        # check day number
        m, d = (int(p[1]), int(p[2]))
        if d > 28 and m == 2:
          if year % 4 == 0 and (year % 100 or year % 400 == 0):
            days[1] = 29
          else:
            days[1] = 28
        if d > days[m - 1]:  continue  
        
        print(f" first 'square-sum' pandigital time: "
              f"{p[2]}/{p[1]}/{p[0]} {p[3]}:{p[4]}")  
        break       # we have found the first date 
      

      Like

  • Unknown's avatar

    Jim Randell 9:31 am on 30 August 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2743: Line-up 

    From The Sunday Times, 19th April 2015 [link] [link]

    I have arranged the numbers from 1 to 27 in a 3-by-3-by-3 cubical array. I have noticed that the nine numbers making up one of the faces of the cube are all primes.

    Also, I have searched through the array and written down the sum of any three numbers that are in a straight line. I have then calculated the grand total of all those line-sums. It turns out that the grand total is itself a perfect cube!

    What is that grand total?

    [teaser2743]

     
    • Jim Randell's avatar

      Jim Randell 9:32 am on 30 August 2022 Permalink | Reply

      Considering a standard 3×3×3 Rubik’s cube, we can think of is as consisting of 27 cubelets:

      8 corner pieces (with 3 colours)
      12 edge pieces (with 2 colours)
      6 middle pieces (with 1 colour)
      1 core piece (with 0 colours)

      And if we consider the number of lines each type of cubelet appears in we have:

      corner = 7 lines
      edge = 4 lines
      middle = 5 lines
      core = 13 lines

      The grand total then consists of:

      7× (8 corner pieces)
      4× (12 edge pieces)
      5× (6 centre pieces )
      13× (1 core piece)

      And as long as we distribute the numbers amongst the same type of piece any arrangement will have the same grand total. (Although to confirm with the layout specified in the puzzle you have to keep the primes all together on one face).

      There are only nine primes between 1 and 27 (= 2, 3, 5, 7, 11, 13, 17, 19, 23) and these are arranged on one of the faces, so we have 4 corners, 4 edges, and 1 centre that are prime.

      So, we just need to split the primes into sets of size (4, 4, 1) with sums (p1, p2, p3), and the remaining 18 non-primes into sets of size (4, 8, 5, 1) with sums (n1, n2, n3, n4), such that:

      T = 7(p1 + n1) + 4(p2 + n2) + 5(p3 + n3) + 13 n4 [= a perfect cube]

      This can be written as:

      T = 4(p1 + p2 + p3 + n1 + n2 + n3 + n4) + 3(p1 + n1) + (p3 + n3) + 9 n4

      and we know that: (p1 + p2 + p3 + n1 + n2 + n3 + n4) = tri(27), so:

      T = 4 tri(27) + 3(p1 + n1) + (p3 + n3) + 9 n4

      So the maximum possible value for T is 2363, giving a maximum possible cube of 13³ = 2197.

      And the minimum possible value for T is 1731, giving a minimum possible cube of 13³ = 2197.

      So, the only possibility for a grand total that is a cube is 13³ = 2197.

      Solution: The grand total is 2197.


      But now we need to show there is at least one solution.

      If we do find a solution we can permute the numbers amongst their own groups without changing the grand total to give a class with (4! × 4! × 1!) × (4! × 8! × 5! × 1!) = 66886041600 solutions. (Which we can divide by 8 to account for reflections/rotations).

      This Python program chooses a non-prime core value, and partitions the primes into sets of size (4, 1) and the remaining non-primes into sets of size (4, 5), and in each case the contribution to the grand total is recorded. We then look for pairs of contributions that can make a grand total that is a perfect cube. (There are many of these, so we stop when we have found a single example for a particular cube).

      It runs in 13s and finds there are many possible core values that lead to viable solutions (and each of these has many partitions sums that lead to many solutions).

      Run: [ @replit ]

      from enigma import (irange, primes, subsets, intc, intf, cbrt, cb, printf)
      
      # partition set <s> into subsets with sizes in <ns>
      def partition(s, ns, ss=[]):
        if not ns:
          yield ss
        elif len(ns) == 1:
          for x in subsets(s, size=ns[0]):
            yield ss + [x]
        else:
          for x in subsets(s, size=ns[0]):
            yield from partition(s.difference(x), ns[1:], ss + [x])
      
      # primes
      ps = set(primes.between(1, 27))
      
      # non-primes
      nps = set(irange(1, 27)).difference(ps)
      
      # total of primes and non-primes (= tri(27))
      t = sum(ps) + sum(nps)
      
      # record grand totals
      Ts = set()
      
      # split the primes into sets of size (4, 1) and record the contribution to the total
      pss = set(3 * sum(p1) + sum(p3) for (p1, p3) in partition(ps, (4, 1)))
      printf("[{n} prime partitions]", n=len(pss))
      
      # choose the core value (non-prime)
      for v in nps:
        printf("[core={v}]")
      
        # split the remaining non-primes into sets of size (4, 5) and record the contribution to the total
        nss = set(3 * sum(n1) + sum(n3) for (n1, n3) in partition(nps.difference({v}), (4, 5)))
        printf("[{n} non-prime partitions]", n=len(nss))
      
        # find possible cubes
        t0 = 4 * t + 9 * v
        cubes = set(cb(x) for x in irange(intc(cbrt(t0 + min(pss) + min(nss))), intf(cbrt(t0 + max(pss) + max(nss)))))
        printf("[possible cubes = {cubes}]", cubes=sorted(cubes))
      
        # find pairs from pss and nss that give a cube grand total
        for T in cubes:
          for p in pss:
            n = T - t0 - p
            if n > 0 and n in nss:
              printf("[p={p} n={n} -> T={T}]")
              Ts.add(T)
              break  # we only need one example for this T
      
      # output solution
      printf("grand total = {Ts}")
      

      There are many, many ways to place the numbers on the cube.

      Here is one (this has 8 as the core value – the smallest possible, but it can be any non-prime value between 8 and 27):

      In this solution the different types of cubelet are (prime + non-prime values):

      8 corners = (7, 17, 19, 23) + (24, 25, 26, 27); sum = 168
      12 edges = (2, 3, 5, 11) + (1, 4, 6, 9, 10, 12, 14, 16); sum = 93
      6 middles = (13) + (15, 18, 20, 21, 22); sum = 109
      1 core = () + (8); sum = 8

      And so the grand total is:

      7×168 + 4×93 + 5×109 + 13×8 = 2197 = 13³

      Like

  • Unknown's avatar

    Jim Randell 9:07 am on 18 August 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2729: Factorial fact 

    From The Sunday Times, 11th January 2015 [link] [link]

    The “factorials” of numbers (denoted by !) are defined by:

    1! = 1
    2! = 2 × 1
    3! = 3 × 2 × 1
    4! = 4 × 3 × 2 × 1
    etc.

    It is possible to take eleven of the twelve factorials 1!, 2!, 3!, 4!, 5!, 6!, 7!, 8!, 9!, 10!, 11!, 12! and to split them into groups of three, four and four, so that in each group the product of the factorials in that group is a perfect square.

    What are the factorials in the group whose product is the smallest?

    [teaser2729]

     
    • Jim Randell's avatar

      Jim Randell 9:07 am on 18 August 2022 Permalink | Reply

      This Python program runs in 56ms. (Internal run time is 1.68ms).

      Run: [ @replit ]

      from enigma import (
        irange, subsets, factorial, multiply, is_square_p,
        seq_all_different as all_different, printf
      )
      
      # measure function
      fn = lambda ns: multiply(factorial(n) for n in ns)
      
      # find groups of k-tuples with a measure that is a square
      def generate(k):
        for ns in subsets(irange(1, 12), size=k):
          if is_square_p(fn(ns)):
            yield ns
      
      # collect 3-, 4-tuples
      n3s = list(generate(3))
      n4s = list(generate(4))
      
      # choose two disjoint 4-tuples
      for (g1, g2) in subsets(n4s, size=2):
        if not all_different(g1 + g2): continue
        # and find a disjoint 3-tuple
        for g3 in n3s:
          if not all_different(g1 + g2 + g3): continue
          # find the group with the minimal measure
          g = min(g1, g2, g3, key=fn)
          printf("min = {g} [{g3} {g1} {g2}]")
      

      Solution: The factorials in the group with the smallest product are: 1!, 8!, 9!.

      There are 2 ways to form the groups:

      (1, 8, 9) (2, 3, 11, 12) (4, 5, 7, 10)
      (1, 8, 9) (2, 4, 11, 12) (3, 5, 7, 10)

      Like

    • GeoffR's avatar

      GeoffR 2:38 pm on 18 August 2022 Permalink | Reply

      A brute force solution produced products of 11, 14 and 18 digits long for groups of 3, 4 and 4 factorials. The solution ran in approximately 20 sec.

      The smallest solution was repeated if the break statement is excluded.

      from itertools import permutations
      from enigma import factorial as fact
      from enigma import is_square as is_sq
      
      # Precalculate the twelve factorials
      F1,F2, F3 = fact(1), fact(2), fact(3)
      F4, F5, F6 = fact(4), fact(5), fact(6)
      F7, F8, F9 = fact(7), fact(8), fact(9)
      F10, F11, F12 = fact(10), fact(11), fact(12)
      
      # Factorial dictionary for output
      ND = {F1: 'F1', F2:'F2', F3:'F3', F4:'F4', F5:'F5',F6:'F6',
            F7:'F7', F8:'F8', F9:'F9', F10:'F10', F11:'F11', F12:'F12' }
            
      for p1 in permutations((F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12),11):
        a, b, c, d, e, f, g, h, i, j, k = p1
        # split factorials into groups of three, four and four
        if not (is_sq(a * b * c)): continue
        if not (is_sq(d * e * f * g)): continue
        if not( is_sq(h * i * j * k)):continue
        # sort factorial products
        T = sorted((a * b * c , d * e * f * g, h * i * j * k))
        if a * b * c < d * e * f * g and a * b * c < h * i * j * k:
          print(f"Factorials in smallest product are {ND[a]}, {ND[b]} and {ND[c]}.")
          print(f"Smallest factorial product = {a * b * c}")
          break
      
      # Factorials in smallest product are F1, F8 and F9.
      # Smallest factorial product = 14631321600
      
      
      
      

      Like

      • GeoffR's avatar

        GeoffR 9:17 am on 19 August 2022 Permalink | Reply

        A three stage permutation seemed more compatible with a (3,4,4) group of numbers. This solution was considerably faster than my previous posting, running in 4.64ms.

        from itertools import permutations
        from enigma import factorial as fact, is_square as is_sq, timer
        
        timer.start()
        # Precalculate the twelve factorials
        F1,F2, F3 = fact(1), fact(2), fact(3)
        F4, F5, F6 = fact(4), fact(5), fact(6)
        F7, F8, F9 = fact(7), fact(8), fact(9)
        F10, F11, F12 = fact(10), fact(11), fact(12)
        
        factorials = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12} 
        
        # Factorial dictionary for output
        ND = {F1: 'F1', F2:'F2', F3:'F3', F4:'F4', F5:'F5',F6:'F6',
              F7:'F7', F8:'F8', F9:'F9', F10:'F10', F11:'F11', F12:'F12' }
        
        # first stage permutation
        # split factorials into groups of three, four and four
        for p1 in permutations(factorials, 3):
          a, b, c = p1
          if not (is_sq(a * b * c)):
            continue
        
          # second stage permutation
          q1 = factorials.difference({a, b, c})
          for p2 in permutations(q1, 4):
            d, e, f, g = p2
            if not (is_sq(d * e * f * g)):
              continue
        
            # third stage permutation
            q2 = q1.difference({d, e, f, g})
            for p3 in permutations(q2, 4):
              h, i, j, k = p3
              if not( is_sq(h * i * j * k)):
                continue
              
              # sort factorial products
              T = sorted((a * b * c , d * e * f * g, h * i * j * k))
              if (a * b * c) < (d * e * f * g) and (a * b * c) < (h * i * j * k):
                print(f"Factorials in smallest product are {ND[a]}, {ND[b]} and {ND[c]}.")
                print(f"Smallest factorial product = {a * b * c}")
                timer.stop()
                timer.report()
                # only one solution needed
                exit()
        
        # Factorials in smallest product are F8, F1 and F9.
        # Smallest factorial product = 14631321600
        # [timing] total time: 0.0046427s (4.64ms)
        
        
        
        
        

        Like

    • NigelR's avatar

      NigelR 11:33 pm on 18 August 2022 Permalink | Reply

      [timing] total time: 0.0030880s (3.09ms)

      from itertools import combinations as comb
      from enigma import timer
      timer.start()
      
      def fac(num): #returns num!
          return num*fac(num-1) if num > 1 else 1
      
      def is_square(num): #returns True if num is square
              return round(num ** 0.5) ** 2 == num
      
      def lprod(lst) : #returns product of elements in list
          res = 1
          for x in lst:
               res = res * x
          return res
      
      facs = {i:fac(i) for i in range(1,13)}
      # identify group of i numbers between 1 and 12 where product of their factorial is a square
      group = lambda i:[x for x in comb(facs.keys(),i) if is_square(lprod([facs[y] for y in x]))]
      threes,fours = group(3),group(4)
      #identify group of 3 and two groups of 4 where all 11 elements are different
      valid = [[x,y[0],y[1]] for x in threes for y in comb(fours,2) if len(set(list(x)+[z for sub in y for z in sub]))==11]
      valid = [y for x in valid for y in x] # flatten valid
      prods =[lprod(list(x)) for x in valid] # generate products of factorials in valid group
      res=valid[prods.index(min(prods))] #identify factorials with minimum product
      print('Factorials in group with smallest product are:',*res)
      timer.stop()
      timer.report()
      

      Factorials in group with smallest product are: 1 8 9

      Like

  • Unknown's avatar

    Jim Randell 3:16 pm on 29 July 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 3123: A six-pipe problem 

    From The Sunday Times, 31st July 2022 [link] [link]

     

    A factory makes six types of cylindrical pipe, A to F in decreasing size, whose diameters in centimetres are whole numbers, with type A 50 per cent wider than type B. The pipes are stacked in the yard as a touching row of As with an alternating row of touching Bs and Cs in the next layer, with each B touching two As. Type Ds fill the gap between the As and the ground; Es fill the gap between As and the Bs; and Fs fill the gap between As, Ds and the ground. Finally another row of As is put on top of the stack, giving a height of less than 5 metres.

    What is the final height of the stack in centimetres?

    [teaser3123]

     
    • Jim Randell's avatar

      Jim Randell 3:55 pm on 29 July 2022 Permalink | Reply

      The title of this puzzle is presumably an allusion to Sherlock Holmes’ “three pipe problem” in The Red-Headed League.

      Some judicious applications of Pythogoras’ theorem gets us the answer.

      This Python program runs in 54ms. (Internal runtime is 313µs).

      Run: [ @replit ]

      from enigma import (irange, div, is_square, sq, printf)
      
      # suppose the pipes have diameters a, b, c, d, e, f (in cm)
      # we know h = 2a + c < 500; b = (2/3)a
      for a in irange(6, 249, step=3):
        b = div(2 * a, 3)
        # from pipes ABC
        c = a - b
        # calculate the height of the stack (in centimetres)
        h = 2 * a + c
        if not (h < 500): break
        # from pipes AAD: (a + d)^2 = a^2 + (a - d)^2
        d = div(a, 4)
        if d is None: continue
        # from pipes ABC: (a + e)^2 = (a + c - b - e)^2 + (b + c)^2
        e = div(sq(b) + sq(c) - a * (b - c), 2 * a - b + c)
        if e is None: continue
        # from pipes ADF: (d + f)^2 = (d - f)^2 + x^2; (a + f)^2 = (a - f)^2 + y^2; x + y = a
        r = is_square(a * d)
        if r is None: continue
        f = div(sq(a), 4 * (a + d + 2 * r))
        if f is None: continue
        if a > b > c > d > e > f > 0:
          # output solution (in cm)
          printf("height = {h} cm [a={a} b={b} c={c} d={d} e={e} f={f}]")
      

      Solution: The height of the stack is 420 cm.

      Note that the derivation of d requires a to also be divisible by 4 (as well as 3), so we could consider just multiples of 12 for a for a slight increase in speed.


      Manually:

      If we suppose a = 180k, then we can simplify the expressions used in the program to give the following values:

      a = 180k
      b = 120k
      c = 60k
      d = 45k
      e = 24k
      f = 20k

      And we require all these values to be positive integers, so k takes on a positive integer value.

      The total height of the stack is h = 2a + c = 420k, and we require h < 500.

      So the only permissible value is k = 1, and the answer follows directly.

      Like

    • Frits's avatar

      Frits 2:26 pm on 2 August 2022 Permalink | Reply

      Problem is when to stop with your analysis.
      Ultimately each diameter can be expressed in terms of the smallest diameter.

         
      from enigma import SubstitutedExpression
      
      # AGH, BIJ, CK, DL, EM and FN variables are diameters
      # if (x - y)^2 = z^2 then (2x - 2y)^2 = (2z)^2
      # so we can also calculate with diameters in the Pythagorean formulae
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # type A 50 per cent wider than type B
          # diameter A is equal to diameter C + two times radius B
          # a height of less than 5 metres so 2a + c = 7c < 500
          "1 < CK < 72",
          "3 * CK = AGH",
          "2 * CK = BIJ",
          
          # (a + d)^2 = a^2 + (a - d)^2 so 4ad = a^2
          "div(AGH, 4) = DL",
          
          # (a + e)^2 = (a + c - b - e)^2 + (b + c)^2 using a = 3c and b = 2c
          # 6ce + 9c^2 = 4c^2 - 4ce + 9c^2
          "div(2 * CK, 5) = EM",
          
          # (d + f)^2 = (d - f)^2 + x^2; (a + f)^2 = (a - f)^2 + (a - x)^2;
          # so 4df = x^2 and 4af = (a - x)^2
          "4.0 * AGH * FN == (AGH - 2 * (DL * FN)**.5)**2",
          
          # A to F in decreasing size
          "AGH > BIJ > CK > DL > EM > FN"
        ],
        answer="2 * AGH + CK, (AGH, BIJ, CK, DL, EM, FN)",
        d2i=dict([(k, "CF") for k in range(8, 10)]),
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"the final height of the stack in centimetres: {ans[0]}")
      

      Like

  • Unknown's avatar

    Jim Randell 6:31 am on 19 July 2022 Permalink | Reply
    Tags: by: Nick MacKinnon   

    Teaser 2721: Milliner’s hat-trick 

    From The Sunday Times, 16th November 2014 [link] [link]

    A football tournament has four groups each of four teams, with the teams in the same group playing each other once. So far the teams have each played two games and in each group the distribution of points is different. Also, in each group just one pair of teams are level on points and their positions have been determined by their “goals scored”. Milliner has scored four (including a hat-trick), Orlando two, and nine other players have scored one goal each. Despite his success, Orlando’s team is not the top of their group.

    What are the results of the two games that Milliner’s team have played? And the two games that Orlando’s team have played?

    [teaser2721]

     
    • Jim Randell's avatar

      Jim Randell 6:33 am on 19 July 2022 Permalink | Reply

      The puzzle is not explicit on the scoring system used (i.e. 2 points for a win, or 3 points for a win), although it turns out it doesn’t matter which is used, the answer to the puzzle is the same in both cases.

      In each group each team has played 2 of their 3 matches. So there have been 4 matches played (and there are 2 matches left to play). So among the 4 groups there have been 16 matches played in total.

      The minimum possible number of goals scored among the 4 matches in a group is 2. So the maximum possible number of goals scored is 15 − 3×2 = 9. (Although it may be possible to reduce this theoretical maximum value, which would speed up the program).

      For each group, this Python program considers the scores for teams A (1st), B (2nd), C (3rd), D (4th), such that each team has played 2 matches, and two of the teams are tied on points, but have different “goals for” values.

      It then chooses 4 different points distributions (one for each of the groups), and from the previously examined scores it chooses 4 sets of scores that have 15 goals scored in total, and ensures there are possible teams for both Milliner and Orlando.

      The program runs in 361ms.

      Run: [ @replit ]

      from collections import defaultdict
      from enigma import (
        Football, irange, subsets, tuples, cproduct, peek, delete,
        update, find, unzip, reverse, map2str, join, printf
      )
      
      # scoring regime (2 or 3 points for a win)
      football = Football(games='wdlx', points=dict(w=2, d=1))
      
      # labels for the teams
      teams = (A, B, C, D) = tuple("ABCD")
      
      # keys for the matches
      ks = list(x + y for (x, y) in subsets(teams, size=2))
      
      # allocate <t> goals among the matches <ms>, return scores <ss>
      def allocate(t, ms, ss=dict()):
        # are we done?
        if not ms:
          if t == 0:
            yield ss
        else:
          # calculate the number of surplus goals
          n = t - sum(v == 'w' for v in ms.values())
          if n < 0: return
          # choose the next match to allocate goals to
          (k, v) = peek(ms.items())
          if v == 'x':
            # not played
            yield from allocate(t, delete(ms, [k]), update(ss, [(k, None)]))
          elif v == 'w' or v == 'l':
            # a win (or loss), calculate scores (x, y)
            for x in irange(1, n + 1):
              for y in irange(0, min(x - 1, n - x + 1)):
                s = ((x, y) if v == 'w' else (y, x))
                yield from allocate(t - x - y, delete(ms, [k]), update(ss, [(k, s)]))
          elif v == 'd':
            # a draw, calculate scores (x, x)
            for x in irange(0, n // 2):
              yield from allocate(t - x - x, delete(ms, [k]), update(ss, [(k, (x, x))]))
      
      # check goals scored by team <t> in a match from scores <ss> is <n> or more
      def scored(t, ss, n):
        for (k, v) in ss.items():
          if v is not None:
            i = find(k, t)
            if i != -1 and v[i] >= n:
              return True
      
      # map: <pts> -> <total goals> -> (<match scores>, <milliner>, <orlando>) values
      d = defaultdict(lambda: defaultdict(list))
      
      # consider possible match outcomes
      for ms in football.games(repeat=len(ks)):
        ms = dict(zip(ks, ms))
        # compute the table for each team
        table = dict((t, football.extract_table(ms, t)) for t in teams)
        # each team has played twice
        if not all(table[t].played == 2 for t in teams): continue
        # points are increasing, with exactly 2 teams having the same points
        pts = tuple(table[t].points for t in teams)
        if len(set(pts)) != 3: continue
        if not all(x >= y for (x, y) in tuples(pts, 2)): continue
        # allocate goals to the matches
        for t in irange(2, 9):
          for ss in allocate(t, ms):
            # extract the goals (for, against) for each team
            goals = dict((t, football.extract_goals(ss, t)) for t in teams)
            # check teams with same points have different "goals for" values
            if any(p1 == p2 and not (goals[t1][0] > goals[t2][0])
              for ((t1, p1), (t2, p2)) in tuples(zip(teams, pts), 2)
            ): continue
            # find teams for Milliner: team must have a match with >= 3 goals scored
            # and have >= 4 goals scored in total
            tM = list(t for (t, (f, _)) in goals.items() if f > 3 and scored(t, ss, 3))
            # find teams for Orlando: must have >= 2 goals, and not be top
            tO = list(t for (t, (f, _)) in goals.items() if t != 'A' and f > 1)
            # record this set of scores
            d[pts][t].append((ss, tM, tO))
      
      # collect scores from <ss> for teams in <ts> (from the team's perspective)
      def collect(ss, ts):
        for t in ts:
          rs = list()
          for (s, f) in unzip(football.extract(ss, t)):
            if s is None: continue
            rs.append(reverse(s) if f else s)
          yield tuple(sorted(rs, reverse=1))
      
      # choose 4 different points distributions
      for ps in subsets(d.keys(), size=4):
        # choose the number of goals scored in each group
        for ts in cproduct(d[p].keys() for p in ps):
          # there should be 15 goals in total
          if sum(ts) != 15: continue
          # choose the actual scores in each match
          for ss in cproduct(d[p][t] for (p, t) in zip(ps, ts)):
            # check for possibilities for Milliner and Orlando
            if not any(tM for (_, tM, _) in ss): continue
            if not any(tO for (_, _, tO) in ss): continue
            # output a solution (groups = scores, pts, M, O; M's team; O's team)
            (Ms, Os) = (set(), set())
            for (i, ((s, tM, tO), p)) in enumerate(zip(ss, ps), start=1):
              printf("Group {i}: {s} {p} {tM} {tO}", s=map2str(s, enc=""), tM=join(tM, enc="[]"), tO=join(tO, enc="[]"))
              Ms.update(collect(s, tM))
              Os.update(collect(s, tO))
            for M in sorted(Ms): printf("M's team: {M}", M=join(M, sep=", "))
            for O in sorted(Os): printf("O's team: {O}", O=join(O, sep=", "))
            printf()
      

      Solution: The results for Milliner’s team are: a 3-1 win, a 1-0 win. The results for Orlando’s team are: a 2-0 win, a 0-1 loss.

      M’s team is a group leader. O’s team is second in their group.

      With “2 points for a win” (and 1 for a draw) there are three possible sets of scores:

      Group 1: A v B = 1-0; A v C = 1-0; B v D = 2-0; C v D = 1-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      Group 1: A v B = 1-0; A v D = 1-0; B v C = 2-0; C v D = 1-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      Group 1: A v C = 1-0; A v D = 1-0; B v C = 0-1; B v D = 2-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      We can examine what happens with “3 points for a win” by changing the initialisation at line 8. And we find in this case there are four possible sets of scores:

      Group 1: A v B = 1-0; A v D = 0-0; B v C = 2-0; C v D = 1-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v B = 1-1; A v D = 1-0; B v C = 0-0; C v D = 0-0; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v B = 1-0; A v D = 0-0; B v C = 2-0; C v D = 1-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v C = 0-0; A v D = 1-0; B v C = 0-0; B v D = 1-1; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v C = 1-0; A v D = 0-0; B v C = 0-1; B v D = 2-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v B = 1-1; A v D = 1-0; B v C = 0-0; C v D = 0-0; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v C = 1-0; A v D = 0-0; B v C = 0-1; B v D = 2-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v C = 0-0; A v D = 1-0; B v C = 0-0; B v D = 1-1; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Like

  • 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

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