約瑟夫環問題——順推法與遞歸法

題目

0,1,n-1這n個數字排成一個圓圈,從數字0開始,每次從這個圓圈裏刪除第m個數字。求出這個圓圈裏剩下的最後一個數字。

例如,0、1、2、3、4這5個數字組成一個圓圈,從數字0開始每次刪除第3個數字,則刪除的前4個數字依次是2、0、4、1,因此最後剩下的數字是3。

來源:力扣(LeetCode)

前不久在LeetCode上刷到一個比較有意思的題,一開始準備用數組暴力破解,在思索良久之後放棄了自己天真的想法。網上找了許多方法之後才弄懂了其中的原理,總結爲以下兩種方法:順推法與遞歸法。

這兩種方法看似只是相反的過程,但其中的原理確實不太相同。不過不管黑貓白貓,逮到耗子的就是好貓。

讓我們來分析一下題意

如下表,在這幾個數之中每次找到第三個數字然後去掉,直到最後一個數字留下來爲止。(注意:每次數三個數字不是又從數字0開始,而是從上一次去掉的數字後面開始)
題意
可以很直觀的看出第三個數字一個一個被去掉,直到最後剩下兩個角逐,花落誰家?答案當然是3。

順推法:顧名思義,其實就是類似於暴力破解,順着思路一步一步做下去。

我們可以藉助ArrayList動態數組實現數字的增加與刪除,在此之前我們所知道的參數也就是n個數字,以及每次要去掉的第m個數字,之後進行的就是初始化數組,然後再一個一個減去就好了。

這裏的問題在於怎麼求出你要去掉的數字,第一次我們減的是2號(這裏是數字2),第二次就要減4號(因爲不夠減呀,輪迴去就是數字1),輪迴去這裏就是一個重要的結點,試分析一手。

數組ArrayList

第一次我們要去掉的數字下標可以用m-1表示(例子裏是2號),
第二次我們要去掉的數字下標打算用(m-1)+m-1表示(例子裏應該是4號),結果和我們肉眼看出來的不符,因爲已經超過數組長度了,怎麼解決呢?

此時我們引入一個求餘算法,可以回到數組開頭。(業內術語都叫求模
我查了查對於正數,這兩個概念似乎沒什麼不同,我們暫且這樣俗氣的叫。
除法

餘數嘛,
如果被除數小於除數,那麼商爲0(很簡單的樣子),直接把被除數拿到餘數裏面就好了;
如果被除數大於除數,那麼商爲一個整數,餘數也就可以看作是從零開始數的標號。

這裏就很符合我們開始的思維邏輯了吧。

因此,每次只需要對要求數組長度求餘就好了,
這樣我們的第一次要去掉的數字的標號可以求出來(3-1)%5=2,
那第二次呢,(2+3-1)%4=0,
第三次,(0+3-1)%3=2,
第四次,(2+3-1)%2=0,
第五次不用說,數組只剩一個元素,下標確實爲0。

java代碼如下:

public int Circle1( int n,int m ) {
  	ArrayList<Integer> list = new ArrayList<>();
  	for( int i = 0;i < n; i++ ) 	//數組初始化
  		list.add(i);
  	int x = 0;    			//記錄每次去掉數字的下標
  	while( n != 1 ) {
    		x = ( x + m - 1 )%n;
    		list.remove(x);
    		n--;   			//數組長度減一
  	}
  	int result = list.get(0);	//最後贏家只有一個,所以下標爲0
  	return result;
 }

既然我們順着推已經思路明確,雖然有點複雜但終究還是求出來了,那麼有簡單一點的思路嗎?請繼續往下看。

遞歸法:可以用數學方法推導出來,重要的是我們知道一個隱含的關鍵條件:最後一個數字其下標爲0

所謂遞歸,
就是我們去掉n個數字時的標號m-1,
接下來我們要繼續求n-1個數字的情況,
然後是n-2個數字的情況…
以此類推,
直到剩下1個數字。

如圖:

遞歸

假設我們已知一個函數 f(1)=0,意思就是最後一次僅剩的數字其標號爲0;
那麼上一個輪迴(倒數第二次)它的標號呢? f(2)
沒錯,直到最開始n個數字,那麼最終贏家的標號爲 f(n),答案不就出來了嗎 。

那麼問題來了,我們怎麼求出 f(2) 甚至是 f(n)呢?

觀察下圖(藍色爲標號,綠色爲輪迴去補上的數字,紅色爲我們要求的數字)
下圖
最後一次數字3的標號爲 f(1)=0;
兩個數時,細心的你會發現,上一次標號f(1)往後數m個數字會是f(2),
這裏主要是去掉的數字後其後面的數字要進入下一步的話,就需要標號全部往前移動m
只不過標號需要稍稍變換,即我們所說的求模(因爲超出數組範圍了),那麼則有f(2)=(f(1)+m)%2,答案是1沒有問題;
三個數時,f(3)=(f(2)+m)%3,答案是1沒有問題,後面步驟相同。
那麼可以得到一個函數:
函數
java代碼如下:

 public int Circle2( int n,int m ) {
	  if( n == 1 )		//最後一個數字標號爲0
	   	return 0;
	  else
	   	return ( Circle2( n-1 , m ) + m ) % n;
}

綜上,便是兩種方法的具體過程,
其中比較難懂的還是求模的道理,那個是說要在一個循環的表之中找數,如果超出範圍,則可重新從0開始進行計算;
還有就是對遞歸方法的掌握,觀察法真的不太容易看出來,目前只能出此下策解釋,如果有更好的理解方法大家可以與我討論。

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