据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。听着这个故事感觉有点坑,也不知道他们活下来的最后两个是怎么逃脱罗马人的追杀的,当然我们并不去探讨历史故事,我们来思考一下关于他是怎么确定这两个问题的,这个故事的算法的很多,我找了两个我认为比较好理解的算法来解决这个问题。
一,数组解决约瑟夫问题
首先,我们可以利用数组去解决这个问题,我相信这也是大多数人看到这个题的第一个解决方法,因为有41个人嘛,我们可以建立一个数组,数组中有41个元素,我们可以将他们全部初始化为0(当然也可以不初始化),然后我们将里面的每个元素进行赋值,因为是每数到3的时候那个人就要死去,所以第一轮的时候我们可以将他们全部赋值上1,2,3然后继续,比如a[0] = 1,a[1] = 2,a[2] = 3,a[3] = 1.......这样我们只需要当检测到当前元素的值为3时,我们就把死亡的次数+1,并且第二轮的时候不用去管数值为3的元素,只需要将不是的3的元素重新初始化,这样相当于值为3的元素已经死去了,当死亡人数为39的时候,所剩下的最后2个元素即为最后可以存活下来的2个人,还有一个细节就是我们的数组下标每增加3才会有一个人死去, 很明显数组下标如果一直自增的话根本等不到第39个人去数组下标便会大于41,这里我们用数组下标 摸上总人数,这样每当我们数组下标达到总人数的时候便会从头开始,永远不会溢出,借此来起到循环遍历数组的作用,我们也可以做个判断,当数组下标溢出的时候我们便让他从头开始,这样也可以起到循环遍历的目的,下面为了便于理解,我分别写上伪代码和正常编写,可以对照着去看!
用伪代码编写
#include <stdio.h>
#include <stdlib.h>
#define 总人数 41
int main()
{
int 约瑟夫环[总人数], 已经死掉的人 = 1, 报数 = 0, 数组下标 = 0;
while (已经死掉的人 <= 41)
{
if (约瑟夫环[数组下标] == 3)
{
数组下标++;
if (数组下标 == 41) //如果数组下标为41,我们就将他归0
{
数组下标 = 0;
}
continue;
}
else
{
约瑟夫环[数组下标] = 报数 % 3 + 1; //给数组赋初值,报数这里用摸,可以使他的值永远在1,2,3循环
报数++;
if (约瑟夫环[数组下标] == 3)
{
printf("第%d次要死的人是%d\n", 已经死掉的人, 数组下标 + 1); //加一是因为数组下标是从0开始的,而我们实际生活中都是从1开始数的!
已经死掉的人++;
}
数组下标++;
if (数组下标 == 41)
{
数组下标 = 0;
}
}
}
system("pause");
return 0;
}
普通变量编写
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a[41], death = 1, i = 0, turn = 0;
while(death <= 41)
{
if(a[i%41] == 3) //运用摸可以让它的值永远不会超过41
{ //一旦超过41我们可以想象成是给它归0
i++;
continue;
}
else
{
a[i % 41] = turn++ % 3 + 1;
if(a[i%41] == 3)
{
printf("第%d次死的人是%d\n", death, i % 41 + 1);
death++;
}
i++;
}
}
system("pause");
return 0;
}
二. 递归处理约瑟夫问题
用递归处理约瑟夫问题不要太精妙,当初刚学完数组处理约瑟夫问题再看了下别人用递归写的代码,瞬间懵逼了!
int Ysf(int total,int n)
{
if (n == 1) return 2;
return (Ysf(total - 1, n - 1) + 3) % total;
}
2行。。。。。。。。。(
已经不能做朋友了吗
)
不过仔细研究下发现这个问题用递归处理真的很巧妙,不过递归理解起来还是比数组难的,对于我这个完全不懂递归的,这个思路我大致是看懂了!
这里我们就来举个栗子吧,当让不会举41个人的栗子啦(毕竟太多了),我们只需要找出一定的规律就好啦,这里我们就假如有9个人,同样还是每数到3的人出列(既然使我们举的栗子就不让他们去死了),那么他们在数组中对应的下标就分别是0-8。我么可以列个图,方便理解。
很简陋的图,将就看吧。。。。我们只列了3行,第一行是一个人都没有出列的时候,第二行是出列一个人的时候,第三行是出列2个人情况,每次只要有人出列我们就从他的后一个元素从0开始数,像这样我们可以找出每行之间编号的一些规律,从上面可以看出当我们出列到一行只有一个人的时候那我们就找到了最后活着的那一个,我们可以根据他所在行的编号来往上一步步推出他在第一行是几号,上图我右边公式中的n代表他在当前行所处的元素的编号,
即我们找到规律是(当前编号+3)% 上一行的总人数 即可以推出他在上一行所处的编号
即我们可以根据第三行元素的编号来推出他在第二行所处的编号,再根据他在第二行所处的编号来推出他在第一行所处的编号,我们现在还知道第一个出列的元素是二,从而我们可以获取我们想要知道谁是第几个退出的,我把上面的公式再用伪代码展示一下,结合图片应该比较好理解。
#include <stdio.h>
#include <stdlib.h>
int 约瑟夫问题(int 总人数,int 第n个死的人)
{
if (第n个死的人 == 1) return 2;
return (约瑟夫问题(总人数 - 1, 第n个死的人 - 1) + 3) % 总人数;
}
int main()
{
printf("最后一个存活的人是%d\n", 约瑟夫问题(41, 41) + 1);
system("pause");
return 0;
}
因为我们计算的时候都是从0的开始的,而在现实生活中我们是从1开始数的,所以为了对应实际情况,我们应该在打印的时候+1。
有什么说的不对的地方或者有什么更精妙的算法也希望大神能够指出来,谢谢