约瑟夫环--数学高效率解法
问题描述
有n个囚犯站成一个圆圈,准备处决。首先从一个人开始报数,报到m的人被处死,剩下n-1个人继续这个过程,直到最终只剩下一个人留下。问题是:给定了n和m,一开始要站在什么位置才能避免被处决?
#include<iostream> using namespace std; /************约瑟夫问题****************/ typedef struct CLinkList { int data; struct CLinkList *next; }node; int main() { ///建立循环链表 node *L,*r,*s; L = new node; r =L; int n,k;
cin>>n>>k; for(i = 1;i<=n;i++) //尾插法建立链表 { s = new node; s->data = i; r->next = s; r= s; } r->next =L->next; //让最后一个结点指向第一个有数据结点 node *p; p = L->next; delete L; //删除第一个空的结点 ///模拟解决约瑟夫问题 while(p->next != p) //判断条件:因为最后肯定剩下一个人, 循环链表的最后一个数据的next还是他本身 { for(i = 1;i<k-1;i++) { p = p->next; //每k个数死一个人 } cout<<p->next->data<<"->"; p->next = p->next->next; //将该节点从链表上删除。 p = p->next; } cout<<p->data<<endl; return 0; }
为了讨论方便,先把问题稍微改变一下,并不影响原意:
问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。
我们知道第一个出列的人编号一定是 m%n -1。那么剩下的n-1个人便可以看做组成了一个新的约瑟夫环(以编号为k=m%n的人开始):
k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2并且从k开始报0。
现在我们把n-1个人的时候的编号做一下转换(全部减去k):
k --> 0
k+1 --> 1
k+2 --> 2
...
...
k-2 --> n-2
变换后就完完全全成为了(n-1)个人报数的子问题!(这不是让我们倒推吗?!)
假如我们知道这个子问题的解:例如A[n-1]是在n-1个人中的最终胜利者,那么根据上面对A[n-1]进行转换 便刚好就是n个人情况的解。变回去的公式很简单,如下:
A[n]=(A[n-1] +k)%n。(k是在n个人中第一个出列的人的编号,此处这个公式涉及了上面叙述到的编号的转换,需要搞懂了才能理解)
由于k=m%n,那么公式也可以写为A[n]=(A[n-1] +m)%n。为什么呢?很容易理解,7%3=1 , 1%3=1。
如此也是得出了递推公式:
A[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是A[n]
A[1]=0;
A[2]=(A[1]+m)%2;
A[i]=(A[i-1]+m)%i; (i>1)
有了这个公式,我们要做的就是从1-n顺序算出A[i]的数值,最后结果是A[n]。因为实际生活中编号总是从1开始,我们输出A[n]+1
由于是逐级递推,不需要保存每个A[i],于是便得到了最终的程序:
#include<iostream> using namespace std; int main() { int n,m;
cin>>n>>m; int winner=0;//总人数n,数到m的倍数离开,最后的人winner for(int i=2;i<=n;i++) winner=(winner+m)%i; cout<<"Winner:"<<winner+1<<endl; }