约瑟夫问题(有时也称为约瑟夫斯置换,是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。又称“丢手绢问题”.)
1问题来历编辑
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。[1]
17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。
*问题分析与算法设计
约瑟夫问题并不难,但求解的方法很多;题目的变化形式也很多。这里给出一种实现方法。
题目中30个人围成一圈,因而启发我们用一个循环的链来表示,可以使用结构数组来构成一个循环链。结构中有两个成员,其一为指向下一个人的指针,以构成环形的链;其二为该人是否被扔下海的标记,为1表示还在船上。从第一个人开始对还未扔下海的人进行计数,每数到9时,将结构中的标记改为0,表示该人已被扔下海了。这样循环计数直到有15个人被扔下海为止。
一般形式编辑
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
解决方法
方法一:
用链表环表示:首先构造链表环,之后删除第m个元素,往后继续数。用shoot表示1~m的循环。
int josephus1(int n,int m) { if (n<1||m<1) { return -1; } listNode *head=creat(n); int shoot=1; listNode *p=head,*s=p; while(length(p)>1) { if (shoot++==m) { s=p->next; remove(head,p->val); cout<<p->val<<"->"; shoot=1; p=s; } else { head=p; p=p->next; } } cout<<head->val<<endl; return head->val; } listNode *creat(int n) { listNode *head,*p,*s; head=(listNode*)malloc(sizeof(listNode)); p=head; int i=1; while(i<=n) { s=(listNode*)malloc(sizeof(listNode)); s->val=i; p->next=s; p=s; i++; } head=head->next; p->next=head; return head; } listNode *remove(listNode *head,int val) { listNode *p,*s; p=head; while (p->val!=val) { s=p; p=p->next; } if (p->val==val) { s->next=p->next; return head; } } int length(listNode* head) { if (head==NULL) return 0; if (head->next==head) return 1; int length=0; listNode *p=head; while (p->next!=head) { p=p->next; length++; } return length+1; }
方法二:
用C++,list容器模拟环,其中没有next。用迭代器++实现,代码如下:
int josephus2(int n,int m) { if (n<1||m<1) { return -1; } list<int> li; for (int i=1;i<=n;i++) { li.push_back(i); } list<int> ::iterator iter=li.begin(); while (li.size()>1) { for (int i=1;i<m;i++) { iter++; if (iter==li.end()) { iter=li.begin(); } } list<int> ::iterator next=++iter; if (next==li.end()) { next=li.begin(); } --iter; cout<<*(iter)<<"->"; li.erase(iter); iter=next; } cout<<*(iter)<<endl; return *(iter); }
方法三:
无论是用链表实现还是用数组实现都有一个共同点:要模拟整个游戏过程,不仅程序写起来比较烦,而且时间复杂度高达O(nm),空间复杂度为O(n),当n,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。我们注意到原问题仅仅是要求出最后的胜利者的序号,而不是要读者模拟整个过程。因此如果要追求效率,就要打破常规,实施一点数学策略。
为了讨论方便,先把问题稍微改变一下,并不影响原意:
问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。
我们知道第一个人(编号一定是(m-1) mod n) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m mod 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) mod n
如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
令f表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]
递推公式
f[1]=0;
f=(f+m) mod i; (i>1)
有了这个公式,我们要做的就是从1-n顺序算出f的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1
由于是逐级递推,不需要保存每个f,程序也是异常简单:
代码如下:
int josephus(int n,int m) { if (n<1||m<1) { return -1; } int f=0; for (int i=1;i<=n;i++) { f=(f+m)%i; } return f+1; }
此方法时间复杂度为O(n),空间复杂度为O(1)。缺点是无法实现过程输出,只能求得最后一个元素的值。
完整代码如下;
其中 int josephus2(int n,int m)和 int josephus3(int n,int m)是利用list容器实现的两种思想相同,不同操作的代码。
#include <iostream> #include <list> #include<iterator > using namespace std; struct listNode { int val; listNode *next; }; int josephus(int n,int m); int josephus1(int n,int m); listNode *creat(int n); listNode *remove(listNode *head,int val); int length(listNode* head); int josephus2(int n,int m); int josephus3(int n,int m); void main() { int n=41; int m=3; cout<<josephus(n,m)<<endl; cout<<josephus1(n,m)<<endl; cout<<josephus2(n,m)<<endl; cout<<josephus3(n,m)<<endl; } int josephus(int n,int m) { if (n<1||m<1) { return -1; } int f=0; for (int i=1;i<=n;i++) { f=(f+m)%i; } return f+1; } int josephus1(int n,int m) { if (n<1||m<1) { return -1; } listNode *head=creat(n); int shoot=1; listNode *p=head,*s=p; while(length(p)>1) { if (shoot++==m) { s=p->next; remove(head,p->val); cout<<p->val<<"->"; shoot=1; p=s; } else { head=p; p=p->next; } } cout<<head->val<<endl; return head->val; } listNode *creat(int n) { listNode *head,*p,*s; head=(listNode*)malloc(sizeof(listNode)); p=head; int i=1; while(i<=n) { s=(listNode*)malloc(sizeof(listNode)); s->val=i; p->next=s; p=s; i++; } head=head->next; p->next=head; return head; } listNode *remove(listNode *head,int val) { listNode *p,*s; p=head; while (p->val!=val) { s=p; p=p->next; } if (p->val==val) { s->next=p->next; return head; } } int length(listNode* head) { if (head==NULL) return 0; if (head->next==head) return 1; int length=0; listNode *p=head; while (p->next!=head) { p=p->next; length++; } return length+1; } int josephus2(int n,int m) { if (n<1||m<1) { return -1; } list<int> li; for (int i=1;i<=n;i++) { li.push_back(i); } list<int> ::iterator iter=li.begin(); while (li.size()>1) { for (int i=1;i<m;i++) { iter++; if (iter==li.end()) { iter=li.begin(); } } list<int> ::iterator next=++iter; if (next==li.end()) { next=li.begin(); } --iter; cout<<*(iter)<<"->"; li.erase(iter); iter=next; } cout<<*(iter)<<endl; return *(iter); } int josephus3(int n,int m) { if (n<=1) { return -1; } list<int> li; for (int i=1;i<=n;i++) { li.push_back(i); } int shoot=1; list<int> ::iterator next=li.begin(); int last=0; list<int>::iterator iter=li.begin(); for (iter=li.begin();li.size()>1;) { if (shoot++==m) { next=++iter; if (next==li.end()) { next=li.begin(); } --iter; last=*iter; cout<<last<<"->"; li.erase(iter); iter=next; shoot=1; } else { iter++; if (iter==li.end()) { iter=li.begin(); } } } last=*iter; cout<<last<<endl; return last; }
运行结构如下: