約瑟夫環遞歸算法(C++)(初學者也能看懂邏輯分析)


題目:

n個人圍成一圈(編號從1到n),從第1個人開始報數,報到m的人出列,從下一個人再重新報數,報到m的人出列,如此下去,直至所有人都出列。求最后一個出列的人的編號。

先給出核心代碼

#include <iostream>
using namespace std;
int josephus(int n, int m) 
{
	if(n == 1) return 0;
	else return ( josephus(n-1,m)+m ) % n;
}
 
int main() 
{
	int n, m;
	cin >> n >> m;
	int result = josephus(n, m);
	cout << result+1 << endl;
}

舉例:n=9,m=4

初始 1 2 3 4 5 6 7 8 9
第一輪 1 2 3 5 6 7 8 9
第二輪 1 2 3 5 6 7 9
第三輪 1 2 5 6 7 9
第四輪 1 2 5 6 7
第五輪 1 2 5 7
第六輪 1 2 7
第七輪 1 2
第八輪 1

思路分析1:

  1. 既然是遞歸,那么就是把復雜的問題一步一步分解為最基本、最簡單的問題來解決。即把上例的n=9,逐步分解為n=8,n=7,n=6,……,n=2,n=1來解決。
  2. 據我們觀察,其中輸入的變量有兩個:一個是人數n,一個是標號數m。所以,假設我們求的遞歸函數就是 f ( n , m ) f(n,m) 。又標號數m是固定的,每次計數的時候都是這個標號。
  3. 遞歸說明,前一個項和后一個項肯定是有關系的,不然遞歸進行不下去。即 f ( n , m ) f(n,m) f ( n 1 , m ) f(n-1,m) 之間有一定關系

那么思路便已經建立,我們需要找到 f ( n , m ) f(n,m) f ( n 1 , m ) f(n-1,m) 之間的關系即可,即找到n=9與n=8,n=8與n=7,……,n=3與n=2,n=2與n=1之間的關系。

既然是把復雜的問題一步一步分解為最基本、最簡單的問題來解決,那我們便從n=1的問題來一步步逆向推回去。

思路分析2:

當n=1時, f ( 1 , m ) f(1,m)

當n=1時,環中只有1個人(編號為1),那么最后一個出列的一定是編號為1的人。為了跟之后的推導統一,我們這里得出的result不記為1,而記為0。所以,當n=1時,result=0。

m 2 3 4 5 6 7 8 9 10 11 12
n=1,最后一個出列的人的編號 1 1 1 1 1 1 1 1 1 1 1

當n=2時, f ( 2 , m ) f(2,m)

當n=2時,環中有2個人(編號為1,2),那么最后一個出列的人可能編號為1,也可能編號為2,這要看m的奇偶決定。如果m為奇數,那么最后一個出列的是編號為2的人;如果m為偶數,那么最后一個出列的人是編號為1的人。

m 2 3 4 5 6 7 8 9 10 11 12
n=2,最后一個出列的人的編號 1 2 1 2 1 2 1 2 1 2 1

當n=3時, f ( 3 , m ) f(3,m)

當n=3時,環中有3個人(編號為1,2,3),那么最后一個出列的人可能編號為1,可能編號為2,也可能是編號為3。

m 2 3 4 5 6 7 8 9 10 11 12
n=3,最后一個出列的人的編號 3 2 2 1 1 3 3 2 2 1 1

可以這樣想,當在m確定的情況下,則當n=3時,環中每個人依次報數,報到m的人就離開,那么報完一輪后,肯定走掉一個人,那么還剩2個人,即n=2。用數學的語言說就是從 f ( 3 , m ) f(3,m) 遞推為 f ( 2 , m ) f(2,m) 了,即每進行一輪,n都減1。

那么問題思路更加清晰了,就是找 f ( 3 , m ) f(3,m) f ( 2 , m ) f(2,m) 之間的遞推關系,我們用最笨的方法(窮舉法)來找規律

