1. 程式人生 > >約瑟夫環O(N)和O(M*N)演算法詳解

約瑟夫環O(N)和O(M*N)演算法詳解

問題描述:
已知n個人(以編號1,2,3…n分別表示)圍坐在一張圓桌周圍。從編號為1的人開始報數,數到m的那個人出列;他的下一個人又從1開始報數,數到m的那個人又出列;依此規律重複下去,直到圓桌周圍的人全部出列,求最後一個出列人的編號。

該問題可以用陣列或者迴圈連結串列模擬,因為都是直接模擬,需要迴圈n次,每次數m次,所以時間複雜度都是O(n*m)。雖然複雜度比O(n)大,但是可以求出出圈的編號順序。也正是因為該方法將每次出圈的人都找了出來,所以做了很多無用操作,複雜度才會高達O(n*m)。
下面是陣列和連結串列方法的程式碼:
題意:n個人,從第k個人開始數1,數到m的人剔除,輸出剔除的順序。

#include <iostream>
#include<cstdio>
#include<ctime>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define MAXN 10000
int a[MAXN];
int N,k,m;
bool InCircle[MAXN];
typedef struct _node{
    struct _node* prev;
    struct _node* next;
    int number;
}node;
node* MakeDLL(int
n){ node *head = new node;/*Memory allocated to the head node*/ node *tail; int i; head->next = head; head->prev = head; head->number = 1; tail = head; for(i=2;i<=n;i++){ node *p = new node;/*Memory allocated to a new node*/ p->number = i; p->next = tail->next; p->prev = tail; tail->next = p; tail = p; head->prev = tail; } return
head; } void JosephDLL(node* head, int k, int m){ int i; node *NodeToDelete,*q; NodeToDelete = head; for(i=1; i<k; i++)/*Get the element numbered k*/ NodeToDelete = NodeToDelete->next; while(head->next != head){ for(i=1; i<m; i++){ NodeToDelete = NodeToDelete->next;/*Count m times*/ } /*Delete operation,begin*/ q = NodeToDelete->next; q->prev = NodeToDelete->prev; NodeToDelete->prev->next = q; printf("%d ",NodeToDelete->number); if(NodeToDelete == head){/*If the member to be deleted is the head, redefine the head.*/ head = q; } free(NodeToDelete); /*Delete operation,end*/ NodeToDelete = q;/*Count from the next node*/ } printf("%d\n",head->number); } void JosephArr(){ int tmpN=N,i=k; while(tmpN--){ int tmpM=m; for(;tmpM;i++){/*Count m times*/ if(InCircle[i%(N+1)])/*If the member is in the circle, count on it(That is the operation tmpM--) */ tmpM--; } i--; cout<<i%(N+1)<<" "; InCircle[i%(N+1)]=0;/*delete the member i%(N+1) from the circle*/ } } int main(){ freopen("input.txt","r",stdin); freopen("output.txt","w",stdout); cout<<"Input N,k,m:"<<endl; cin>>N>>k>>m; for(int i=1;i<=N;i++){ a[i]=i; InCircle[i]=1; } cout<<"Base on the array : "; JosephArr();/*Base on the array*/ for(int i=1;i<=N;i++){ InCircle[i]=1; } node* head; head = MakeDLL(N); cout<<endl<<"Base on the doubly link list : "; JosephDLL(head,k,m);/*Base on the doubly linked lists*/ return 0; }

O(n)方法運用動態規劃,或者說遞推,或者說找規律,都行吧。雖然複雜度低,但是不能求出出圈的順序。如果需要求出出圈的順序,複雜度依然是O(n*m)。
該方法遞推公式網上很多都能找到,但是卻極少人給出遞推過程。
我們發現,當從圈出來一個人之後,後一個人需要從1開始重新計數直到計到m。例如有n個人(1,2,3,… ,n-2,n-1),第m個人出來之後,需要從第m+1開計數。如果從m+1個人重新編號(即m+1個人編號為0,m+1為2,… …),那麼對剩下的n-1個人的操作跟開始有n個人的時候操作就是一樣的。
這就是可以運用動態規劃的原因:n的情況跟n-1的情況存在著某種關係。而我們只需用動態方程將這種關係表達出來,問題就可以解決。
下面顯示瞭如何進行重新編號:

這裡寫圖片描述
設n個人圍成一個圈的時候出列是第X’號,n-1個人圍成一個圈的時候出列的是X號,並假設X已經求出來,那麼根據上面的遞推公式就可以反推求X’: 因為X=(X’*n+n-m)%n=X’*n%n+n%n-m%n=X’-m%n
所以X’=X+m%n=(X+m)%n
如果定義:dp[n]表示圈裡有n個人的時候最後剩下那個人的編號,答案就是dp[n]。
那麼就有dp[n]=(dp[n-1]+m)%n
但是dp[n-1]並不知道,所以dp[n]也沒法求出。當然dp[n-1]也需要通過動態方程求出:
dp[n-1]=(dp[n-2]+m)%n。
同樣dp[n-2],dp[n-3]….分別需要用dp[n-3],dp[n-4]求出。
最後dp[2]需要dp[1]求出,然而dp[1]=0是已知的。
所以從dp[2]開始依次可以求出dp[2],dp[3],dp[4]…dp[n]。
那麼問題就解決了。

#include <iostream>
#include<cstdio>
#include<ctime>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define MAXN 10000
int dp[MAXN];
int joseph(int m, int n) {
    dp[1] = 0;
    for(int i = 2; i <= n; i++){
        dp[i] = (dp[i-1] + m) % i;
    }
    return dp[n];
}
int main(){
    freopen("input.txt","r",stdin);
    freopen("output.txt","w",stdout);
    int m, n;
    cin >> m >> n;
    cout << joseph(m, n) << endl;
}