约瑟夫问题
题目描述
n个人围成一圈,从编号为1的开始从1~m报数,报到m的人离开,接着再从下一个人重新报数。这样反反复复,直到只剩下一个人,求编号为多少的人留下?
输入格式
两个整数n、m。n表示总人数,m表示报数的截止上限。
输出格式
一个整数,表示剩下的人的编号。
AC代码
链表版(模拟) O(nm)
#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; struct node { int next;//记录当前参与者的后一位的位置 int post;//记录当前参与者的前一位的位置 }; node circle[1000]; int main() { int num,exit_num,present=1;//定义:剩余人数num,报数上限exit_num,当前位置present cout<<"Please input the number of people: "<<endl; cin>>num; cout<<"Please input the exit number: "<<endl; cin>>exit_num; //初始化链表为一个圈 for(int i=1;i<=num;i++) { circle[i].next=i+1; circle[i].post=i-1; } circle[num].next=1;//最后一位的下一个为第一位 circle[1].post=num;//第一位的上一位为最后一位 while(num>1) { for(int i=1;i<exit_num;i++)//模拟报数 present=circle[present].next; circle[circle[present].post].next=circle[present].next;//对于退出者进行删除操作:上一位的下一位为当前的下一位;下一位的上一位为当前的上一位 circle[circle[present].next].post=circle[present].post; present=circle[present].next;//从当前的下一位再一次开始报数 num--;//更新剩余人数 } cout<<present<<endl; system("pause"); return 0; }
采用链表的优势就是:可以非常出色地维护一个环形的结构,并且避免了处理跳过不参与报数的位置,而且在效率上——由于直接跳过了退出的位置——相较于一般的数组更为优秀~
不过这个是下学期才学的知识......
数组版(模拟) O(nm)
#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; bool circle[1000]; int main() { int num,exit_num,present=1,cnt;//定义num为初始参与人数,exit_num为报数的上限,present为当前位置,cnt为剩余人数 cout<<"Please input the number of participants: "<<endl; cin>>num; cout<<"Please input the exit number"<<endl; cin>>exit_num; cnt=num;//初始化cnt为总人数 while(cnt>1) { for(int i=1;i<=exit_num;i++)//模拟报数 { if(circle[present]==true)//若当前位置的参与者已经退出,则不计入报数,还原i值 i--; else if(i==exit_num)//若当前参与者未退出,且报数已经达到上限,则令其退出 circle[present]=true; present=(present==num)?(1):(present+1);//更新当前位置为下一位 } cnt--;//更新剩余人数 } for(int i=1;i<=num;i++)//遍历整个数组,寻找剩下的唯一参与者 if(circle[i]==false) cout<<i<<endl; system("pause"); return 0; }
数组版的代码要相对复杂一点,原因在于我们不能对数组结构中的元素进行“删除”操作,这使得我们在遍历时(也就是模拟报数过程时)要考虑已经不参与报数的位置,在更新当前位置present时也要注意模拟环状的结构。另外,由于我们无法通过present的值直接确定答案,我们在最后需要单独从1~num遍历一次数组,找到尚未被标记的位置并输出。
递推版(数学技巧) O(n)
#include<iostream> #include<cstdlib> #include<cstdio> using namespace std; int main() { int num,exit_num,f=0;//定义总人数为num,报数的截止上限为exit_num cout<<"Please input the number of people: "<<endl; cin>>num; cout<<"Please input the exit number: "<<endl; cin>>exit_num; for(int i=1;i<=num;i++) f=(f+exit_num)%i; cout<<f+1<<endl; system("pause"); return 0; }
啧啧啧,是不是感觉清爽了不少?这个代码是如此的简洁,让我们来着重了解一下~
为方便叙述,我们先将num个人从0~(num-1)编号。
接着,我们不妨假设有num个人,我们很容易得出,第一个退出的人的编号肯定是k=(exit_num−1)%num。当编号为k的人退出后,接下来我们可以将问题看作:从原编号为k+1的人开始重新编号,然后就是一个人数为num−1个人的子问题了。这里其实包含了递归的思想,本题可用递归法求解~ (无论是递归还是递推,其实两者思路基本一致)
当然,这num−1个人的答案并不是最终答案,因为它是重新编号后的答案,所以我们要将其恢复原编号。
怎么恢复呢???我们发现,新编号为0的人前面有exit_num−1个人,也就是说,新的编号相对于原来的编号左移了exit_num,所以最终的答案应该要加上exit_num。
那么我们就可以得到递推公式:设f[n]为总人数为n时最后剩下的人的编号,则有f[n]=(f[n−1]+exit_num)%n(模n是为了防止计算所得值溢出),并且易得f(1)=0,可借此进行循环递推。
当n=num时,f[num]就是最后剩余人的编号。由于我们是按0~(num-1)编号,而题目要求1~num编号,因此答案应为f[num]+1。
递归实现 O(n)
#include<iostream> #include<cstdlib> #include<cstdio> using namespace std; int work(int n,int m) { if(n==1) return 0; return (work(n-1,m)+m)%n; } int main() { int num,exit_num;//定义总人数为num,报数的截止上限为exit_num cout<<"Please input the number of people: "<<endl; cin>>num; cout<<"Please input the exit number: "<<endl; cin>>exit_num; int ans=work(num,exit_num)+1; cout<<ans<<endl; system("pause"); return 0; }