Engee 文档
Notebook

"100囚犯"算法

Julia编程语言中"100囚犯"任务中囚犯生存策略的实现和建模的一个例子。

导言

"100囚犯"算法是从概率论和combinatorics一个众所周知的难题。 它展示了一种自相矛盾的情况,其中使用某种策略显着增加了成功的机会(与随机选择相比),尽管直觉上似乎这是不可能的。

任务的本质:

我们有100名囚犯,编号从1到100,还有100个盒子,也编号从1到100,每个盒子都有一张隐藏的卡片,上面写着其中一名囚犯的号码(每张卡片一张)。 目标是确保所有囚犯可以通过打开每个不超过50个盒子来找到自己的卡片。

当所有100人都找到他们的牌时,胜利被认为是实现的。

有两种可能的策略:随机和最优:

-Random:每个囚犯随机选择50个盒子;

-Optimal("循环方法"):每个人都从一个带有自己号码的盒子开始,然后进入带有他们之前找到的卡号码的盒子。

这个例子的目的是使用大量的数值实验来证明这两种方法之间的区别。

要在Julia中使用外部库,必须先安装它们。:

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

之后,我们连接必要的模块。

In [ ]:
using Random      # Модуль для перемешивания массивов (shuffle!), получения случайных выборок (randperm)
using Formatting  # Функциональность для красивого форматирования вывода (например, format(...))

Randomplay函数

这个特性运行了大量的模拟,囚犯使用随机策略:他们打开50个盒子,没有任何逻辑。

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

最佳播放功能

该函数模拟相同的过程,但已经使用了基于循环遍历与其中找到的值相对应的框数的最佳策略。

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

建模和结果分析

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.

我们考虑了两种不同的方法来解决"100囚犯"问题:

1)随机(无效),

2)最优(基于对排列中的循环的分析)。

通过大量的数值实验(在我们的例子中,十万),清楚地显示了获胜概率的不同。:

-通过完全随机的搜索,不太可能获胜(约0%);

-当使用周期性策略时,大约30-35%的情况下可能获胜。

正是这种计算使得即使在这种"看似混乱"的系统中也可以理解数学优化的重要性。 这个问题是图论和概率策略的一个很好的例子.

该示例是使用[Rosetta代码]的材料开发的(https://rosettacode.org/wiki/100_prisoners