1. 程式人生 > >Josephus(約瑟夫)環問題的數學方法,使用遞推公式。

Josephus(約瑟夫)環問題的數學方法,使用遞推公式。

無論是用連結串列實現還是用陣列實現都有一個共同點:要模擬整個遊戲過程,不僅程式寫起來比較煩,而且時間複雜度高達O(nm),當n,m非常大(例如上百萬,上千萬)的時候,幾乎是沒有辦法在短時間內出結果的。我們注意到原問題僅僅是要求出最後的勝利者的序號,而不是要讀者模擬整個過程。因此如果要追求效率,就要打破常規,實施一點數學策略。

為了討論方便,先把問題稍微改變一下,並不影響原意:問題描述:n個人(編號0~(n-1)),從0開始報數,報到m-1的退出 ,剩下的人繼續從0開始報數。求勝利者的編號。我們知道第一個人(編號一定是(m-1)%n) 出列之後,剩下的n-1個人組成了一個新的約瑟夫環(以編號為k=m%n的人開始):

 k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2  並且從k開始報0。  序列1: 0, 1, 2, 3 … n-2, n-1  序列2: 0, 1, 2, 3 … k-1, k+1, …, n-2, n-1  序列3: k, k+1, k+2, k+3, …, n-2, n-1, 1, 2, 3,…, k-2,  序列4:0, 1, 2, 3 …, 5, 6, 7, 8, …, n-3, n-2       變換後就完完全全成為了(n-1)個人報數的子問題,假如我們知道這個子問題的解:例如x是最終的勝利者,那麼根據上面這個表把這個x變回去不剛好就是n個人情況的解嗎?!!變回去的公式很簡單,相信大家都可以推出來:
 ∵ k=m%n;  ∴ x' = x+k = x+ m%n ; 而 x+ m%n 可能大於n  ∴x'= (x+ m%n)%n = (x+m)%n  得到 x‘=(x+m)%n        如何知道(n-1)個人報數的問題的解?對,只要知道(n-2)個人的解就行了。(n-2)個人的解呢?當然是先求(n-3)的情況 ---- 這顯然就是一個倒推問題!好了,思路出來了,下面寫遞推公式:  令f表示i個人玩遊戲報m退出最後勝利者的編號,最後的結果自然是f[n].  遞推公式:  f[1]=0;  f[i]=(f[i-1]+m)%i; (i>1)      有了這個公式,我們要做的就是從1-n順序算出f的數值,最後結果是f[n]。我們輸出f[n]由於是逐級遞推,不需要儲存每個,程式也是異常簡單:(注意編號是0 -- n-1)
public class JOSEPHUS_Deep {
   public static void main(String[] args) {
    int n, m, i, s=0; 
        m=5;n=6;
 
    for (i=2; i<=n; i++) {
//因為是從兩個人開始計算,所以i=2,而不是0
      s=(s+m)%i;
     }
     System.out.println("The winner is "+(s+1));
   }  
}


開始我看這段文字的時候都沒怎麼懂,可能是數學太差的原因吧!

   後來我就一句一句來看,結合例項:0 1 2 3 4 5  m=5.

移除4過後,的狀態變成了:5 0 1 2 3.     k=m%n=5

如下表

現在我們把他們的編號做一下轉換:

k k-5 k-4 k-3 k-2
X’ 5 0 1 2 3
轉換關係 :X’=(x+k)%n(此例中:n=6)
x 0 1 2 3 4

於是呢,n個人的問題就變成了n-1個人的問題。

遞推公式:

  f[1]=0; //就是說如果只有一個人,那麼最後的勝利者就是他,他的編號為0

  f[i]=(f[i-1]+m)%i; (i>1) //如果不止一個人,有i個人,那麼就需要求出i-1個人的最後勝利的結果f[i-1],然後呢,利用遞推公式x‘=(x+m)%n 就可以知道第i個的最後結果了。

例如:程式中有這樣兩句:s=0;s=(s+m)%i;

就是說:s=0表示只有一個人的結果。

計算第2個人的結果需要用到第1個人的結果

當i=2,m=5,得到s=1,表示2個人轉圈的結果

當i=3,m=5,得到s=0,表示3個人轉圈的結果。

最後,由於我們選擇6個人轉圈編號一般習慣為1 2 3 4 5 6 ,並不是0 1 2 3 4 5 6

所以輸出結果需要s+1。

我現在想不懂的是,怎麼來確定第i個人的出列編號……它列出是的最後的人!

 k --> 0  k+1 --> 1  k+2 --> 2  ...  ...  k-3 --> n-3  k-2 --> n-2