故事背景
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
这里我们就算出最后一个存活的人即可。
方案1(一维数组)
/**
* 一维数组解法
*/
public static void ysf3() {
Scanner sc = new Scanner(System.in);
System.out.println("请输入总人数");
int personNum = sc.nextInt(); // 总人数
System.out.println("请输入相隔几个人报数");
int skip = sc.nextInt(); // 相隔几个人报数
int[] josephArr = new int[personNum];
int aliveNum = personNum;
int index = -1; // 因为第一个报数的下标是0,因此这个index初始值是-1
while (aliveNum > 1) {
int tempSkip = skip;
// 开始找下个要自杀的人
while (true) {
index = (index+1)%personNum; // 下标+1
if (josephArr[index] == 1) continue; // 如果该下标的值为1,那么就说明这个位置的人已经死了,跳出这个小循环
tempSkip --;
if (tempSkip == 0) break;
}
// 找到这个下标之后 将他的值赋1
josephArr[index] = 1;
System.out.println("杀死的人是:"+index);
aliveNum--; // 存活人数-1
for (int i=0; i<personNum; i++) {
System.out.print(josephArr[i]+" ");
}
System.out.println();
}
for (int i=0; i<personNum; i++)
if (josephArr[i] == 0)
System.out.println("活下来的人是:"+i);
}
这个解法比较简单,我就不做多详细解释了。
方案2(递归)
单纯求出存活到最后的人的初始下标
这个相对比较简单。
先上代码:
public static void main(String[] args) {
int aliveIndex = ysf1(6, 3);
System.out.println("下标为"+aliveIndex+"的人能活着!");
}
/**
* 返回值含义: 人数为n的圈,下个(也就是第一个)要自杀的人在当前圈中的下标
* @param n : 当前圈中一共多少人
* @param k : 报数的次数
* @return
*/
public static int ysf1(int n, int k) {
if (n == 1)
return 0;
else
return (ysf1(n-1,k)+k) % n;
}
理解:
ysf1(n, k) 的含义是,当前圈有n个人,以k为步长,最终最后一个留下的人在当前数组中的坐标
显然,当还有最后一个人的时候,那么最终这个人在当前圈的下标为0,即ysf1(1,k)=0
有n人,删掉第k人(下标为k-1)之后变成了n-1人,下次起始的地方是从这个出局的人后面开始算的,那么我们可以将这个新的起始位置的下标设为“0”组成一个新圈(认真思考,每个圈都是独立的)
以 n为6, k为3,举例说明:
0 1 2 3 4 5 // 删之前 即“先状态”
3 4 0 1 2 // 删之后 即“后状态”
那么若要从n-1个人的“后状态”恢复到“先状态”,其实所有人的编号相当于加k,亦即f[n] = f[n-1] + k,再由于是环的问题,所以加完再用“先状态”时的人数n取模即可。若递归到了1个人的情况,赢家就是编号为0的人。
求出自杀的人的出圈顺序
这个相对于上面来说稍难一点,但本质其实是一样的!
public static void main(String[] args) {
for (int i=1; i<=5; i++) {
System.out.println("第个"+i+"自杀的人,初始下标为"+ysf2(6,3,i));
}
}
/**
* 返回值含义: 人数为n的圈,第i个自杀的人在当前圈中的下标
* @param n : 当前圈中共有n个人
* @param k : 报数的次数
* @param i : 想求出第几个人自杀
* @return
*/
public static int ysf2(int n, int k,int i) {
if (i == 1)
return (k-1+n)%n;
else
return (ysf2(n-1,k,i-1)+k)%n;
}
理解:
ysf2(n, k, i)的含义是,人数为n的圈,以k为步长,第i个出圈的人在当前圈的下标
以 n为10,k为3 为例
0 1 2 3 4 5 6 7 8 9
7 8 0 1 2 3 4 5 6 第一次出圈
4 5 6 7 0 1 2 3 第二次出圈
1 2 3 4 5 6 0 第三次出圈
由上面数据可得
ysf2(10, 3, 1) = 2
ysf2(10, 3, 2) = 5 => ysf2(9, 3, 1) = 2
ysf2(10, 3, 3) = 8 => ysf2(9, 3, 2) = 5 => ysf2(8, 3, 1) = 2
可以得出规律
ysf2(n, k ,i) = (ysf2(n-1, k, i-1)+k)%n, i != 1
ysf2(n, k, i) = (k-1+n)%n, i = 1
总结
约瑟夫递归解法最主要的就是找到新圈与旧圈下标之间的关系,然后构造出一个有意义的方法,然后找寻其中的规律。 ysf2(n,k,i)相对于ysf1(n,k)来说难理解一点,多思考几遍。
如果这片文章有帮助到你,点个赞呗!
参考文章:「约瑟夫问题 递归解法」