問題描述
在世界中心貝拿勒斯(在印度北部)的圣廟里,一塊黃銅板上插著三根寶石針奈辰。印度教的主神梵天在創(chuàng)造世界的時候栏妖,在其中一根針上從下到上地穿好了由大到小的64片金片,這就是所謂的漢諾塔奖恰。不論白天黑夜吊趾,總有一個僧侶在按照下面的法則移動這些金片:一次只移動一片,不管在哪根針上瑟啃,小片必須在大片上面论泛。僧侶們預(yù)言,當所有的金片都從梵天穿好的那根針上移到另外一根針上時蛹屿,世界就將在一聲霹靂中消滅屁奏,而梵塔、廟宇和眾生也都將同歸于盡错负。
扯遠了坟瓢,把這個問題簡單描述下有A,B,C三根柱子勇边,將A柱上N個從小到大疊放的盤子移動到C柱,一次只能移動一個折联,不重復(fù)移動粒褒,小盤子必須在大盤子上面。
問一共需要移動多少次,步驟是什么?
解決思路
讓我們從簡單的情況下開始考慮
首先我說明幾個數(shù)學(xué)符號诚镰,我自己瞎編的奕坟,為了方便表示問題,不用打那么多字
A -> B
箭頭意思代表是從 A柱 移動到 B柱 ,每次移動都是移動最上面的那一塊
n = 1
只有一個盤子的時候清笨,很簡單执赡,直接移就是了,用數(shù)學(xué)符號記錄一下代表 A -> C
n = 2
如果有兩個盤子,就分為三步
1.先將最上面的盤子也就是(從下往上數(shù))第2個盤子,先移動到B 記為 A -> B
2.然后將第一個盤子移動移動到C , A -> C
3.最后將 B柱子上的盤子 移動到 C柱 , B -> C
n = 3
如果有三個盤子,就分為7步,這里就不打字了函筋,太累沙合,用數(shù)學(xué)符號表示
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C
額,不要擺出一副黑人問號臉...跌帐,自己隨便拿三個道具擺一擺就知道了
當你擺一擺的時候就知道首懈,我知道了,一個盤子移動1次谨敛,二個盤子移動3次究履,三個盤子移動7次,四個盤子移動15次脸狸,N個盤子移動 (2^n - 1) 次!
恭喜你答對了最仑!數(shù)學(xué)歸納法找規(guī)律不需多久就可以找到規(guī)律,但還有問題是究竟要怎么移呢?... 以及用程序怎么解決呢炊甲?
n = n....
抽象出這個問題
將問題抽象出來并且轉(zhuǎn)化為數(shù)學(xué)模型或者公式泥彤,是解決現(xiàn)實生活中復(fù)雜問題的一個很好的解決辦法,例如谷歌的翻譯卿啡,大家都覺得很智能吟吝,自然語言的翻譯從20世界60年代就開始研究了,具體細節(jié)這里不是重點颈娜,最后的解決思路是基于統(tǒng)計模型來解決的剑逃,也就是說,困擾了很多年的問題最后是抽象成一個概率論公式得以解決官辽,事實上蛹磺,眾多復(fù)雜的問題最后進行抽象其實就是幾個流程和公式而已,下面就以這個哈諾塔問題為例同仆。
當有N個盤子的時候萤捆,似乎很難去想具體怎么實現(xiàn),既然這么難想,就不用去想鳖轰,首先這個問題清酥,中有三個柱子,A,B,C蕴侣,這里也太具體了焰轻,抽象一下,怎么抽象呢昆雀?假如這個問題只有2根柱子辱志,你能完成嗎?廢話狞膘,肯定不行啊揩懒,我還需要一個柱子來輔助移動,所以這里的A,B,C三個柱子就抽象成挽封,起始柱已球,中間柱,目標柱辅愿,這里用from,mid,to來表示
ok智亮,現(xiàn)在是盤子的數(shù)量抽象成了n,柱子也抽象成了,from,mid,to 三種柱子点待,下面對過程進行抽象阔蛉,將n個盤子從 from 柱子 移動到 to 柱 ,其實總體來看是三步
將n-1個盤子從 起始 柱 移動到 中間 柱
將第n個盤子從 起始 柱 移動到 目標 柱
將第n-1個盤子從 中間 柱 移動到 目標 柱 這里你可能會說了癞埠,我靠状原,不是只讓移動一個盤子的嗎,你這第1步和第3步移動了n-1個盤子啊...苗踪,對颠区,所以我這里的過程說的是抽象的過程,也就是說不管具體的實現(xiàn)細節(jié)是怎樣的徒探,要達成所有的盤子都從A->C的效果瓦呼,中間一定是有一步是達到這個效果的喂窟,就好比你從北京去紐約测暗,假設(shè)只有一條國際航班,要經(jīng)過巴黎磨澡,那我就可以說你從北京去紐約碗啄,只有兩步,第一步是去巴黎稳摄,第二步是從巴黎去紐約稚字,這里的道理是同樣的。
那么現(xiàn)在將上面的第1步怎么實現(xiàn)呢?同樣抽象胆描,只要將n 替換成 n-1 即可,第三步也是同理
將n-2個盤子從 起始 柱 移動到 中間 柱
將第n-1個盤子從 起始 柱 移動到 目標 柱
將第n-2個盤子從 中間 柱 移動到 目標 柱
代碼實現(xiàn)
那這個過程瘫想,用程序來抽象就是 一個Plate方法,接受四個參數(shù),n,from,mid,to,四個參數(shù)的意思分別是
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);
}
你可以發(fā)現(xiàn)除去注釋昌讲,真正的代碼只有5行,就將這個問題給解決了国夜,再次提醒這里的from , mid ,to 是形參,代表的是起始住短绸,中間柱车吹,和目標駐,不是具體的哪一個柱子醋闭,所以在第12行窄驹,因為第一步是將N-1個移動到中間柱,所以參數(shù)時from,to,mid,第18行將n-1個從中間柱移動到目標駐,所以參數(shù)時mid,from,to,中間的參數(shù)就是需要借助的柱子证逻。
下面測試一下代碼,這里根據(jù)題目把from,mid,to起個名分別是A,B,C,那執(zhí)行這個方法就是將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
發(fā)現(xiàn)和上面人為思考的結(jié)果是一樣的哦
當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步,也沒有問題,再多的我就不測了,有興趣的自己試試按照上面的打印結(jié)果來進行操作
推算次數(shù)
利用遞歸的方法同樣可以很容易的寫出計算次數(shù)的方法
public int countMovePlate(int n) {
if (n <= 1) return 1;
return countMovePlate(n - 1) + 1 +countMovePlate(n-1);
}
那問題來了,還能優(yōu)化嗎乐埠?
上文說到人為觀察,利用數(shù)學(xué)歸納法可以得出需要的次數(shù)是 (2^n - 1) 次囚企,那么這個數(shù)究竟是怎么得到呢饮戳?
先把上面的程序復(fù)制下來,進行觀察
/* 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);
核心代碼就三行,假設(shè)moveplate這個方法需要移動的次數(shù)為 (a_n) 次,那么上面的這三行需要移動的次數(shù)就應(yīng)該是 [ a_n = a_{n-1} + 1 + a_{n-1} ]
第一步是 (a_n) ,第二步是固定的1次,第三步又是 (a_n) ,然后當 n = 1的時候 (a_1 = 1) 洞拨,再總結(jié)整理一下就成了
[ a_n=\left{ \begin{aligned} 1, n = 1\ 2a_{n-1} + 1,n > 1 \end{aligned} \right. ]
有沒有夢回高中的趕腳扯罐,這是一個很簡單的變形等比數(shù)列,我們讓兩邊都加上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公式的刪除線找了半天都沒找到烦衣,知道的麻煩告知一下.
約分結(jié)果是
[\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 ]
所以經(jīng)過推導(dǎo)之后java代碼如下,因為涉及到 (2^n) 這種運算,可以使用移位符,這樣底層移動速度很快
代碼如下
public static int countMovePlate(int n) {
return n >= 1 ? (1 << n) - 1 : 0;
}
結(jié)論
最終我們解決漢諾塔的移動順序與統(tǒng)計次數(shù)的代碼如下,可以看出并不需要幾行代碼就解決了問題
/* 打印出移動順序 */
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);
}
/* 返回需要移動的次數(shù) */
public static int countMovePlate(int n) { return n >= 1 ? (1 << n) - 1 : 0;}
java學(xué)習(xí)交流群:669823128 禁止閑聊歹河,非喜勿加