化繁為簡 經典的漢諾塔遞迴問題 in Java
問題描述
在世界中心貝拿勒斯(在印度北部)的聖廟裡,一塊黃銅板上插著三根寶石針。印度教的主神梵天在創造世界的時候,在其中一根針上從下到上地穿好了由大到小的64片金片,這就是所謂的漢諾塔。不論白天黑夜,總有一個僧侶在按照下面的法則移動這些金片:一次只移動一片,不管在哪根針上,小片必須在大片上面。僧侶們預言,當所有的金片都從梵天穿好的那根針上移到另外一根針上時,世界就將在一聲霹靂中消滅,而梵塔、廟宇和眾生也都將同歸於盡。
扯遠了,把這個問題簡單描述下有A,B,C三根柱子,將A柱上N個從小到大疊放的盤子移動到C柱,一次只能移動一個,不重複移動,小盤子必須在大盤子上面。
問一共需要移動多少次,步驟是什麼?
解決思路
讓我們從簡單的情況下開始考慮
首先我說明幾個數學符號,我自己瞎編的,為了方便表示問題,不用打那麼多字
A -> B
箭頭意思代表是從 A柱 移動到 B柱 ,每次移動都是移動最上面的那一塊
n = 1
只有一個盤子的時候,很簡單,直接移就是了,用數學符號記錄一下代表 A -> C
n = 2
如果有兩個盤子,就分為三步
1.先將最上面的盤子也就是(從下往上數)第2個盤子,先移動到B 記為 A -> B
2.然後將第一個盤子移動移動到C , A -> C
3.最後將 B柱子上的盤子 移動到 C柱 , B -> C
n = 3
如果有三個盤子,就分為7步,這裡就不打字了,太累,用數學符號表示
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
額,不要擺出一副黑人問號臉...,自己隨便拿三個道具擺一擺就知道了
當你擺一擺的時候就知道,我知道了,一個盤子移動1次,二個盤子移動3次,三個盤子移動7次,四個盤子移動15次,N個盤子移動 (2^n - 1) 次!
恭喜你答對了!數學歸納法找規律不需多久就可以找到規律,但還有問題是究竟要怎麼移呢?... 以及用程式怎麼解決呢?
n = n....
抽象出這個問題
將問題抽象出來並且轉化為數學模型或者公式,是解決現實生活中複雜問題的一個很好的解決辦法,例如谷歌的翻譯,大家都覺得很智慧,自然語言的翻譯從20世界60年代就開始研究了,具體細節這裡不是重點,最後的解決思路是基於統計模型來解決的,也就是說,困擾了很多年的問題最後是抽象成一個概率論公式得以解決,事實上,眾多複雜的問題最後進行抽象其實就是幾個流程和公式而已,下面就以這個哈諾塔問題為例。
當有N個盤子的時候,似乎很難去想具體怎麼實現,既然這麼難想,就不用去想,首先這個問題,中有三個柱子,A,B,C,這裡也太具體了,抽象一下,怎麼抽象呢?假如這個問題只有2根柱子,你能完成嗎?廢話,肯定不行啊,我還需要一個柱子來輔助移動,所以這裡的A,B,C三個柱子就抽象成,起始柱,中間柱,目標柱,這裡用from,mid,to來表示
ok,現在是盤子的數量抽象成了n,柱子也抽象成了,from,mid,to 三種柱子,下面對過程進行抽象,將n個盤子從 from 柱子 移動到 to 柱 ,其實總體來看是三步
將n-1個盤子從 起始 柱 移動到 中間 柱
將第n個盤子從 起始 柱 移動到 目標 柱
將第n-1個盤子從 中間 柱 移動到 目標 柱 這裡你可能會說了,我靠,不是隻讓移動一個盤子的嗎,你這第1步和第3步移動了n-1個盤子啊...,對,所以我這裡的過程說的是抽象的過程,也就是說不管具體的實現細節是怎樣的,要達成所有的盤子都從A->C的效果,中間一定是有一步是達到這個效果的,就好比你從北京去紐約,假設只有一條國際航班,要經過巴黎,那我就可以說你從北京去紐約,只有兩步,第一步是去巴黎,第二步是從巴黎去紐約,這裡的道理是同樣的。
那麼現在將上面的第1步怎麼實現呢?同樣抽象,只要將n 替換成 n-1 即可,第三步也是同理
將n-2個盤子從 起始 柱 移動到 中間 柱
將第n-1個盤子從 起始 柱 移動到 目標 柱
將第n-2個盤子從 中間 柱 移動到 目標 柱
程式碼實現
那這個過程,用程式來抽象就是 一個Plate方法,接受四個引數,n,from,mid,to,四個引數的意思分別是
n 代表要移動幾個盤子
from 代表起始柱的名字
mid 代表藉助的中間柱的名字
to 代表目標柱的名字
方法的 作用是 將 n 個盤子 從 from 移動到 to
程式碼如下
/**
* 將n個盤子從 from 移動到 to
*/
public static void movePlate(int n, String from, String mid, String to) {
/* 如果只有一個盤子,就直接從 from 移動到 to */
if (n <= 1) {
System.out.println(from + " -> " + to);
return;
}
/* 1.將 n-1 個盤子 從 from 移動到 mid */
movePlate(n - 1, from, to, mid);
/* 2.將第 n 個盤子 從 from 移動到 to */
System.out.println(from + " -> " + to);
/* 3.將 n-1 個盤子 從mid 移動到 to */
movePlate(n - 1, mid, from, to);
}
你可以發現除去註釋,真正的程式碼只有5行,就將這個問題給解決了,再次提醒這裡的from , mid ,to 是形參,代表的是起始住,中間柱,和目標駐,不是具體的哪一個柱子,所以在第12行,因為第一步是將N-1個移動到中間柱,所以引數時from,to,mid,第18行將n-1個從中間柱移動到目標駐,所以引數時mid,from,to,中間的引數就是需要藉助的柱子。
下面測試一下程式碼,這裡根據題目把from,mid,to起個名分別是A,B,C,那執行這個方法就是將3個盤子從A移動到C
public static void main(String[] args) {
movePlate(3, "A", "B", "C");
}
n = 3 的時候
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
Process finished with exit code 0
發現和上面人為思考的結果是一樣的哦
當n = 4的時候
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
A -> C
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C
Process finished with exit code 0
一共是15步,也沒有問題,再多的我就不測了,有興趣的自己試試按照上面的列印結果來進行操作
推算次數
利用遞迴的方法同樣可以很容易的寫出計算次數的方法
public int countMovePlate(int n) {
if (n <= 1) return 1;
return countMovePlate(n - 1) + 1 +countMovePlate(n-1);
}
那問題來了,還能優化嗎?
上文說到人為觀察,利用數學歸納法可以得出需要的次數是 (2^n - 1) 次,那麼這個數究竟是怎麼得到呢?
先把上面的程式複製下來,進行觀察
/* 1.將 n-1 個盤子 從 from 移動到 mid */
movePlate(n - 1, from, to, mid);
/* 2.將第 n 個盤子 從 from 移動到 to */
System.out.println(from + " -> " + to);
/* 3.將 n-1 個盤子 從mid 移動到 to */
movePlate(n - 1, mid, from, to);
核心程式碼就三行,假設moveplate這個方法需要移動的次數為 (a_n) 次,那麼上面的這三行需要移動的次數就應該是 [ a_n = a_{n-1} + 1 + a_{n-1} ]
第一步是 (a_n) ,第二步是固定的1次,第三步又是 (a_n) ,然後當 n = 1的時候 (a_1 = 1) ,再總結整理一下就成了
[ a_n=\left{ \begin{aligned} 1, n = 1\ 2a_{n-1} + 1,n > 1 \end{aligned} \right. ]
有沒有夢迴高中的趕腳,這是一個很簡單的變形等比數列,我們讓兩邊都加上1
[ a_n + 1 = 2a_{n-1} + 1 + 1 ]
也就是
[ a_n + 1 = 2a_{n-1} + 2 ]
再提取一下
[ a_n + 1 = 2(a_{n-1} + 1) ]
兩邊都除以 ((a_{n-1} + 1))
於是就成了
[ \frac {a_n + 1}{a_{n-1} + 1} = 2 ]
那接著將這個公式一直寫豎式將他們相乘
[ \frac {a_n + 1} {a_{n-1} + 1} = 2 ]
[\frac {a_{n-1} + 1}{a_{n-2} + 1} = 2 ]
[\frac {a_{n-2} + 1}{a_{n-3} + 1} = 2 ]
[\vdots]
[\frac {a_2 + 1}{a_1 + 1} = 2 ]
接著約分, Markdown LaTex公式的刪除線找了半天都沒找到,知道的麻煩告知一下.
約分結果是
[\frac {a_n + 1}{a_1 + 1} = 2^{n-1} ]
接著將前面的 (a_1 = 1) 代入,於是
[\frac {a_n + 1}{2} = 2^{n-1} ]
再整理一下
[a_n = 2^n - 1 , n > 1]
所以說
[ a_n=\left{ \begin{aligned} 1, n = 1\ 2^n - 1 , n > 1 \end{aligned} \right. ]
將n =1 代入 n > 1 的情況,也是成立的,因此
[a_n = 2^n - 1 ]
所以經過推導之後java程式碼如下,因為涉及到 (2^n) 這種運算,可以使用移位符,這樣底層移動速度很快
程式碼如下
public static int countMovePlate(int n) {
return n >= 1 ? (1 << n) - 1 : 0;
}
結論
最終我們解決漢諾塔的移動順序與統計次數的程式碼如下,可以看出並不需要幾行程式碼就解決了問題
/* 列印出移動順序 */
public static void movePlate(int n, String from, String mid, String to) {
if (n <= 1) {
System.out.println(from + " -> " + to);
return;
}
movePlate(n - 1, from, to, mid);
System.out.println(from + " -> " + to);
movePlate(n - 1, mid, from, to);
}
/* 返回需要移動的次數 */
public static int countMovePlate(int n) { return n >= 1 ? (1 << n) - 1 : 0;}
java學習交流群:669823128 禁止閒聊,非喜勿加
相關文章
- 經典遞迴解決漢諾塔!遞迴
- 第二章 :查詢與排序-------遞迴經典問題——漢諾塔問題排序遞迴
- 遞迴實現漢諾塔問題遞迴
- C#中漢諾塔問題的遞迴解法C#遞迴
- 漢諾塔和遞迴遞迴
- python3:遞迴解漢諾塔問題Python遞迴
- 漢諾塔非遞迴演算法遞迴演算法
- 漢諾塔非遞迴棧程式碼遞迴
- 漢諾塔的圖解遞迴演算法圖解遞迴演算法
- c++遞迴與迭代實現漢諾塔C++遞迴
- 從漢諾塔遊戲理解python遞迴函式遊戲Python遞迴函式
- 百練OJ:4147:漢諾塔問題(Hanoi)——python實現漢諾塔Python
- Python實現:漢諾塔問題Python
- 奇怪的漢諾塔 - 題解
- LeetCode 90 | 經典遞迴問題,求出所有不重複的子集IILeetCode遞迴
- [演算法練習及思路-程式設計師面試金典(Java解法)]No46.漢諾塔問題演算法程式設計師面試Java
- 【YbtOJ高效進階 遞推-2】奇怪漢諾塔
- 原:八皇后問題的遞迴和非遞迴Java實現遞迴Java
- js解決漢諾塔問題程式碼例項JS
- SQL 漢諾塔SQL
- JAVA漢諾塔遞迴 之SpringCloud企業分散式微服務雲架構快速開發平臺Java遞迴SpringGCCloud分散式微服務架構
- js漢諾塔問題解決方法程式碼例項JS
- 漢諾塔的實現
- 漢諾塔詳解
- 漢諾塔-PythonPython
- ACM 漢諾塔(三)ACM
- 化繁為簡-優化sql優化SQL
- 樹遞迴問題的求解遞迴
- Event loop的化繁為簡(一)OOP
- Event loop的化繁為簡(二)OOP
- 漢諾塔通項公式公式
- 遞迴路徑問題遞迴
- 揹包問題的遞迴與非遞迴演算法遞迴演算法
- 簡單的java遞迴演算法Java遞迴演算法
- java經典面試題Java面試題
- Java解決遞迴造成的堆疊溢位問題Java遞迴
- 開發的藝術,化繁為簡
- [面試題]事件迴圈經典面試題解析面試題事件