先對比兩張表格,為了方便看,我把上面兩張表格合並了:

m 2 3 4 5 6 7 8 9 10 11 12
n=2,最后一個出列的人的編號 1 2 1 2 1 2 1 2 1 2 1
n=3,最后一個出列的人的編號 3 2 2 1 1 3 3 2 2 1 1

得出一張新的規律表格:(n=3確定)(編號為0即編號為3)

m f ( n , m ) f(n,m) f ( n 1 , m ) f(n-1,m) 規律
2 f ( 3 , 2 ) = 3 f(3,2)=3 f ( 2 , 2 ) = 1 f(2,2)=1 f ( 3 , 2 ) = [ f ( 2 , 2 ) + 2 ] % n f(3,2)=[f(2,2)+2]\%n
3 f ( 3 , 3 ) = 2 f(3,3)=2 f ( 2 , 3 ) = 2 f(2,3)=2 f ( 3 , 3 ) = [ f ( 2 , 3 ) + 3 ] % n f(3,3)=[f(2,3)+3]\%n
4 f ( 3 , 4 ) = 2 f(3,4)=2 f ( 2 , 4 ) = 1 f(2,4)=1 f ( 3 , 4 ) = [ f ( 2 , 4 ) + 4 ] % n f(3,4)=[f(2,4)+4]\%n
5 f ( 3 , 5 ) = 1 f(3,5)=1 f ( 2 , 5 ) = 2 f(2,5)=2 f ( 3 , 5 ) = [ f ( 2 , 5 ) + 5 ] % n f(3,5)=[f(2,5)+5]\%n
6 f ( 3 , 6 ) = 1 f(3,6)=1 f ( 2 , 6 ) = 1 f(2,6)=1 f ( 3 , 6 ) = [ f ( 2 , 6 ) + 6 ] % n f(3,6)=[f(2,6)+6]\%n
7 f ( 3 , 7 ) = 3 f(3,7)=3 f ( 2 , 7 ) = 2 f(2,7)=2 f ( 3 , 7 ) = [ f ( 2 , 7 ) + 7 ] % n f(3,7)=[f(2,7)+7]\%n
8 f ( 3 , 8 ) = 3 f(3,8)=3 f ( 2 , 8 ) = 1 f(2,8)=1 f ( 3 , 8 ) = [ f ( 2 , 8 ) + 8 ] % n f(3,8)=[f(2,8)+8]\%n
9 f ( 3 , 9 ) = 2 f(3,9)=2 f ( 2 , 9 ) = 2 f(2,9)=2 f ( 3 , 9 ) = [ f ( 2 , 9 ) + 9 ] % n f(3,9)=[f(2,9)+9]\%n
10 f ( 3 , 10 ) = 2 f(3,10)=2 f ( 2 , 10 ) = 1 f(2,10)=1 f ( 3 , 10 ) = [ f ( 2 , 10 ) + 10 ] % n f(3,10)=[f(2,10)+10]\%n
11 f ( 3 , 11 ) = 1 f(3,11)=1 f ( 2 , 11 ) = 2 f(2,11)=2 f ( 3 , 11 ) = [ f ( 2 , 11 ) + 11 ] % n f(3,11)=[f(2,11)+11]\%n
12 f ( 3 , 12 ) = 1 f(3,12)=1 f ( 2 , 12 ) = 1 f(2,12)=1 f ( 3 , 12 ) = [ f ( 2 , 12 ) + 12 ] % n f(3,12)=[f(2,12)+12]\%n

非常容易的觀察得到,遞推規律為 f ( n , m ) = [ f ( n 1 , m ) + m ] % n f(n,m)=[f(n-1,m)+m]\%n
再把找到的這個規律代入 f ( 4 , m ) f(4,m) f ( 3 , m ) f(3,m) 之間驗證,發現也是正確的。即得出遞推關系式。

為什么?

