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 7:43 am on 25 November 2025 Permalink |
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:
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.LikeLike
Frits 4:58 am on 30 November 2025 Permalink |
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))LikeLike
Jim Randell 8:17 am on 30 November 2025 Permalink |
@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).
LikeLike
Frits 3:51 am on 1 December 2025 Permalink |
@Jim,
I found that out myself today (when comparing it to Brian’s output).
Please use:
LikeLike