本文参考了http://blog.csdn.net/wuzhekai1985,http://blog.csdn.net/kangroger/article/details/39254619等文章,在此一并致谢。
约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后 结果+1即为原问题的解。
我们最容易想到的解法就是使用链表、数组等数据结构来模拟整个过程。下面分别给出数组、自定义链表和使用STL中的list的解法:
int JosephusProblem_Solution1(int n,int m) { bool *p=new bool[n]; for(int i=0;i<n;i++) *(p+i)=true; int res,count=0; for(int i=0,j=0;;i++) { if(*(p+i)) { j++; if(j==m) { *(p+i)=false; j=0; count++; } if(count==n) { res=i; break; } } if(i==n-1) i=-1; } delete []p; return res; }
struct ListNode { int num; ListNode *next; ListNode(int n=0,ListNode *p=NULL) {num=n;next=p;} }; int JosephusProblem_Solution2(int n,int m) { if(n<1||m<1) return -1; ListNode *pHead=new ListNode(); ListNode *pCurrentNode=pHead; ListNode *pLastNode=NULL; unsigned i; for(i=1;i<n;i++) { pCurrentNode->next=new ListNode(i); pCurrentNode=pCurrentNode->next; } pCurrentNode->next=pHead; pLastNode=pCurrentNode; pCurrentNode=pHead; while(pCurrentNode->next!=pCurrentNode) { for(i=0;i<m-1;i++) { pLastNode=pCurrentNode; pCurrentNode=pCurrentNode->next; } pLastNode->next=pCurrentNode->next; delete pCurrentNode; pCurrentNode=pLastNode->next; } int result=pCurrentNode->num; delete pCurrentNode; return result; }
int JosephusProblem_Solution3(int n,int m) { if(n<1||m<1) return -1; list<int> listInt; unsigned i; for(i=0;i<n;i++) listInt.push_back(i); list<int>::iterator iterCurrent=listInt.begin(); while(listInt.size()>1) { for(i=0;i<m-1;i++) { if(++iterCurrent==listInt.end()) iterCurrent=listInt.begin(); } list<int>::iterator iterDel=iterCurrent; if(++iterCurrent==listInt.end()) iterCurrent=listInt.begin(); listInt.erase(iterDel); } return *iterCurrent; }
上述方法的效率很低,其时间复杂度为O(mn)。当n和m很大时,很难在短时间内得出结果。不过好处就是可以给出n个人出圈的次序。只要在删除前保存一下即可。在程序设计竞赛中我们往往只需要求出最后留下的编号,可以利用递推关系寻求更简便的解法。
首先,第一个出列的人编号一定是m%n-1,之后剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%n的人开始):
k k+1 k+2 ... n-2 n-1 0 1 2 ... k-2并且从k开始报0。
现在我们把他们的编号做一下转换:
k -->0
k+1 --> 1
k+2 --> 2
...
...
k-2 --> n-2
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去刚好就是n个人情况的解。变回去的公式很简单:x'=(x+k)%n
如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况——这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]。
递推公式:
f[1]=0;
f[i]=(f[i-1]+m)%i; (i>1)
POJ3517:第一次报的是m,以后每次报的都是k,简单逆推一下即可。
//POJ3517 #include<stdio.h> int m,n,k; int ans(int num) { if(num==1) return 0; return (k+ans(num-1))%num; } int main() { scanf("%d %d %d",&n,&k,&m); while(n) { printf("%d\n",(ans(n-1)+m)%n+1); scanf("%d %d %d",&n,&k,&m); } return 0; }其中(ans(n-1)+m)%n模拟的是第一次报数的情况,ans函数模拟的是以后每次报数的情况,加1是因为原题中是从1开始的。
POJ2359:对于输入的字符串,从开始依次进行报数,当报到N=1999时,删除对应的字符,字符串可以看作是首尾相连的环,直到剩余最后一个字符。直接套用公式即可。
//POJ2359 #include<iostream> using namespace std; const int m=1999; char str[30005]; int main() { char ch; int k=0,s=0; while((ch=getchar())!=EOF) { if(ch!='\n') str[k++]=ch; } for(int i=2;i<=k;i++) s=(s+m)%i; if(str[s]=='?') printf("Yes\n"); else if(str[s]==' ') printf("No\n"); else printf("No comments\n"); return 0; }POJ1781:m值固定为2,也就是每隔一个人死一个。很显然,当n值为2的k次幂时,剩下的总是第一个人。
我们试着用推导最开始的递推公式的办法进一步寻找一下规律。
假设有8个人,分别是1 2 3 4 5 6 7 8,第一轮后剩下1 3 5 7 ,给它们重新编号为1 2 3 4 ,得出递推关系式:
f(2n)=2f(n)-1
假设有9个人,分别是1 2 3 4 5 6 7 8 9,第一轮后剩下1 3 5 7 9,由于1接下来也会被杀死,不妨计为3 5 7 9,给它们重新编号为1 2 3 4,得出递推关系式:
f(2n+1)=2f(n)+1
我们依次计算有n个人时最后剩下的人的编号,寻找规律:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
1 1 3 1 3 5 7 1 3 5 7 9 11 13 15 1 3 5 7 9 11
通项公式为f(2n+k)=2k+1。详细证明可以参考《具体数学》。
//POJ1781 #include<iostream> using namespace std; int main() { char str[10]; long long a,b,c,d; while(scanf("%s",str)!=EOF) { if(str[0]=='0'&&str[1]=='0'&&str[3]=='0') break; a=(str[0]-'0')*10+str[1]-'0'; for(b=1;b<=str[3]-'0';b++) a=a*10; c=1; while(1) { if(c>a)break; c=2*c; } if(c!=1) c=c/2; a=a-c; printf("%lld\n",2*a+1); } return 0; }POJ2244:从第一个人报数并被杀死,最后剩下第二个人,求最小的m值。从2开始枚举m即可。
//POJ2244 #include<iostream> using namespace std; int n; int last(int n,int m) { int ret=0; for(int i=2;i<=n;i++) ret=(ret+m)%i; return ret; } int main() { while(scanf("%d",&n),n) { int i=2; while(last(n-1,i)!=0) i++; printf("%d\n",i); } return 0; }为什么last(n-1,i)要等于0呢,因为第一个最开始就被杀死,所以我们把第二个人当成第一个人再开始递推的。
POJ1012:一共有2k个人,求m使得前k个人存活,后k个人死亡。只要前k个人没有死那么每次重新编号时他们的原有编号就不会改变。在这里打表保存每个k值对应的m值以免超时。很显然,m的最小取值是k+1。不同之处在于这道题需要正向模拟这个过程,而前面的递推公式是逆向模拟的。
//POJ1012 #include<iostream> using namespace std; int main() { int k; int Joseph[14]={0}; while(cin>>k) { if(!k) break; if(Joseph[k]) { cout<<Joseph[k]<<endl; continue; } int m=k+1; int n=2*k; int ans[30]={0}; for(int i=1;i<=k;i++) { ans[i]=(ans[i-1]+m-1)%(n-i+1); if(ans[i]<k) { i=0; m++; } } Joseph[k]=m; cout<<m<<endl; } return 0; }