約瑟夫問題

此處還有一個不錯的JAVA版本 http://wayfoon.javaeye.com/blog/432527

 

 

約瑟夫問題的來歷

據說著名猶太歷史學家 Josephus有過以下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡爲止。然而Josephus 和他的朋友並不想遵從,Josephus要他的朋友先假裝遵從,他將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。

 

17世紀的法國數學家加斯帕在《數目的遊戲問題》中講了這樣一個故事:15個教徒和15 個非教徒在深海上遇險,必須將一半的人投入海中,其餘的人才能倖免於難,於是想了一個辦法:30個人圍成一圓圈,從第一個人開始依次報數,每數到第九個人就將他扔入大海,如此循環進行直到僅餘15個人爲止。問怎樣排法,才能使每次投入大海的都是非教徒。

 

*問題分析與算法設計

 

約瑟夫問題並不難,但求解的方法很多;題目的變化形式也很多。這裏給出一種實現方法。

 

題目中30個人圍成一圈,因而啓發我們用一個循環的鏈來表示。可以使用結構數組來構成一個循環鏈。結構中有兩個成員,其一爲指向下一個人的指針,以構成環形的鏈;其二爲該人是否被扔下海的標記,爲1表示還在船上。從第一個人開始對還未扔下海的人進行計數,每數到9時,將結構中的標記改爲0,表示該人已被扔下海了。這樣循環計數直到有15個人被扔下海爲止。

 


約瑟夫問題的一般形式:

約瑟夫問題是個有名的問題:N個人圍成一圈,從第一個開始報數,第M個將被殺掉,最後剩下一個,其餘人都將被殺掉。例如N=6,M=5,被殺掉的人的序號爲5,4,6,2,3。最後剩下1號。

 

假定在圈子裏前K個爲好人,後K個爲壞人,你的任務是確定這樣的最少M,使得所有的壞人在第一個好人之前被殺掉。

 

C++代碼示例:

 

#include<iostream>

 

using namespace std;

 

void main()

 

{

 

int n,m,a[101],k,i,j,num; //計數器是從1開始的,所以100個人用101

 

cout<<"請輸入參加遊戲的玩家人數(不超過100人):";

 

cin>>n;

 

cout<<"----------------------------------------"<<endl;

 

if(n>100)

 

{

 

cout<<"玩家太多,請重新登陸此程序!"<<endl;

 

return;

 

}

 

cout<<"輸入遊戲中要玩的數字:";

 

cin>>m;

 

cout<<"----------------------------------------"<<endl;

 

for(i=1;i<=n;i++)

 

{

 

a【i】=1;//注意百度百科裏不讓使用ASCII裏的方括號,這裏是中文字符集裏的方括號,

 

}

 
j=0;
 
k=0;
 
for(i=1;i<=n;i++){
 
if(a【i】==1){
 
j=j+a【i】;
 
if(j==m)
 
{
 
j=0;
 
a【i】=0;
 
k++;
 
}
 
if(k==n){
 
num=i;
 
break;
 
}
 
}
 
if(i==n)
 
i=0;
 
}
 
cout<<"最後獲勝的玩家是第 "<<num<<" 號玩家!"<<endl;
 
cout<<"----------------------------------------"<<endl;
 
}
 

 

 

寫完密碼約瑟夫就想到原來看到約瑟夫問題的一個數學解法 很巧妙很簡單 不過只能推出最後一個出列的人

 

無論是用鏈表實現還是用數組實現都有一個共同點:要模擬整個遊戲過程,不僅程序寫起來比較煩,而且時間複雜度高達O(nm),當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 --> 0

 

k+1 --> 1

 

k+2 --> 2

 

...

 

...

 

k-2 --> n-2

 

k-1 --> n-1

 

