问题背景
约瑟夫环问题的起源可以追溯到古罗马时期的一个传说。在一次战争中,40个士兵被敌人包围,为了避免被俘,他们决定自杀。他们围成一个圈,每数到第3个人就自杀,直到最后只剩下一个人。这个问题被称为约瑟夫问题,是因为其中一个士兵约瑟夫(Josephus)据说通过计算的位置逃过了这一劫。
一、递推公式
从最基本的情况开始分析
题目可以简化为: 有N
个人排列成一个环,每隔M
个人就淘汰一个人,直到只剩下一个人。我们需要找出最后幸存者的位置。
当只有一个人时N=1
,显然他是幸存者,位置为0。
当有两个人时N=2
,每数到第 M
个人自杀,剩下的人的位置可以通过递推公式计算出来。递推公式的推导过程如下:
假设 N=5, M=3
- 当
N=1
时,幸存者的位置是:
f(1,3)=0
- 当
N=2
时,幸存者的位置是:
f(2,3)=(f(1,3)+3)%2=(0+3)%2=1
- 当
N=3
时,幸存者的位置是:
f(3,3)=(f(2,3)+3)%3=(1+3)%3=1
- 当
N=4
时,幸存者的位置是:
f(4,3)=(f(3,3)+3)%4=(1+3)%4=0
- 当
N=5
时,幸存者的位置是:
f(5,3)=(f(4,3)+3)%5=(0+3)%5=3
所以,当N=5
时,最后幸存者的位置在从0开始编号的情况下是3
递推公式为:f(N,M)=(f(N−1,M)+M)%N
我们可以用这个公式逐步计算出较大的 N
的情况。
为什么人数为N时的幸存者位置,取决于N-1?
回顾递推公式
f(N,M)=(f(N−1,M)+M)%N
这个公式的核心思想是:如果我们知道N−1
个人时的幸存者位置,那么可以通过这个位置计算出 N
个人时的幸存者位置。
假设我们已经知道N−1
个人时的幸存者位置为f(N−1,M)
。
当有N
个人时,我们从第一个人开始数,每数到第M
个人时,把这个人移除。
移除这个人后,剩下的N−1
个人的情况和之前的情况是一样的,只是位置发生了变化。
位置变化的详细分析
为了推导递推公式,我们先理解从N
个人到 N−1
个人的转换过程。
假设我们知道N−1
个人的情况下,最后幸存者的位置是f(N−1,M)
。
从N
个人到N−1
个人:
当有N
个人时,编号从 0
到N−1
。
第一次淘汰发生在编号为(M−1)%N
的人。
淘汰后,剩下N−1
个人,并且从淘汰的下一个人开始重新编号。
重新编号:
淘汰编号为(M−1)%N
的人后,剩下的N−1
个人的编号变为:
(M%N),(M+1%N),…,(N−1),0,1,…,(M−2)
我们可以将这些编号重新映射为从0
到N−2
的新编号:
0,1,2,…,N−2
对于N−1
个人,我们已经知道幸存者的位置是f(N−1,M)
,这个位置是重新编号后的相对位置。
我们需要将这个相对位置转换回原来的编号体系。
重新编号的第f(N−1,M)
个人在原来编号中的位置是:(f(N−1,M)+M)%N
二、代码实现
1.递归实现
public class JosephusProblem {
public static int josephus(int n, int m) {
if (n == 1) {
return 0;
} else {
return (josephus(n - 1, m) + m) % n;
}
}
public static void main(String[] args) {
int n = 5; // 总人数
int m = 3; // 每数到第 m 个人
int result = josephus(n, m); // 结果从 0 开始编号
System.out.println("最后剩下的人的位置是: " + result);
}
}
2.迭代实现
public class JosephusProblem {
public static int josephus(int n, int m) {
int ans = 0;
for (int i = 2; i <= n; i++) {
ans = (ans + m) % i;
}
return ans;
}
public static void main(String[] args) {
int n = 5; // 总人数
int m = 3; // 每数到第 m 个人
int result = josephus(n, m); // 结果从 0 开始编号
System.out.println("最后剩下的人的位置是: " + result);
}
}