猴子選大王--約瑟夫問題淺析
猴子選大王問題是一個十分經典的算法問題,這個問題是這樣的:一堆猴子都有編號,編號是1,2,3 ...m,這群猴子(m個)按照1-m的順序圍坐一圈,從第1開始數,每數到第N個,該猴子就要離開此圈,這樣依次下來,直到圈中只剩下最后一只猴子,則該猴子為大王。
這個問題要解決起來並不難,但求解的方法很多;題目的變化形式也很多,而我們統稱這類問題為約瑟夫問題。這類題目基本的描述為:N個人圍成一圈,從第一個開始報數,第M個將被殺掉,最后剩下一個,其余人都將被殺掉。例如N=6,M=5,被殺掉的順序是:5,4,6,2,3,1。下面我們先來分析一下解決這類問題的幾個步驟。
(1)由於對於每個人只有死和活兩種狀態,因此可以用布朗型數組標記每個人的狀態,可用true表示死,false表示活。
(2)開始時每個人都是活的,所以數組初值全部賦為false。
(3)模擬殺人過程,直到所有人都被殺死為止。
題目中N個人圍成一圈,因而啟發我們用一個循環的鏈來表示,可以使用數組結構來構成一個循環鏈表。結構中有兩個成員,其一為指向下一個人的指針,以構成環形的鏈;其二為該人是否被殺死的標記,為1表示還存活。從第一個人開始對還存活的人進行計數,每數到M時,將結構中的標記改為0,表示該人已被殺死。這樣循環計數直到有15個人被殺死為止。
但是,無論是用鏈表實現還是用數組實現都有一個共同點:要模擬整個游戲過程,不僅程序寫起來比較煩,而且時間復雜度高達O(nm),當n,m非常大(例如上百萬,上千萬)的時候,幾乎是沒有辦法在短時間內出結果的。我們注意到原問題僅僅是要求出最后的勝利者的序號,而不是要讀者模擬整個過程。因此如果要追求效率,就要打破常規,實施一點數學策略。
為了討論方便,先把問題稍微改變一下,並不影響原意:
問題描述:n個人(編號0~(n-1)),從0開始報數,報到(m-1)的退出,剩下的人繼續從0開始報數。求勝利者的編號。
我們知道第一個人(編號一定是(m-1)) 出列之后,剩下的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[i]=(f[i-1]+m) mod i; (i>1)
有了這個公式,我們要做的就是從1-n順序算出f的數值,最后結果是f[n]。因為實際生活中編號總是從1開始,我們輸出f[n]+1
由於是逐級遞推,不需要保存每個f,程序也是異常簡單:
1 package com.jredu100.ch4.test; 2 3 import java.util.Scanner; 4 /** 5 * 約瑟夫問題 6 * @author ymyBlogs 7 * 8 */ 9 public class Test11 { 10 11 public static void main(String[] args) { 12 // TODO Auto-generated method stub 13 int s=0; 14 int M=3; 15 Scanner sc=new Scanner(System.in); 16 System.out.println("請輸入人數:"); 17 int n=sc.nextInt(); 18 for(int i=2;i<=n;i++){ 19 s=(s+M)%i; 20 } 21 System.out.println("最終位置為:"); 22 System.out.println(s+1); 23 } 24 25 }
這個算法的時間復雜度為O(n),相對於模擬算法已經有了很大的提高。算n,m等於一百萬,一千萬的情況不是問題了。可見,適當地運用數學策略,不僅可以讓編程變得簡單,而且往往會成倍地提高算法執行效率。