The "100 prisoners" algorithm
An example of the implementation and modeling of a prisoner survival strategy in the "100 prisoners" task in the Julia programming language.
Introduction
The "100 Prisoners" algorithm is a well—known puzzle from probability theory and combinatorics. It demonstrates a paradoxical situation in which using a certain strategy significantly increases the chances of success (compared to random selection), although intuitively it seems that this is impossible.
The essence of the task:
We have 100 prisoners, numbered from 1 to 100, and 100 boxes, also numbered from 1 to 100, each with a hidden card with the number of one of the prisoners (one card for each). The goal is to ensure that all prisoners can find their own card by opening no more than 50 boxes each.
A victory is considered achieved when all 100 people find their cards.
There are two possible strategies: random and optimal:
- Random: Each prisoner selects 50 boxes at random;
- Optimal ("cyclic method"): everyone starts with a box with their own number and then proceeds to the box with the number of the card they found earlier.
The purpose of the example is to demonstrate the difference between these two approaches using a large number of numerical experiments.
To use external libraries in Julia, you must first install them.:
import Pkg; Pkg.add("Random") # Library for generating random data
Pkg.add("Formatting") # Library for formatting output
After that, we connect the necessary modules.
using Random # A module for shuffling arrays (shuffle!), obtaining random samples (randperm)
using Formatting # Functionality for beautiful output formatting (for example, format(...))
Randomplay function
This feature runs a large number of simulations where prisoners use a random strategy: they open 50 boxes without any logic.
"""
randomplay(n::Int64, numprisoners::Int64=100)
Выполняет указанное число (n) симуляций ситуации задачи '100 prisoners',
using a RANDOM search strategy. Each prisoner opens 50 random boxes.
Returns the percentage of cases (out of n attempts) when all 100 prisoners found their cards.
"""
function randomplay(n::Int64, numprisoners::Int64=100)
pardoned = 0 # The counter of successful simulations
for i in 1:n # Running a cycle for all simulations
indrawer = collect(1:numprisoners) # Creating a list of cards inside the boxes [1, 2, ..., 100]
shuffle!(indrawer) # Shuffle the cards (simulate a random distribution)
found_all = true # We assume that everyone will find their cards.
for prisoner in 1:numprisoners # We go through each prisoner
found = false # Initially, we believe that this prisoner did not find the card.
# We select 50 random mailbox numbers:
for reveal in randperm(numprisoners)[1:div(numprisoners, 2)]
if indrawer[reveal] == prisoner # A card with the required number was found
found = true
break # The prisoner has successfully completed the search
end
end
if !found # If the current prisoner did NOT find it, then the WHOLE group lost.
found_all = false
break # There's no point playing anymore
end
end
if found_all # If everyone has done well, we increase the victory counter.
pardoned += 1
end
end
return 100.0 * pardoned / n # Converting a share to a percentage
end
Optimalplay function
The function simulates the same process, but already uses an optimal strategy based on cycling through the numbers of boxes corresponding to the values found in them.
"""
optimalplay(n::Int64, numprisoners::Int64=100)
Выполняет указанное число (n) симуляций ситуации задачи '100 prisoners',
using an OPTIMAL strategy: each prisoner starts with a box of his own number and continues to move
according to the sequence where the next number is taken from the found card.
Returns the percentage of cases (out of n attempts) when all 100 prisoners found their cards.
"""
function optimalplay(n::Int64, numprisoners::Int64=100)
pardoned = 0 # Successful Game Counter
for i in 1:n
indrawer = collect(1:numprisoners) # Initialize the array with the card numbers
shuffle!(indrawer) # We lay them out randomly as follows
found_all = true # Initial assumption: everything is successful
for prisoner in 1:numprisoners # Busting prisoners
reveal = prisoner # Starting step: open the box with the prisoner's number.
found = false
# We open up to 50 boxes
for j in 1:div(numprisoners, 2)
card = indrawer[reveal] # We are looking at the contents of the current mailbox
if card == prisoner # If the card you are looking for is there, success!
found = true
break
else
reveal = card # Let's move on to the mailbox with the number we just saw.
end
end
if !found # If someone fails, the whole group loses
found_all = false
break
end
end
if found_all # If no one made a mistake, the group was verified.
pardoned += 1
end
end
return 100.0 * pardoned / n # Percentage of successful games
end
Modeling and analysis of results
const N = 100_000 # The number of repetitions of the simulation (can be changed)
println("Simulation count: $N") # Notification of the number of simulations
println("Random play wins: ",
format(randomplay(N), precision=3), # The result of a random strategy with formatted output
"% of simulations.")
println("Optimal play wins: ",
format(optimalplay(N), precision=3), # The result of an optimal strategy
"% of simulations.")
We have considered two different approaches to solving the “100 prisoners” problem:
-
random (ineffective),
-
optimal (based on the analysis of cycles in permutations).
Through a large number of numerical experiments (in our case, one hundred thousand), it was clearly shown how the winning probabilities differ.:
-
With a completely random search, a win is unlikely (about 0%);
-
When using a cyclical strategy, a win is possible in about ~30-35% of cases.
It is this calculation that makes it possible to understand the importance of mathematical optimization even in such "seemingly chaotic" systems. This problem serves as an excellent illustration of graph theory and probabilistic strategies.
The example was developed using materials from Rosetta Code