1. 程式人生 > >C語言用陣列1. 簡單約瑟夫環問題: N個人,編號從1~N圍成一圈,輸入一個數T,從1號開始報數,報到T的人出圈;下一人又從1開始報數,下一個報到T的人出圈,輸出出圈順序。 考慮問實現約瑟夫環問題

C語言用陣列1. 簡單約瑟夫環問題: N個人,編號從1~N圍成一圈,輸入一個數T,從1號開始報數,報到T的人出圈;下一人又從1開始報數,下一個報到T的人出圈,輸出出圈順序。 考慮問實現約瑟夫環問題

1. 簡單約瑟夫環問題:

N個人,編號從1~N圍成一圈,輸入一個數T,從1號開始報數,報到T的人出圈;下一人又從1開始報數,下一個報到T的人出圈,輸出出圈順序。

考慮問題:

報到T的人出圈,怎麼表示出圈?要麼刪除對應的標號,其他的標號前移(如果是陣列結構,要依次移動元素,效率低;如果是連結串列,刪除結點也比較麻煩);要麼設定狀態標誌位(宣告一個狀態陣列status[N],status[i] ==0時表示未出圈,出圈時將對應第i號人的status置為出圈的次數;即status[i]=count)

解決了表示出圈的問題,那如何出圈?如果報數T大於總人數N時,需要取餘才能將報號的範圍限制在標號為0~N-1中。

流程呢?

while(出圈人數<總人數)

{

        從start下標依次查詢status為0的下標(需要儲存start下標)

                     計數

                判斷計數是否等於T

              若計數等於T

                         出圈,更新對應下標的status,出圈人數加1

}

  1. void joseph()  
  2. {  
  3.     int T,N;  
  4.     scanf("%d%d",&T,&N);  
  5.     //T為出列週期,N為N個人,即環的元素個數
  6.     int status[1000];  
  7.     memset(status,0,sizeof(status));  
  8.     int start,end;  
  9.     start = -1;  
  10.     int count = 0;  
  11.     while(count<N)  
  12.     {  
  13.         int i=0;  
  14.         while(1)  
  15.         {  
  16.             start = (start+1) % N;  
  17.             if(status[start] == 0)  
  18.             {  
  19.                 i++;  
  20.             }  
  21.             if(i == T)  
  22.             {  
  23.                 ++count;  
  24.                 status[start]=count;  
  25.                 break;  
  26.             }  
  27.         }  
  28.     }  
  29.     for(int k=0;k<N;k++)  
  30.         printf("%d",status[k]);  
  31. }  

2. 複雜約瑟夫環

1~N個人構成一圈,每個人手中有個號碼,讀入一個數T,從第一個人開始報數,報到T的人出圈;下一個人接著從1報數,報到出圈人手裡的號碼的人出圈,依次進行。

與簡單約瑟夫環不同的是T變了;每出圈一個,要更新對應的T;

  1. void rand_joseph()  
  2. {  
  3.     //如果約瑟夫環中的數是使用者輸入的資料時
  4.     int N,data[1000],status[1000];  
  5.     memset(data,0,sizeof(data));  
  6.     memset(status,0,sizeof(status));  
  7.     int i=0;  
  8.     scanf("%d",&N);  
  9.     while(i<N)  
  10.     {  
  11.         scanf("%d",&data[i]);  
  12.         i++;  
  13.     }  
  14.     int T;  
  15.     scanf("%d",&T);//第一個週期由使用者輸入,其他的是出隊使用者對應的值
  16.     int count = 0;  
  17.     int index = -1;  
  18.     while(count<N)  
  19.     {  
  20.         int i=0;  
  21.         while(1)  
  22.         {  
  23.             index = (index+1)%N;  
  24.             if(status[index] == 0 )  
  25.             {  
  26.                 i++;  
  27.             }  
  28.             if(i == T)  
  29.             {  
  30.                 ++count;  
  31.                 status[index] = count;  
  32.                 T = data[index];  
  33.                 break;  
  34.             }  
  35.         }  
  36.     }  
  37.     for(int i=1;i<=N;i++)  
  38.     {  
  39.         for(int j=0;j<N;j++)  
  40.         if(status[j] == i)  
  41.             printf("第%d個出列的是第%d人\n",i,j+1);  
  42.     }  
  43. }  

3. 如果要求最後出圈的人的編號,還有簡單的方法:

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

為了討論方便,先把問題稍微改變一下,並不影響原意:
問題描述: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。
現在我們把他們的編號做一下轉換:

k     --> 0
k+1   --> 1
k+2   --> 2
...
...
k-2   --> n-2
k-1   --> n-1
變換後就完完全全成為了(n-1)個人報數的子問題,假如我們知道這個子問題的解:例如x是最終的勝利者,那麼根據上面這個表把這個x變回去不剛好就是n個人情況的解嗎?!!變回去的公式很簡單,相信大家都可以推出來:x'=(x+k)%n

如何知道(n-1)個人報數的問題的解?對,只要知道(n-2)個人的解就行了。(n-2)個人的解呢?當然是先求(n-3)的情況 ---- 這顯然就是一個倒推問題!好了,思路出來了,下面寫遞推公式:

令f[i]表示i個人玩遊戲報m退出最後勝利者的編號,最後的結果自然是f[n]

遞推公式
f[1]=0;
f[i]=(f[i-1]+m)%i;  (i>1)

有了這個公式,我們要做的就是從1-n順序算出f[i]的數值,最後結果是f[n]。因為實際生活中編號總是從1開始,我們輸出f[n]+1
由於是逐級遞推,不需要儲存每個f[i],程式也是異常簡單:

  1. #include <stdio.h>
  2. int main()  
  3. {  
  4.     int n, m, i, s = 0;  
  5.     printf ("N M = ");  
  6.     scanf("%d%d", &n, &m);  
  7.     for (i = 2; i <= n; i++)  
  8.     {  
  9.         s = (s + m) % i;  
  10.     }  
  11.     printf ("\nThe winner is %d\n", s+1);  
  12. }