文章也同時在個人博客 http://kimihe.com/更新
漢諾塔問題(Hanoi Tower)
漢諾塔問題的描述就是有三根柱子,其中一根從上至下按照從小到大的順序疊放著若干圓盤,我們的目標就是借助另外兩根空柱子,把這一碟圓盤移到另外任意一根柱子上。在移動端過程中,編號較大的圓盤只能在較小的下面,而不能在上面。例如3可以在1,2的下面,但不能在1,2的上面。
解決漢諾塔問題的一個很好的思路就是利用遞歸,把n個圓盤移到另外一根柱子上需要先把底盤上方的n-1個圓盤進行移動,而移動這n-1個又需要先移動更上面的n-2個,如此循環至最開始的只有一個圓盤的情況。
上述思路的核心就是把一個復雜問題分解成若干較小的問題,而較小的問題的模型和大問題的相同,直到最后到達一個臨界點,該臨界點處問題的解決非常簡單。這種把大問題不斷分解成較小的子問題的思路與遞歸契合,而最終的臨界點條件滿足后,我們就能向上回溯,從最簡單的情況到最復雜的情況,一層層地解決。
算法簡述
- 首先,遞歸移動上方的n-1個圓盤到一根“合適”的柱子上
- 接著,移動底盤到另一根“合適”的柱子,簡單的一步操作
- 最后,把這n-1個圓盤疊加在底盤上,與第一點的遞歸操作相同
算法偽代碼如下:
hanoi(n) {
if (n==0) {
return;
}
if (n==1) {
move(n);
return;
}
hanoi(n-1);
move(n);
hanoi(n-1);
}
在實際編寫過程中,我們更多地是需要考慮如何把每一步過程詳細地表達出來,我們可以在move(n)函數中打印相關信息,即n號圓盤從from柱子移動到to柱子。
如果由人工操作,只要告訴我們應該移動哪個圓盤,按照大編號在下的規則,我們就很容易看出應該把這個圓盤移到哪個柱子上。因為n號圓盤在移動時,你不能放在含有比它小的圓盤的柱子上,只能放在另外一根上面,這一點很直觀。
但是如果要把該過程打印出來,讓機器來進行柱子的判斷還是比較復雜的,因此我們最好把移動的起點,終點,中間暫存的柱子也加入遞歸函數中,即加入三根柱子的實時狀態。于是偽代碼就變成了如下:
void hanoi(n, from, to, tmp) {
if (n == 0) {
return;
}
if (n == 1) {
moveBottom(n, from, to);
return;
}
hanoi(n-1, from, tmp, to);
moveBottom(n, from, to);
hanoi(n-1, tmp, to, from);
}
臨界點n=1時不需要臨時存放的柱子,直接移到目標柱子即可。
整個問題的起點我們可以人工設定,如把A柱子上的一堆圓盤移到C柱子上,中間過程借助B柱子暫存,以此啟動我們的“遞歸機器”。如下:
hanoi(diskNumber, 'A', 'C', 'B');
當然你也可以從A移到B,中間借助C,問題的模型是一樣的。
總結
設定好初始狀態和遞歸函數后,我們就能啟動“遞歸機器”,當遞歸函數到達臨界點,即只有一個盤子時,進行完最簡單的移動操作后,整個過程開始回溯。遞歸借助棧的性質,我們能夠安全保存每一次遞歸調用前的狀態,一步步從簡單返回最復雜的情況,抽絲剝繭地把大問題降維成小問題。
你想要數到100,你得先數99,數99得先數98,...,一直到最開始你得先數0。這也是一種遞歸的思路,后續步驟都可以同結構地依賴之前的步驟。
代碼地址
代碼示例可見https://github.com/kimihe/FUN_IN_CS/tree/master/HanoiTower。
視頻教學推薦
推薦B站上一個很棒的科普視頻:用二進制來解漢諾塔問題。