Engee documentation
Notebook

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.:

In [ ]:
import Pkg; Pkg.add("Random")     # Library for generating random data
Pkg.add("Formatting") # Library for formatting output
   Resolving package versions...
    Updating `~/.project/Project.toml`
  [9a3f8284] + Random v1.11.0
  No Changes to `~/.project/Manifest.toml`
   Resolving package versions...
    Updating `~/.project/Project.toml`
  [59287772] + Formatting v0.4.3
    Updating `~/.project/Manifest.toml`
  [59287772] + Formatting v0.4.3

After that, we connect the necessary modules.

In [ ]:
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.

In [ ]:
"""
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
Out[0]:
randomplay

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.

In [ ]:
"""
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
Out[0]:
optimalplay

Modeling and analysis of results

In [ ]:
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.")
Simulation count: 100000
Random play wins: 0.000% of simulations.
Optimal play wins: 31.467% of simulations.

We have considered two different approaches to solving the “100 prisoners” problem:

  1. random (ineffective),

  2. 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