Games & Miscellaneous contents
The Josephus Problem
People stand in a circle and every k-th one is eliminated; find the survivor with a recurrence in O(n), the closed form for k = 2, and an O(k log n) method.
Read first: Recursion, Bit Manipulation
people stand in a circle, numbered . Starting from person 1, count people clockwise; the -th person is eliminated and leaves the circle; counting resumes from the next person. Repeat until one person remains. Who is the survivor?
Legend says Flavius Josephus, cornered by Roman soldiers with 40 companions, worked out where to stand to be the last one alive.
Simulation
The obvious approach keeps a list and removes an element times. Removing from the middle of a Python list costs , so the total is ; with a deque rotation it is .
def josephus_simulation(n, k):
people = list(range(1, n + 1))
idx = 0
while len(people) > 1:
idx = (idx + k - 1) % len(people)
people.pop(idx)
return people[0]
assert josephus_simulation(7, 3) == 4
assert josephus_simulation(41, 3) == 31 # the legendary answer
assert josephus_simulation(10, 2) == 5recurrence
Let be the 0-indexed position of the survivor among people. After the first elimination (person in 0-indexed form) there are people, and counting restarts from the person at position . That subgame is the same problem with people, so its survivor position must be translated back to the original numbering by shifting positions:
def josephus(n, k):
"""0-indexed survivor position."""
survivor = 0
for size in range(2, n + 1):
survivor = (survivor + k) % size
return survivor
assert josephus(7, 3) + 1 == 4
assert josephus(41, 3) + 1 == 31
assert all(josephus(n, k) + 1 == josephus_simulation(n, k) for n in range(1, 40) for k in range(1, 12))
assert josephus(10 ** 6, 7) >= 0 # a million people: still instantAdd 1 for the 1-indexed answer. This is time and memory.
Closed form for
For : write with . The survivor is (1-indexed). In binary, the answer is obtained by rotating the binary representation of left by one position: move the leading 1 to the end.
def josephus_k2(n):
m = n.bit_length() - 1
L = n - (1 << m)
return 2 * L + 1
def josephus_k2_rotate(n):
b = bin(n)[2:]
return int(b[1:] + b[0], 2)
assert [josephus_k2(n) for n in range(1, 11)] == [1, 1, 3, 1, 3, 5, 7, 1, 3, 5]
assert all(josephus_k2(n) == josephus_k2_rotate(n) == josephus(n, 2) + 1 for n in range(1, 500))Why. With even, one pass removes all even-numbered people and leaves people in the same situation but with odd numbers; with odd, the first pass removes 2, 4, ..., , and then person 1 is eliminated next, leaving people starting from person 3. This gives and (1-indexed), which is a rotation of bits.
for small
When is huge (say ) and is small, the recurrence is too slow. Observe that if is small, the recurrence adds many times before the modulus wraps around. We can jump over the whole run of steps in which no wrap occurs:
def josephus_fast(n, k):
"""0-indexed survivor, O(k log n)."""
if k == 1:
return n - 1
size, pos = 1, 0 # invariant: pos is the survivor among `size` people
while size < n:
# number of steps before pos + steps*k reaches `size + steps`, i.e. before a wrap-around
steps = (size - pos - 1) // (k - 1) + 1
steps = min(steps, n - size)
pos = (pos + steps * k) % (size + steps)
size += steps
return pos
assert all(josephus_fast(n, k) == josephus(n, k) for n in range(1, 300) for k in range(1, 9))
assert josephus_fast(10 ** 6, 3) == josephus(10 ** 6, 3)
assert josephus_fast(10 ** 18, 2) + 1 == josephus_k2(10 ** 18)Each iteration multiplies size by roughly , so there are iterations. That makes feasible for small .
A recursive formulation of the same trick handles the case via directly, as in the loop.
Related problems
- Identify the order of eliminations (simulate with a Fenwick tree using "find the -th remaining person", ).
- The survivor when the first count starts from a different person: shift the answer.
- Variations where the direction alternates or the step changes with each round.
def elimination_order(n, k):
"""Order in which people are eliminated (O(n^2) simulation; fine for small n)."""
people = list(range(1, n + 1))
idx, order = 0, []
while people:
idx = (idx + k - 1) % len(people)
order.append(people.pop(idx))
return order
assert elimination_order(7, 3) == [3, 6, 2, 7, 5, 1, 4]