Teaser 3324: Prime tournament
From The Sunday Times, 7th June 2026 [link] [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, multiset, 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 m = multiset.from_pairs(zip(ps, qs)) ts = list(m.sorted(reverse=1)) printf("-> {ts}") assert sum(ts) == t # 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: There were 16 players in the tournament. The most common score was 11 points.
With 16 players there are C(16, 2) = 120 matches, and so 120 points are allocated.
The scores were:
The bottom two players (with 2 points each) get to share the £ 10 booby prize, so they get £ 5 each.
There are many possible tables that correspond to this score sequence, but here is one example to show it is possible:
So, although we don’t need the MiniZinc program to check that a score sequence is viable, it is handy to generate an example table for a given score sequence.
The first program finds 16 candidate score sequences:
Each sequence starts with 2 copies of the largest prime less than n (and this is the smallest number of copies of the largest prime that can appear, as it must appear a prime number of times).
But none of the cases where the largest prime is (n − 1) are possible, as the first player (A) has to win all of their matches, and so does the second player (B). But they cannot both win the A v B match.
This means we can immediately eliminate all candidate numbers of players that are one more than a prime number.
This leaves a single candidate score sequence:
So if the puzzle has a solution it must be derived from this score sequence, and the sequence is enough to give the required answer without finding a viable table.
However for a complete solution we need to demonstrate that the table can be constructed. In my first solution (above) I used a MiniZinc model to search for a viable table, and in my second solution (below) I used Landau’s Theorem to check the score sequence. The Landau condition is both necessary and sufficient for a valid table to exist, but it does not build a constructive example.
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):
(And I believe the theorem also holds providing 1 point is divided between the teams involved in each match, specifically it still holds in the case where each side in a drawn match is awarded ½ point. [J W Moon, 1963]).
The following Python program runs in 138ms. (Internal runtime is 67ms).
from enigma import (irange, primes, C, express, csum, compare, multiset, printf) # check the Landau condition for the score sequence of a tournament of <n> teams # ss = ordered score sequence (low to high) 0 <= ss[i] <= n - 1 def landau(n, ss): if not (len(ss) == n and n > 1): raise ValueError("landau: invalid arguments") for (i, s) in enumerate(csum(ss), start=1): r = compare(s, C(i, 2)) if r == -1: return False return (r == 0) # 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 m = multiset.from_pairs(zip(ps, qs)) ts = list(m.sorted()) assert sum(ts) == t # check the Landau condition for this sequence if not landau(n, ts): continue # output solution ts.reverse() printf("[n={n}: ps={ps} t={t} -> scores = {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
Frits 8:01 pm on 8 June 2026 Permalink |
I totally forgot to work on yesterday’s teaser.
# primes below 28 P = [2, 3, 5, 7, 11, 13, 17, 19, 23] Pmin2 = {p - 2 for p in P} # decompose: choose numbers from <ns> so that sum(chosen numbers) equals <t> def decompose(t, k, ns, s=[]): if k == 0: if t == 0: # count last added number if s.count(s[-1]) in Pmin2: yield s else: for n in ns: if s and n > s[-1]: # count last added number if s.count(s[-1]) not in Pmin2: break if n <= t and (not s or n >= s[-1]): yield from decompose(t - n, k - 1, ns, s + [n]) # Landau theorem (from Ruud's program) def is_feasible_round_robin(wins): prefix = 0 for k in range(1, len(wins) + 1): prefix += wins[k - 1] if 2 * prefix < k * (k - 1): return False return True # number of players for n in range(2, 28, 2): pts = (n * (n - 1)) // 2 # each prime number less than the number of players occurred as somebody's score if not(prms := [p for p in P if p < n]): continue # each such score was achieved by a prime number of players (so at least 2) if (todo := pts - 2 * sum(prms)) < 0: continue # determine the primes to make the missing <todo> points for s in decompose(todo, n - len(prms) * 2, prms): # the players who tied bottom shared equally the £10 booby prize if 1000 % (2 + s.count(2)): continue # is this set of wins viable (using Landau theorem) if not is_feasible_round_robin(wins := sorted(prms * 2 + s)): continue # frequency of scores freq = sorted(((wins.count(w), w) for w in set(wins))) # calculate most common scores mcs = [str(y) for (x, y) in freq if x == freq[-1][0]] print(f"answer: {n} players, most common score = {' or '.join(mcs)}")LikeLike
Alex.T.Sutherland 6:58 pm on 12 June 2026 Permalink |
The method used is as follows:-
Iterate from N = 10:2:26 (does not get to the end).
Calculate Cartesian Products to find all combinations (cp).
Extract those vectors with a first digit 2 or 5 together with
sum of cp*primes = games played.
(primes are those relevant to N).
Test each of these vectors against Landau’s Tournament Theorem
until there is only one vector left that satisfies it.
This gives the number of players who have played which primes.
The vector is unique. Hence the answer to the question.
Break when single vector found.
My Landau’s function is similar to a python Landau function.
Run time from N=10:? is ~ 4ms.
My answer : Product of the Landau’s output vector = 240.
LikeLike
Tony Smith 10:59 am on 15 June 2026 Permalink |
An algorithm in which teams starting from the bottom beat the correct number of the lowest available teams can be used to construct a valid tournament from the single candidate score sequence.
Team 16 beats 15 and 14.
15 beats 14 and 13
14 beats 13, 12,11
13 beats 16, 12,11
12 beats 16,15,11,10,9
11 beats 16,15,,10,9,8
10 beats16,15,14,13,9 etc
LikeLike