Teaser 2845: Baled out
From The Sunday Times, 2nd April 2017 [link] [link]
In my fantasy Euro football championship the four home teams were in the same group, with each team playing each other team once. Group positions were decided on points, then using “goal differences” and then “goals scored” if necessary.
After having played two games each, just three goals had been scored in the group, leading to England being ahead with Northern Ireland second, Scotland third and Wales fourth. Wales realised that they had to score at least three goals in their remaining game in order to have a chance of being group leaders. In the final game of the group, Bale scored a third goal in the dying seconds, resulting in Wales being group winners and knocking England into second place.
What were the scores in the six games (in the order EvN, EvS, EvW, NvS, NvW, SvW)?
[teaser2845]





Jim Randell 9:15 am on 12 May 2022 Permalink |
There are 4 teams, and each team plays each other team once. So there are 6 matches in total, each team playing 3 matches.
When each team has played each other team twice, 4 of the matches have been played, leaving 2 matches remaining.
My first programmed solution was quite long and a bit clunky (and relied on a bit of analysis).
The following program is neater. It considers all possibilities for the 4 played matches, and looks for sets of scorelines that have 3 goals scored in total and place the teams in order (E, N, S, W). It then examines scenarios for the remaining 2 matches, such that W can take 1st place, but must score at least 3 goals in order to do so. Once a viable scenario is found, we look for the actual scenario (where W scores 3 goals in their final match to come 1st, and E finishes in 2nd place). The scenarios are then output (matches and table).
It runs in 244ms.
Run: [ @replit ]
from enigma import (defaultdict, Football, subsets, partitions, irange, inf, diff, peek, update, zip_eq, join, sprintf, printf) # scoring system football = Football(points=dict(w=3, d=1)) # labels for the teams teams = (E, N, S, W) = (0, 1, 2, 3) # keys for the matches matches = list(subsets(teams, size=2)) # generate possible scores for <n> matches with <g> total goals scored def _scores(n, g, ss=[]): if n == 1: for x in irange(0, g): yield ss + [(x, g - x)] else: # choose the number of goals scored in this match for m in irange(0, g): for x in irange(0, m): yield from _scores(n - 1, g - m, ss + [(x, m - x)]) # generate sets of <n> matches with the specified range of goals scored def scores(n, goals=None, min_goals=0, max_goals=inf): if goals is not None: min_goals = max_goals = goals for g in irange(min_goals, max_goals): yield from _scores(n, g) # assign positions to teams, based on scores <ss> def table(ss): # calculate the points, goals for, goals against for each team (pts, gf, ga) = (list(), list(), list()) for t in teams: (ss_, ts_) = football.extract(ss, t) table = football.table(football.outcomes(ss_), ts_) pts.append(table.points) (f, a) = football.goals(ss_, ts_) gf.append(f) ga.append(a) # calculate the positions of the teams (1st, 2nd, 3rd, 4th) pos = sorted(teams, key=(lambda t: (pts[t], gf[t] - ga[t], gf[t])), reverse=1) return (pos, pts, gf, ga) # names for the teams name = "ENSW" # output matches def output_matches(n, ss): fmt = lambda k, v: sprintf("{x}v{y} = {a}-{b}", x=name[k[0]], y=name[k[1]], a=v[0], b=v[1]) printf("{n} {s}", s=join((fmt(k, v) for (k, v) in sorted(ss.items())), sep="; ")) # output the table def output_table(pos, pts, gf, ga): for (i, t) in enumerate(pos, start=1): printf(" {i}: {n} -> {p} pts, f={f} a={a}", n=name[t], p=pts[t], f=gf[t], a=ga[t]) # choose the 2 unplayed matches for unplayed in partitions(teams, 2): played = diff(matches, unplayed) # allocate scores for the 4 played matches, with 3 goals scored for ps in scores(len(played), goals=3): ss1 = dict(zip(played, ps)) # check the first scenario: E > N > S > W (pos1, pts1, gf1, ga1) = table(ss1) if not zip_eq(pos1, [E, N, S, W]): continue # now consider scores for the 2 unplayed matches (say up to 6 goals) # and look for situations where W can take 1st place uW = peek(x for x in unplayed if W in x) Ws = defaultdict(list) for us in scores(len(unplayed), min_goals=0, max_goals=6): ss2 = update(ss1, unplayed, us) # check W can take 1st place (pos2, pts2, gf2, ga2) = table(ss2) if not (pos2[0] == W): continue # record match results by W's goals (_, w) = ss2[uW] Ws[w].append(ss2) if w < 3: break # we can stop if w < 3 # W needs to score at least 3 goals to take 1st place if not (Ws and min(Ws.keys()) == 3): continue # now look for the actual situation, where W scores 3 goals, and E takes 2nd place for ss2 in Ws[3]: # check E is in 2nd place (pos2, pts2, gf2, ga2) = table(ss2) if not (pos2[1] == E): continue # output solution output_matches("[1]", ss1) output_table(pos1, pts1, gf1, ga1) printf("") output_matches("[2]", ss2) output_table(pos2, pts2, gf2, ga2) printf("\n")Solution: The scores were: EvN = 0-0; EvS = 0-1; EvW = 2-0; NvS = 1-0; NvW = 0-3; SvW = 0-0.
At first I wasn’t sure whether we were using a “2 points for a win” or a “3 points for a win” scoring regime. (The puzzle text does not mention how the points are allocated).
In the program I used “3 points for a win, 1 point for a draw”, but the answer is the same if “2 points for a win, 1 point for a draw” is used. This can be changed at line 5.
When the program generates possible scorelines for the final 2 matches it only looks for matches with up to 6 goals scored in total. You can increase this value at line 74 of the program, but it doesn’t affect the answer found.
LikeLike