1. 程式人生 > 實用技巧 >約瑟夫環--數學高效率解法

約瑟夫環--數學高效率解法

問題描述

有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,m資料量很小的時候,我們可以用迴圈連結串列模擬約瑟夫環的過程。當模擬到人數等於1的時候,輸出剩下的人的序號即可。這種方法往往實現起來比較簡單,而且也很容易理解。但是時間複雜度卻是很糟糕的,達到了O(n*m),因為即使2個人,也要模擬K遍,存在著大量的浪費。在n,m比較大的時候(n*m達到10^8或者更大),那麼要得出結果往往需要耗費很長的時間,但是我們可以運用一點數學上的技巧,將最後結果推匯出來。

為了討論方便,先把問題稍微改變一下,並不影響原意:
問題描述: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; }