一 问题描述
约瑟夫环问题的基本描述如下:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为1的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,要求找到最后一个出列的人或者模拟这个过程。
二 问题解法
在解决这个问题之前,首先我们对人物进行虚拟编号,即相当于从0开始把人物重新进行编号,即用0,1,2,3,...n-1来表示人物的编号,最后返回的编号结果加上1,就是原问题的解(为什么这么做呢,下文有解释)。而关于该问题的解通常有两种方法:
1.利用循环链表或者数组来模拟整个过程。
具体来讲,整个过程很明显就可以看成是一个循环链表删除节点的问题。当然,我们也可以用数组来代替循环链表来模拟整个计数以及出列的过程。此处只给出利用数组来模拟这个过程的解法,最终结果为最后一个出列的人的编号:
#include<iostream> #include<unordered_map> #include<queue> #include<cstring> #include<cstdlib> #include<cmath> #include<algorithm> #include<sstream> #include<set> #include<map> using namespace std; int main() { int n,m; cin>>n>>m; vector<int>rs(n); for(int i = 0 ; i < n; i++) rs[i] = i + 1;//对人物重新进行编号,从0开始 int cur_index = 0;//当前圆桌状态下的出列人的编号 int out_cnt = 0;//用以表示出列的人数 int cnt = n;//表示当前圆桌的总人数 while(out_cnt < n - 1)//当out_cnt等于n-1时,循环结束,此时圆桌师生最后一个人,即我们要的结果 { if(cur_index + m > cnt) { if((cur_index + m) % cnt == 0)//这种情况需要单独考虑,否则cur_index就变成负值了 cur_index = cnt - 1; else cur_index = (cur_index + m) % cnt - 1; } else cur_index = cur_index + m - 1; cnt--; out_cnt++; cout<<"当前出列的为:"<<*(rs.begin() + cur_index)<<endl; rs.erase(rs.begin() + cur_index);//从数组中删去需要出队的人员 } cout<<"最后一个出列的人物为 :"<<rs[0]<<endl; }
该方法的时间复杂度为O(n),空间复杂度为O(n),整个算法的基本流程还是比较清晰的,相当于每次循环更新cur_cnt、cnt和out_cnt这三个变量,当out_cnt == n-1时,此时出队的人数一共有n-1人,圆桌上只剩下一个人了,停止循环。此外,该算法有几点需要注意:
(1)首先,我们为什么要对用户进行重新编号(从0开始到n-1),在我看来,这是因为在整个循环过程中我们用到了对当前圆桌人数总数cnt进行了取余的操作,而取余的结果包括0到cnt -1,即包括0;如果编号是从1开始的话,在余数为0的时候需要特殊处理,而从0开始编号的话,一方面符合编程习惯(下标从0开始计数);另一方面面对取余操作不需要特殊处理。
(2)代码中的cur_index指的当前圆桌状态下需要出队的人的编号,即数到m的人的编号(此处的编号指的是重新编号的编号);由于cur_index的值表示的是重新编号后的编号,但它的初值表示的是最开始数数的那个人的编号(初始值的时候就不表示需要出队的人的编号了),由于题目要求的是从编号为1的人开始数数,并且其对应的新编号为0,故cur_index的初值为0。此外,在循环计算cur_index时,我们发现不论是哪种情况,cur_index更新值都有一个减1的操作;这是因为cur_index每次加m得到的值或者加m再取余得到的值,实际上是需要出队人员的原始编号(即从1开始到n结束的那个编号),而cur_index应该表示的是重新编号后的编号,而新编号比旧编号小1,所以需要减去1,这其实可以看成是一个规律。此处可以举个例子,例如:对于n=5,m=3;cur_index的初值为0,cur_index + 3 <5,所以cur_index + m = 3(不用进行取余操作了),如果不减1的话,3表示的是新编号,对应的旧编号就是4,而实际上应该出队的人员的编号是3,对应的新编号是2。
(3)数组rs的下标相当于新编号,而数组存储的内容相当于旧编号,rs每次删除的元素对应每次出队的人员的编号,在这里我们需要了解erase的原理,rs每删除一个元素时,被删除之后的元素就会前移,相当于新旧编号的对应关系也发生了变化,即下一个开始数数的人占据了之前出队人员的位置,它的新编号发生了变化;而对向量进行erase操作后元素移动的原理实质和圆桌人员移动的情况是一致的啊,然后结合cur_index进行操作(把vector进行erase操作后移动元素的原理看成是圆桌人员移动),所以说能利用vector代替循环链表模拟整个过程。
2.利用数学推导得出的公式直接求解。
方法1中利用了数组直接进行过程模拟,但空间复杂度比较高,下面给出一种更为常见的方法,即直接对整个过程归纳出一个数学公式来。公式的具体推导本文不详细描述,可参考:https://blog.csdn.net/wusuopubupt/article/details/18214999
上文中给出了公式$f(i)=[f(i-1) + m] \% i$ 其中$f(i)$表示的是当圆桌人数为$i$时,应该出队人员的编号
可用递归和迭代实现
三 问题变种