目录
约瑟夫问题大概是这样的:有n个人围成一圈,编号为 1, 2,....,n 。从第一个人开始报数(从1开始),报到 k 数字的人出圈,然后下一个人又从 1 开始报数,...,以此类推,直到最后只剩下一个人,问这个人的编号是多少。
模拟解决约瑟夫问题
大概思想:用双端队列模拟这个圈,每次将前k-1个人移至队列尾部,然后删除队列第一个元素,直到剩下一个人。
#include <iostream>
#include <deque>
using namespace std;
int n; // n 个人
int k; // 报到k数字的人出圈,从1开始报
deque<int> Q; // 模拟围成的圈
int main(){
cin >> n >> k;
for(int i = 1; i <= n; i++){
Q.push_back(i); // 1~n 依次排成一排 : 1 2 3 4 ... n
}
while(Q.size() != 1){ // 直到最后只剩一个人退出循环
for(int i = 0; i < k - 1; i++){ // 将k-1个人移至尾部
int t = Q.front(); // 得到队列的第一个元素
Q.pop_front(); // 并删除第一个元素
Q.push_back(t); // 将这个元素插入到队列的尾部:2 3 4 ..... n 1
}
Q.pop_front(); // 将这个人出圈
}
cout << Q.front() << endl; // 最后就剩下一个人,输出即可
return 0;
}
递推解决约瑟夫环问题
大概思想:
如图,开始的时候,编号依次为 0,1,...,n - 1(图中n=10),然后报到2数字出圈(图中红色数字代表出圈)。然后下一轮重新编号:谁出圈,就从这个人的下一个人开始从 0 编号。
直到最后还剩下一个人,那么他的编号一定是 0(图中黄色框),我们所要求的也就是这个列的第一个数字(黄色列第一个数字)。
我们发现,我们已知的(黄色框)和我们想求的都在同一列,一个在下边,一个在上边,如果能依次递推该多好啊
我们正是通过第 i 轮的编号求出第 i - 1轮的编号,然后依次递推求出来的。那么相邻的两轮编号之间有什么关系呢,我们来开始分析
我们先看【开始】到【第1轮】编号之间有什么关系,如下图:
我们把上一轮的编号叫做 oldNum , 下一轮的编号叫做 newNum,也就是说 oldNum 和 new之间有什么关系呢?
不难发现,他们的关系是 oldNum = (newNum + 2) % 10。
我们再来看看【第1轮】到【第2轮】编号之间的关系,看看能否找到规律,如图:
同样的,上一轮的编号为 oldNum,下一轮的编号为 newNum,不难发现,他们之间的关系为: oldNum = (newNum + 2) % 9
和【开始】到【第1轮】之间的关系 oldNum = (newNum + 2) % 10 比较,仿佛如果接着分析【第2轮】到【第3轮】,是不是有 oldNum = (newNum + 2) % 8
通过查表发现,还真是这么回事。变化的只有 % 后面的数字,而 % 前面都是固定不变的。那么这个 % 后面的数字是什么?
其实他就是上一轮中剩下的人数(图中第二列)。
我们一开始只知道图中黄色框为 0, 这样我们就能求出黄色框上面框的数字了:此时 newNum = 0(黄色框),可以求出黄色框上面框 oldNum = (newNum + 2) % 2。然后又可以求黄色框上上面框的编号,.... ,就这样,通过迭代直到求出一开始的编号。
#include <iostream>
using namespace std;
int main(){
int n; // n个人
// 这里编号从0开始,即 0 1 2 3 4 ..... n-1
int k; // 报到k数字的人出圈,每次从1开始报数
cin >> n >> k;
int num = 0; // 当人数为1个人的时候,那个活下来的人的编号,它总是0,因为每次重新从0开始编号
// 现在反推当人数为n的时候编号为多少
for(int current = 2; current <= n; current++){
// 当人数为current个人的时候那个活下来的编号
num = (num + k) % current;
}
// 因为一开始的编号是 从0开始到n-1, 而题目是从1到n编号,所以+1
cout << num + 1 <<endl;
return 0;
}
递归解决约瑟夫环问题
从递推到递归的转变不难,就如同斐波那契数列求第 n 项那样,递推是从底向上,而递归是从上至下,递推是从已知推未知,而递归是一开始不知道,就问下一层,直到最底层知道,然后就向上传。
#include <iostream>
using namespace std;
// 还剩下 current 人的那一轮
int Num(int current, int k){
if(current == 1){
// 当剩下一个人,他的编号总是0
return 0;
}else{
// 我们现在不知道下一轮的编号, 就问下一层,也就是这里 Num(current-1, k) 的含义
return (Num(current - 1, k) + k) % current;
}
}
int main(){
int n, k;
cin >> n >> k;
// 编号 0 1 2 3 .... n-1
// 最后记得+1,因为一开始题目编号规定是1-n。如果题目规定0到n-1,则不需要+1
cout << Num(n, k) + 1 << endl;
return 0;
}