"100囚犯"算法
Julia编程语言中"100囚犯"任务中囚犯生存策略的实现和建模的一个例子。
导言
"100囚犯"算法是从概率论和combinatorics一个众所周知的难题。 它展示了一种自相矛盾的情况,其中使用某种策略显着增加了成功的机会(与随机选择相比),尽管直觉上似乎这是不可能的。
任务的本质:
我们有100名囚犯,编号从1到100,还有100个盒子,也编号从1到100,每个盒子都有一张隐藏的卡片,上面写着其中一名囚犯的号码(每张卡片一张)。 目标是确保所有囚犯都可以通过打开每个不超过50个盒子来找到自己的卡片。
当所有100人都找到他们的牌时,胜利被认为是实现的。
有两种可能的策略:随机和最优:
-Random:每个囚犯随机选择50个盒子;
-Optimal("循环方法"):每个人都从一个带有自己号码的盒子开始,然后进入带有他们之前找到的卡号码的盒子。
这个例子的目的是使用大量的数值实验来证明这两种方法之间的区别。
要在Julia中使用外部库,必须先安装它们。:
import Pkg; Pkg.add("Random") # 用于生成随机数据的库
Pkg.add("Formatting") # 用于格式化输出的库
之后,我们连接必要的模块。
using Random # 用于洗牌数组的模块(shuffle!),获得随机样本(randperm)
using Formatting # 漂亮的输出格式的功能(例如,格式(。..))
Randomplay函数
这个特性运行了大量的模拟,囚犯使用随机策略:他们打开50个盒子,没有任何逻辑。
"""
randomplay(n::Int64, numprisoners::Int64=100)
Выполняет указанное число (n) симуляций ситуации задачи '100名囚犯',
采用随机搜索策略。 每个囚犯打开50个随机盒子。
返回所有100名囚犯发现他们的卡片时(n次尝试中)的百分比。
"""
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
最佳播放功能
该函数模拟相同的过程,但已经使用了基于循环遍历与其中找到的值相对应的框数的最佳策略。
"""
optimalplay(n::Int64, numprisoners::Int64=100)
Выполняет указанное число (n) симуляций ситуации задачи '100名囚犯',
使用最优策略:每个囚犯从一个自己数量的盒子开始,并继续移动
根据从找到的卡片中取出下一个数字的顺序。
返回所有100名囚犯发现他们的卡片时(n次尝试中)的百分比。
"""
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
建模和结果分析
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.")
我们考虑了两种不同的方法来解决"100囚犯"问题:
1)随机(无效),
2)最优(基于对排列中的循环的分析)。
通过大量的数值实验(在我们的例子中,十万),清楚地显示了获胜概率的不同。:
-通过完全随机的搜索,不太可能获胜(约0%);
-当使用周期性策略时,大约30-35%的情况下可能获胜。
正是这种计算使得即使在这种"看似混乱"的系统中也可以理解数学优化的重要性。 这个问题是图论和概率策略的一个很好的例子.
该示例是使用[罗塞塔代码]的材料开发的(https://rosettacode.org/wiki/100_prisoners )