*問題分析與算法設計
約瑟夫問題並不難,但求解的方法很多;題目的變化形式也很多。這裏給出壹種實現方法。
題目中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=1;//註意百度百科裏不讓使用ASCII裏的方括號,這裏是中文字符集裏的方括號,
}
j=0;
k=0;
for(i=1;i<=n+1;i++){
if(a==1){
j=j+a;
if(j==m)
{
j=0;
a=0;
k++;
}
if(k==n){
num=i;
break;
}
}
if(i==n+1)
i=0;
}
cout<<"最後獲勝的玩家是第 "<<num<<" 號玩家!"<<endl;
cout<<"----------------------------------------"<<endl;
}
寫完密碼約瑟夫就想到原來看到約瑟夫問題的壹個數學解法 很巧妙很簡單 不過只能推出最後壹個出列的人
無論是用鏈表實現還是用數組實現都有壹個***同點:要模擬整個遊戲過程,不僅程序寫起來比較煩,而且時間復雜度高達O(nm),當n,m非常大(例如上百萬,上千萬)的時候,幾乎是沒有辦法在短時間內出結果的。我們註意到原問題僅僅是要求出最後的勝利者的序號,而不是要讀者模擬整個過程。因此如果要追求效率,就要打破常規,實施壹點數學策略。
為了討論方便,先把問題稍微改變壹下,並不影響原意:
問題描述:n個人(編號0~(n-1)),從0開始報數,報到(m-1)的退出,剩下的人繼續從0開始報數。求勝利者的編號。
我們知道第壹個人(編號壹定是m mod n-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
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
③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.
筆算解決約瑟夫問題
在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(1