變換後就完完全全成爲了(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,程序也是異常簡單:

 

c++

 

#include <stdio.h>

 

int main()

 

{

 

int n, m, i, s=0;

 

printf ("N M = "); scanf("%d%d", &n, &m);

 

for (i=2; i<=n; i++) s=(s+m)%i;

 

printf ("The winner is %d\n", s+1);

 

}

 

pascal

 

var n,m,i,s:integer;

 

begin

 

write('N M =');

 

read(n,m);

 

for i:=2 to n do

 

s:=(s+m) mod i;

 

writeln('The winner is ',s+1);

 

end.

 

這個算法的時間複雜度爲O(n),相對於模擬算法已經有了很大的提高。算n,m等於一百萬,一千萬的情況不是問題了。可見,適當地運用數學策略,不僅可以讓編程變得簡單,而且往往會成倍地提高算法執行效率。

 

約瑟夫問題10e100版(from vijios)

 

描述 Description

 

n個人排成一圈。從某個人開始,按順時針方向依次編號。從編號爲1的人開始順時針“一二一”報數,報到2的人退出圈子。這樣不斷循環下去,圈子裏的人將不斷減少。由於人的個數是有限的,因此最終會剩下一個人。試問最後剩下的人最開始的編號。

 

輸入格式 Input Format

 

一個正整數n,表示人的個數。輸入數據保證數字n不超過100位。

 

輸出格式 Output Format

 

一個正整數。它表示經過“一二一”報數後最後剩下的人的編號。

 

樣例輸入 Sample Input

 

9

 

樣例輸出 Sample Output

 

3

 

時間限制 Time Limitation

 

各個測試點1s

 

註釋 Hint

 

樣例說明

 

當n=9時,退出圈子的人的編號依次爲:

 

2 4 6 8 1 5 9 7

 

最後剩下的人編號爲3

 

初見這道題,可能會想到模擬。可是數據實在太大啦!!

 

我們先拿手來算,可知n分別爲1,2,3,4,5,6,7,8...時的結果是1,1,3,1,3,5,7,1...

 

有如下規律:從1到下一個1爲一組,每一組中都是從1開始遞增的奇數,且每組元素的個數分別爲1,2,4...

 

這樣就好弄了!!

 

大體思路如下:

 

①read(a)

 

②b:=1,c:=1{b爲某一組的元素個數,c爲現在所加到的數}

 

③while c<a do (b:=b*2,c:=b+c){超過目標時停止加數}

 

⑥c:=c-b{退到前一組}

 

⑦x:=a-c{算出目標爲所在組的第幾個元素}

 

⑧ans:=x*2-1{求出該元素}

 

⑨write(ans)

 

有了思路,再加上高精度就可以了。我寫的代碼比較猥瑣,因爲是先把上面的思路敲進去,再寫過程,又把一些簡單的過程合到主程序中了,所以有點亂,也有點猥瑣。起提供思路的作用還是完全可以的吧~~~

 

var a,b,c:array[1..105]of integer;

 

la,lb,lc,i:integer;

 

s:string;

 

procedure incc;

 

var i:integer;

 

begin

 

for i:=1 to 105 do c:=c+b;

 

for i:=1 to 104 do if c>9 then

 

begin

 

c:=c+c div 10;

 

c:=c mod 10;

 

end;

 

end;

 

function cxiaoa:boolean;

 

var i:integer;

 

begin

 

cxiaoa:=false;

 

for i:=105 downto 1 do

 

if c<a then begin cxiaoa:=true;break;end

 

else if c>a then break;

 

end;

 

procedure doubleb;

 

var i:integer;

 

begin

 

for i:=1 to 105 do b:=b*2;

 

for i:=1 to 104 do if b>9 then

 

begin

 

b:=b+b div 10;

 

b:=b mod 10;

 

end;

 

end;

 

procedure decc;

 

var i,j:integer;

 

begin

 

for i:=1 to 104 do

 

if c>=b then c:=c-b else

 

begin

 

j:=i+1;

 

while c[j]=0 do inc(j);

 

while j>i do

 

begin

 

c[j]:=c[j]-1;

 

c[j-1]:=c[j-1]+10;

 

dec(j);

 

end;

 

c:=c-b;

 

end;

 

end;

 

procedure fua;

 

var i:integer;

 

begin

 

for i:=1 to 104 do

 

if a>c then a:=a-c else

 

begin

 

a:=a-1;

 

a:=a+10;

 

a:=a-c;

 

end;

 

end;

 

procedure outit;

 

var i,j:integer;

 

begin

 

for i:=1 to 105 do a:=a*2;

 

for i:=1 to 104 do if a>9 then

 

begin

 

a:=a+a div 10;

 

a:=a mod 10;

 

end;

 

if a[1]>0 then a[1]:=a[1]-1 else

 

begin

 

j:=2;

 

while a[j]=0 do inc(j);

 

while j>1 do

 

begin

 

a[j]:=a[j]-1;

 

a[j-1]:=a[j-1]+10;

 

dec(j);

 

end;

 

a[1]:=a[1]-1;

 

end;

 

for i:=105 downto 1 do if a>0 then begin j:=i;break;end;

 

for i:=j downto 1 do write(a);

 

end;

 

begin

 

readln(s);

 

la:=length(s);

 

for i:=la downto 1 do a:=ord(s[la+1-i])-ord('0');

 

b[1]:=1;

 

c[1]:=1;

 

while cxiaoa do

 

begin

 

doubleb;

 

incc;

 

end;

 

decc;

 

fua;

 

outit;

 

end.

 


約瑟夫問題的另外一個有名的例子:

 


猴子選大王

一. 問題描述:

 

一堆猴子都有編號,編號是1,2,3 ...m ,這羣猴子(m個)按照1-m的順序圍坐一圈,從第1開始數,每數到第N個,該猴子就要離開此圈,這樣依次下來,直到圈中只剩下最後一隻猴子,則該猴子爲大王。

 

二. 基本要求:

 

(1) 輸入數據:輸入m,n m,n 爲整數,n<m

 

(2)中文提示按照m個猴子,數n 個數的方法,輸出爲大王的猴子是幾號 ,建立一個函數來實現此功能

 

C程序:

 

#include <stdio.h>

 

#include<malloc.h>

 

#define LEN sizeof(struct monkey) //定義struct monkey 這個類型的長度

 

struct monkey

 

{

 

int num;

 

struct monkey *next;

 

};

 

struct monkey *create(int m)

 

{

 

struct monkey *head,*p1,*p2;

 

int i;

 

p1=p2=(struct monkey*)malloc(LEN);

 

head=p1;

 

head->num=1;

 

for(i=1,p1->num=1;i<m;i++)

 

{

 

p1=(struct monkey*)malloc(LEN);

 

p1->num=i+1;

 

p2->next=p1;

 

p2=p1;

 

}

 

p2->next=head;

 

return head;

 

}

 

struct monkey *findout(struct monkey *start,int n)

 

{

 

int i;

 

struct monkey *p;

 

i=n;

 

p=start;

 

for(i=1;i<n-1;i++)

 

p=p->next;

 

return p;

 

}

 

struct monkey *letout(struct monkey *last)

 

{

 

struct monkey *out,*next;

 

out=last->next;

 

last->next=out->next;

 

next=out->next;

 

free(out);

 

return next;

 

}

 

int main()

 

{

 

int m,n,i,king;

 

struct monkey *p1,*p2;

 

printf("請輸入猴子的個數m:\n");

 

scanf("%d",&m);

 

printf("每次數猴子的個數n :\n");

 

scanf("%d",&n);

 

if(n==1)

 

{

 

king=m;

 

}

 

else

 

{

 

p1=p2=create(m);

 

for(i=1;i<m;i++)

 

{

 

p2=findout(p1,n);

 

p1=p2;

 

p2=letout(p1);

 

p1=p2;

 

}

 

king=p2->num;

 

free(p2);

 

}

 

printf("猴王的編號是:%d\n",king);

 

return 0;

 

}

 

pascal程序:

 

var

 

a:array[1..10000] of integer;

 

n,s,i,j:integer;

 

begin

 

read(m,n);

 

for i:=1 to m do a[i]:=1;

 

j:=0;

 

for i:=1 to m do

 

begin

 

s:=0;

 

while s<n do

 

begin

 

if j<m then inc(j)

 

else j:=1;

 

s:=s+a[j];

 

end;

 

write(j);

 

a[j]:=0;

 

end;

 

end.

 

#include <stdio.h>

 

int main()

 

{

 

int n, m, i, s=0;

 

printf ("N M = "); scanf("%d%d", &n, &m);

 

for (i=2; i<=n; i++) s=(s+m)%i;

 

printf ("The winner is %d\n", s+1);

 

return 0;

 

}

 

約瑟夫數學算法

 

#include <stdio.h>

 

#include <conio.h>

 

int main( void )

 

{

 

int n, i = 0, m, p;

 

scanf("%d%d", &n, &m); //n總人數,m步長

 

while( ++i <= n )

 

{

 

p = i * m;

 

while (p > n)

 

p = p - n + (p - n - 1)/(m - 1);

 

printf("%d\n", p);

 

}

 

getch();

 

return 0;

 

}

 

約瑟夫遞推算法

 

#include<iostream>

 

using namespace std;

 

int king(int M, int N)

 

{

 

int k = 0;

 

for (int i = 2; i <= M; i++)

 

k = (k + N) % i;

 

return ++k; }i

 

nt main()

 

{

 

int n,m;

 

while(scanf("%d%d",&n,&m)&&n&&m)

 

{

 

cout<<king(n,m)<<endl;

 

}

 

return 0;

 

}

 


筆算解決約瑟夫問題

在M比較小的時候 ,可以用筆算的方法求解,

 

M=2

 

即N個人圍成一圈,1,2,1,2的報數,報到2就去死,直到只剩下一個人爲止。

 

當N=2^k的時候,第一個報數的人就是最後一個死的,

 

對於任意的自然數N 都可以表示爲N=2^k+t,其中t<n/2

 

於是當有t個人去死的時候,就只剩下2^k個人 ,這2^k個人中第一個報數的就是最後去死的。這2^k個人中第一個報數的人就是2t+1

 

於是就求出了當M=2時約瑟夫問題的解:

 

求出不大於N的最大的2的整數次冪,記爲2^k,最後一個去死的人是2(N-2^k)+1

 

M=3

 

即N個人圍成一圈,1,2,3,1,2,3的報數,報到3就去死,直到只剩下一個人爲止。

 

此時要比M=2時要複雜的多

 

我們以N=2009爲例計算

 

N=2009,M=3時最後被殺死的人記爲F(2009,3),或者可以簡單的記爲F(2009)

 

假設現在還剩下n個人,則下一輪將殺死[n/3]個人,[]表示取整,還剩下n-[n/3]個人

 

設這n個人爲a1,a2,...,a(n-1),an

 

從a1開始報數,一圈之後,剩下的人爲a1,a2,a4,a5,...a(n-n mod 3-1),a(n-n mod 3+1),..,an

 

於是可得:

 

1、這一輪中最後一個死的是a(n-n mod 3),下一輪第一個報數的是a(n-n mod 3+1)

 

2、若3|n,則最後死的人爲新一輪的第F(n-[n/3])個人

 

若n mod 3≠0 且f(n-[n/3])<=n mod 3則最後死的人爲新一輪的第n-[n/3]+F(n-[n/3])-(n mod 3)人

 

若n mod 3≠0 且f(n-[n/3])>n mod 3則最後死的人爲新一輪的第F(n-[n/3])-(n mod 3)人

 

3、新一輪第k個人對應原來的第 3*[(k-1)/2]+(k-1)mod 2+1個人

 

綜合1,2,3可得:

 

F(1)=1,F(2)=2,F(3)=2,F(4)=1,F(5)=4,F(6)=1,

 

當f(n-[n/3])<=n mod 3時 k=n-[n/3]+F(n-[n/3])-(n mod 3),F(n)=3*[(k-1)/2]+(k-1)mod 2+1

 

當f(n-[n/3])>n mod 3時 k=F(n-[n/3])-(n mod 3) ,F(n)=3*[(k-1)/2]+(k-1)mod 2+1

 

這種算法需要計算 [log(3/2)2009]次 這個數不大於22,可以用筆算了

 

於是:

 

第一圈,將殺死669個人,這一圈最後一個被殺死的人是2007,還剩下1340個人,

 

第二圈,殺死446人,還剩下894人

 

第三圈,殺死298人,還剩下596人

 

第四圈,殺死198人,還剩下398人

 

第五圈,殺死132人,還剩下266人

 

第六圈,殺死88人,還剩下178人

 

第七圈,殺死59人,還剩下119人

 

第八圈,殺死39人,還剩下80人

 

第九圈,殺死26人,還剩下54人

 

第十圈,殺死18人,還剩36人

 

十一圈,殺死12人,還剩24人

 

十二圈,殺死8人,還剩16人

 

十三圈,殺死5人,還剩11人

 

十四圈,殺死3人,還剩8人

 

十五圈,殺死2人,還剩6人

 

F(1)=1,F(2)=2,F(3)=2,F(4)=1,F(5)=4,F(6)=1,

 

然後逆推回去

 

F(8)=7 F(11)=7 F(16)=8 f(24)=11 f(36)=16 f(54)=23 f(80)=31 f(119)=43 f(178)=62 f(266)=89 f(398)=130

 

F(596)=191 F(894)=286 F(1340)=425 F(2009)=634

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章