標籤:
約瑟夫問題(有時也稱為約瑟夫斯置換,是一個出現在電腦科學和數學中的問題。在電腦編程的演算法中,類似問題又稱為約瑟夫環。又稱“丟手絹問題”.)
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 --> 0k+1 --> 1k+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; }
運行結構如下:
約瑟夫問題、約瑟夫環