Books · Solving Puzzles through Mathematical Programming
Chapter 15
The Riddle of the Pilgrims
In The Canterbury Puzzles (), H. E. Dudeney poses a bedroom-allocation problem styled after Chaucer’s pilgrims: a company must be lodged in a two-storey dormitory, subject to a short list of the Abbot’s constraints. Each floor is a array of rooms with a central staircase, leaving eight rooms per floor and sixteen total. The Abbot’s rules are:
every room holds between one and three pilgrims, inclusive;
each of the four sides of the building lodges exactly eleven pilgrims (the three rooms on that side of the lower floor together with the three rooms on that side of the upper floor);
the upper floor holds exactly twice as many pilgrims as the lower floor.
When the pilgrims arrive, the group turns out to number three more than was first announced, and the monks must produce a second allocation — satisfying the same three rules — for the larger party. The puzzle’s entire force lies in showing that the first total and the second total are uniquely determined. The answer is that the first allocation lodges pilgrims and the second .
The programming model
The problem is a small integer-programming feasibility instance with fewer than twenty variables and a handful of linear equalities.
Variables
Index rooms by with the floor (lower, upper) and the cell. Let be the number of pilgrims in the room at ; the staircase cells are excluded by setting .
Side totals
For each of the four sides, the sum over the six rooms on that side (three cells on the lower floor, three on the upper) equals . Writing for rows and for columns :
Upper-lower ratio
Total feasibility
Let . The problem does not fix : we want to know which values of admit a valid allocation. The CP-SAT model with parameters.enumerate_all_solutions = True reveals exactly two: and . These are the first and second allocations.
A solver in twenty lines
from ortools.sat.python import cp_model
def solve(total=None):
m = cp_model.CpModel()
x = {}
for f in range(2):
for r in range(3):
for c in range(3):
if (r, c) == (1, 1):
x[(f, r, c)] = m.NewIntVar(0, 0, "")
else:
x[(f, r, c)] = m.NewIntVar(1, 3, "")
for r in (0, 2):
m.Add(sum(x[(f, r, c)]
for f in range(2)
for c in range(3)) == 11)
for c in (0, 2):
m.Add(sum(x[(f, r, c)]
for f in range(2)
for r in range(3)) == 11)
lower = sum(x[(0, r, c)]
for r in range(3)
for c in range(3))
upper = sum(x[(1, r, c)]
for r in range(3)
for c in range(3))
m.Add(upper == 2 * lower)
if total is not None:
m.Add(lower + upper == total)
s = cp_model.CpSolver()
s.Solve(m)
return {k: s.Value(v) for k, v in x.items()}
The two allocations
Figure 15.1 displays a representative first allocation ( pilgrims) and Figure 15.2 the unique-in-total second allocation ( pilgrims). In each figure, the lower floor is on the left and the upper floor on the right; the staircase is the shaded central cell; the number in each other cell is the pilgrim count; row and column sums are annotated.
The existence of exactly two feasible totals ( and ) is the fact that resolves Dudeney’s riddle: the monks’ increase of three pilgrims must bring the lodgers from up to , because no intermediate total is possible under the rules.
Sources. Dudeney’s The Canterbury Puzzles () gathers puzzles framed around Chaucerian pilgrims; the riddle here appears in the chapter “The Merry Monks of Riddlewell” as Problem 25. The puzzle’s statement is elementary but its mechanics — the constraint structure determining a finite set of admissible totals, with the step of three pilgrims landing on exactly the next admissible value — anticipate the formal study of integer-programming feasibility by several decades. The CP-SAT enumeration offered above is in principle overkill for a problem with sixteen variables, but it demonstrates, in a single call, that the reasoning underlying Dudeney’s solution has no hidden case left to check.