Teaser 3324: Prime tournament
From The Sunday Times, 7th June 2026 [link]
Our squash club recently organised a round-robin tournament (i.e., each participant played each of the others once), with each game resulting in a win for one of the players. An even number of players, fewer than 28, took part. At the end of the tournament a player’s score was the number of games he or she had won.
It turned out that each player’s score was a prime number, and each prime number less than the number of players occurred as somebody’s score. Furthermore each such score was achieved by a prime number of players! The players who tied bottom shared equally the £10 booby prize.
How many players were there, and what was the most common score?
[teaser3324]





Jim Randell 9:23 am on 7 June 2026 Permalink |
(See also: Enigma 1045).
I split the puzzle into two parts.
The first finds candidate score distributions:
from enigma import (irange, primes, C, express, flatten, rev, run, printf) # consider the number of players = n for n in irange(2, 26, step=2): # find primes less than n ps = list(primes.between(2, n - 1)) if not ps: continue # count the number of matches in the tournament # this is the total number of points allocated t = C(n, 2) printf("n={n}: ps={ps} t={t}") # express the points total using the prime numbers # each prime total must appear a prime number of times for qs in express(t, ps, qs=ps): # must be totals for n players if not (sum(qs) == n): continue # and the number of players with the lowest points must divide into 1000 if not (1000 % qs[0] == 0): continue # generate the list of totals ts = rev(flatten([p] * q for (p, q) in zip(ps, qs))) assert sum(ts) == t printf("-> {ts}") # check to see if this list of totals is viable run("teaser3324mzn.py", *ts)And the second part (which is called from the first program) uses a MiniZinc model to check if the given score distribution is viable:
from enigma import (multiset, sprintf, args, printf) from minizinc import MiniZinc # collect required points pts = args(None, 0, int) n = len(pts) # construct a MiniZinc model to make the table model = sprintf(""" % indices for the players set of int: T = 1..{n}; % points to allocate array[T] of int: points = {pts}; % wins for A against B array [T, T] of var {{0, 1}}: x; % no player plays themselves constraint forall (i in T) (x[i, i] = 0); % symmetry constraints constraint forall (i, j in T where i < j) (x[i, j] + x[j, i] = 1); % points for each player constraint forall (i in T) (points[i] = sum (j in T where j != i) (x[i, j])); solve satisfy; """) # solve the model for [table] in MiniZinc(model).solve(result='x'): # output the table printf() for row in table: printf("{row} -> {t}", t=sum(row)) printf() # output solution m = multiset.from_seq(pts) for (s, k) in m.multiplicity(max(m.values())): printf("num players = {n}; most common score = {s} (appears {k} times)") printf() # one example is enough breakIt turns out only one of the candidate distributions of points allows a table to be constructed, and this gives the answer.
Solution: [To Be Revealed]
LikeLike
Jim Randell 12:47 pm on 7 June 2026 Permalink |
Instead of using MiniZinc to look for a viable table we can use Landau’s Theorem [@wikipedia] to check for a valid sequence of scores (although it does not provide us with an example table).
The following Python program runs in 266ms. (Internal runtime is 180ms).
from enigma import (irange, primes, C, express, flatten, csum, multiset, printf) # consider the number of players = n for n in irange(2, 26, step=2): # find primes less than n ps = list(primes.between(2, n - 1)) if not ps: continue # count the number of matches in the tournament # this is the total number of points allocated t = C(n, 2) # express the points total using the prime numbers # each prime total must appear a prime number of times for qs in express(t, ps, qs=ps): # must be totals for n players if not (sum(qs) == n): continue # and the numbers of players with the lowest points must divide into 1000 if not (1000 % qs[0] == 0): continue # generate the list of totals ts = flatten([p] * q for (p, q) in zip(ps, qs)) # use Landau's Theorem to check if this sequence of totals is viable if all(s >= C(i, 2) for (i, s) in enumerate(csum(ts), start=1)): # output solution ts.reverse() printf("[n={n}: ps={ps} t={t} -> scores = {ts}]") m = multiset.from_seq(ts) for (s, k) in m.multiplicity(max(m.values())): printf("num players = {n}; most common score = {s} (appears {k} times)") printf()LikeLike
Ruud 3:23 pm on 7 June 2026 Permalink |
import peek import istr import functools @functools.cache def primes(n): return [*map(int, istr.primes(n))] def is_feasible_round_robin(wins): # Landau theorem (suggested byChatGPT) prefix = 0 for k in range(1, len(wins) + 1): prefix += wins[k - 1] if prefix < k * (k - 1) // 2: return False return True def assign(total, p, n, nteams): if p: for i in primes(total // p[0] + 1): if total - p[0] * i == 0: if len(p) == 1 and 10 % n[0] == 0 and sum(n) + i == nteams: yield n + [i] break yield from assign(total - p[0] * i, p[1:], n + [i], nteams) for n in range(4, 28, 2): total = n * (n - 1) // 2 for s in assign(total, primes(n), [], n): wins = [] for i, j in zip(s, primes(n)): wins.extend(i * [j]) if is_feasible_round_robin(wins): most_frequent = [j for i, j in zip(s, primes(n)) if i == max(s)] peek(n, most_frequent, wins, s)LikeLike