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")     # Библиотека для генерации случайных данных
Pkg.add("Formatting") # Библиотека для форматирования вывода
   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      # Модуль для перемешивания массивов (shuffle!), получения случайных выборок (randperm)
using Formatting  # Функциональность для красивого форматирования вывода (например, 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 заключенных',
используя СЛУЧАЙНУЮ стратегию поиска. Каждый заключённый открывает 50 случайных ящиков.

Возвращает процент случаев (из n попыток), когда все 100 заключённых нашли свои карточки.
"""
function randomplay(n::Int64, numprisoners::Int64=100)
    pardoned = 0       # Счётчик успешных симуляций
    
    for i in 1:n              # Запускаем цикл по всем симуляциям
        indrawer = collect(1:numprisoners)  # Создаем список карточек внутри ящиков [1, 2, ..., 100]
        shuffle!(indrawer)                  # Перемешиваем карточки (имитируем случайное распределение)

        found_all = true         # Предполагаем, что все найдут свои карточки
        
        for prisoner in 1:numprisoners           # Проходимся по каждому заключённому
            found = false                        # Изначально считаем, что этот заключённый не нашёл карточку

            # Выбираем 50 случайных номеров ящиков:
            for reveal in randperm(numprisoners)[1:div(numprisoners, 2)]  
                
                if indrawer[reveal] == prisoner   # Найдена карточка с нужным номером
                    found = true
                    break                         # Заключённый успешно закончил поиск
                end
            end
            
            if !found                          # Если текущий заключённый НЕ нашёл, то ВСЯ группа проиграла
                found_all = false
                break                            # Больше играть нет смысла
            end
        end

        if found_all                           # Если все справились, увеличиваем счётчик побед
            pardoned += 1
        end
    end

    return 100.0 * pardoned / n                 # Преобразовываем долю в процент
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 заключенных',
используя ОПТИМАЛЬНУЮ стратегию: каждый заключённый начинает с ящика своего номера и продолжает двигаться 
по последовательности, где следующий номер берётся из найденной карточки.

Возвращает процент случаев (из n попыток), когда все 100 заключённых нашли свои карточки.
"""
function optimalplay(n::Int64, numprisoners::Int64=100)
    pardoned = 0          # Счетчик успешных игр  

    for i in 1:n
        indrawer = collect(1:numprisoners)  # Инициализируем массив с номерами карточек
        shuffle!(indrawer)                   # Раскладываем их случаем образом

        found_all = true                     # Исходное допущение: все успешно
        
        for prisoner in 1:numprisoners        # Перебор заключённых
            reveal = prisoner                 # Стартовый шаг: открываем ящик с номером самого заключенного
            found = false                     

            # Открываем до 50 коробок
            for j in 1:div(numprisoners, 2)
                card = indrawer[reveal]         # Смотрим содержимое текущего ящика
                
                if card == prisoner             # Если там лежит искомая карточка – успех!
                    found = true                
                    break
                else
                    reveal = card               # Переходим к ящику с тем номером, который мы только что увидели
                end
            end

            if !found                           # Если кто-то провалился – вся группа теряет
                found_all = false
                break
            end
        end

        if found_all                            # Если никто не ошибся – группа прошла проверку
            pardoned += 1
        end
    end

    return 100.0 * pardoned  / n                 # Процент успешных игр
end
Out[0]:
optimalplay

Modeling and analysis of results

In [ ]:
const N = 100_000                              # Число повторений моделирования (можно изменять)

println("Simulation count: $N")               # Сообщение о количестве симуляций
println("Random play wins: ", 
        format(randomplay(N), precision=3),    # Результат случайной стратегии с форматированным выводом
        "% of simulations.")                   
println("Optimal play wins: ", 
        format(optimalplay(N), precision=3),   # Результат оптимальной стратегии
        "% 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