看到這里,你或許想問為什么遞推關系式是 f ( n , m ) = [ f ( n 1 , m ) + m ] % n f(n,m)=[f(n-1,m)+m]\%n ,我連找規律都沒有找出來,又怎么歸納總結遞推關系式?

其實,這背后有這樣一種思想:
還是拿剛才那個例子說事:
總人數n=9人,從編號為1的人開始報數,每報到4就把一人踢出去(m=4)。

初始 1 2 3 4 5 6 7 8 9
第一輪 1 2 3 5 6 7 8 9

此時,這些編號已經不能組成一個環,因為編號為4的地方產生了一個空位。之后的報數將總要考慮原編號4處的空位問題。

如何才能避免已經產生的空位對報數所造成的影響呢?

不過沒有關系,因為下一次報數將從編號為5的人開始,我們可以將剩下的8個人組成一個新的環(5,6,7,8,9,1,2,3)。即,將3和5首尾相連,這樣報數的時候就不用在意4的空位了。
但是新產生的環的數字並非連續的,報數時不像之前那樣好處理了。

怎么處理新環數字非連續,報數無法簡單用[(當前編號)%n]這個式子遞推的問題?

所以現在我們必須借助存儲結構得知下一個應該報數的現存人員編號。
接下來我們的目的是:使新環上的編號能夠遞推來簡化我們之后的處理。
意思就是我們要改變新環上每個人的編號,從而來建立一種有確定規則的映射,達到映射之后數字可以遞推的目的。且可以將在新環中繼續按原規則報數得到的結果逆推出在舊環中的對應數字。

方法:將長度為n-1的新環與n-1個人組成的編號為1~n-1的環一 一映射。

之前的例子,將剩余的 8人與 8 人環(編號為0 ~ 8)一 一映射。

初始 1 2 3 4 5 6 7 8 9
第一輪 1 2 3 5 6 7 8 9
舊環 1 2 3 5 6 7 8 9
新環 6 7 8 1 2 3 4 5

注意,這里的舊環就是進行一輪(踢掉編號為4的人)之后的環,新環是按照上面的方法把剩下的8人進行重新編號(1~8),且讓原來編號為5的人現在新的編號為1,原來編號為6的人現在編號為2(依此類推)。

這樣新環的編號既解決了舊環編號不連續的問題,又解決了新編號與舊編號之間一一映射的關系,即 ( ) = [ ( ) + m ] % ( n ) (舊的編號)=[(新的編號)+m]\%(舊的人數n)

咦!這個式子好像有點眼熟?
對,就是剛才找規律總結出來的 f ( n , m ) = [ f ( n 1 , m ) + m ] % n f(n,m)=[f(n-1,m)+m]\%n

那再看看這個規律適不適用於第二輪、第三輪……
初始 1 2 3 4 5 6 7 8 9
第一輪 1 2 3 5 6 7 8 9
新環 6 7 8 1 2 3 4 5
第二輪 6 7 8 1 2 3 5
新環 2 3 4 5 6 7 1
第三輪 2 3 5 6 7 1
新環 5 6 1 2 3 4
第四輪 5 6 1 2 3
新環 1 2 3 4 5
第五輪 1 2 3 5
新環 2 3 4 1
第六輪 2 3 1
新環 2 3 1
第七輪 2 3
新環 1 2
第八輪 1
新環 1

驗證成功,每一輪與上一輪之間的關系都滿足 f ( n , m ) = [ f ( n 1 , m ) + m ] % n f(n,m)=[f(n-1,m)+m]\%n ,況且我們之前找規律的那張表也可以充分說明這個關系式的可靠性。
綜上所述,約瑟夫遞歸算法全部講解完成。
再貼一次代碼方便查看:

#include <iostream>
using namespace std;
int josephus(int n, int m) 
{
	if(n == 1) return 0;
	else return ( josephus(n-1,m)+m ) % n;
}
 
int main() 
{
	int n, m;
	cin >> n >> m;
	int result = josephus(n, m);
	cout << result+1 << endl;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM