用棧模擬漢諾塔問題
描述
在經典的漢諾塔問題中,有 3 個塔和 N 個可用來堆砌成塔的不同大小的盤子。要求盤子必須按照從小到大的順序從上往下堆 (如:任意一個盤子,其必須堆在比它大的盤子上面)。同時,你必須滿足以下限制條件:
- 每次只能移動一個盤子。
- 每個盤子從堆的頂部被移動后,只能置放於下一個堆中。
- 每個盤子只能放在比它大的盤子上面。
請寫一段程序,實現將第一個堆的盤子移動到最后一個堆中。
樣例
輸入
3
輸出
towers[0]: []
towers[1]: []
towers[2]: [2,1,0]
思路
因為自己以前沒玩過,也沒有紙和筆,另外一直在想知道每個步驟到底應該怎么做,應該從哪個移到哪個,所以到最后也沒有想出一個可行的辦法,最后迫不得已去搜了一下,結果才發現自己一直在鑽牛角尖:
當只有一個盤子的時候,只需要從將A塔上的一個盤子移到C塔上。
當A塔上有兩個盤子是,先將A塔上的1號盤子(編號從上到下)移動到B塔上,再將A塔上的2號盤子移動的C塔上,最后將B塔上的小盤子移動到C塔上。
當A塔上有3個盤子時,先將A塔上編號1至2的盤子(共2個)移動到B塔上(需借助C塔),然后將A塔上的3號最大的盤子移動到C塔,最后將B塔上的兩個盤子借助A塔移動到C塔上。
當A塔上有n個盤子是,先將A塔上編號1至n-1的盤子(共n-1個)移動到B塔上(借助C塔),然后將A塔上最大的n號盤子移動到C塔上,最后將B塔上的n-1個盤子借助A塔移動到C塔上。
綜上所述,除了只有一個盤子時不需要借助其他塔外,其余情況均一樣(只是事件的復雜程度不一樣)。
看過上面這段話,應該都能寫出來代碼了,不過為了照顧以后的自己,我還是把我自己的思路寫下來吧:
三個塔:A,B,C。需要把盤子從A中移動到C中。
三個塔在每次移動時每個都有一個身份,分別為“源塔”(最開始盤子最多的塔)、“目標塔”(最后要把所有的盤子堆起來的塔)、“輔助塔”(與前面兩個相對而言),每個塔與身份並不固定。
當只有一個盤子的時候:直接把A(源塔)中的盤子放到C(目標塔)中。結束!
當有兩個盤子的時候【目標:把A(源塔)的兩個盤子放到C(目標塔)中】:只用三步:
- 把A(源塔)中最小的盤子放到B(輔助塔)中
- 再將A(源塔)中的第二個盤子放到C(目標塔)中
- 最后再把B(輔助塔)中的最小的盤子放到C(目標塔)中
結束!
當有三個盤子的時候:用兩個盤子時的思維,只用三步:
- 我們只用將A(源塔)中的前兩個盤子放到B(輔助塔)中
- 再將A(源塔)中第三個盤子放到C(目標塔)中
- 最后再把B(輔助塔)中的兩個盤子放到C(目標塔)中
結束——就怪了。因為還沒考慮怎么將A的兩個盤子放到B中,所以這個時候的目標就變了,變成了把A中的前兩個盤子放到B中,這時候就很明顯了,只要把塔的身份變換一下,就和兩個盤子時的操作一模一樣了。之后第二步也很簡單。到了第三步,這個時候,A沒有盤子,B有兩個盤子,C有一個最大的盤子,這個時候的目標是把B中的兩個盤子放到C中。是的,和兩個盤子時的目標又是類似,所以替換一下身份再來一次,問題就真的解決了!
所以,當有N個盤子的時候,我們也只要三步:
- 把A的N-1個盤子放到B中
- 把A所剩下唯一的也是最大的盤子放到C中
- 把B中所有的盤子都放到C中
對於第一步,我們又可以轉化為這三步:
此時的目標是把盤子從A放到B中
- 把A的N-2個盤子放到C中
- 把A最大的盤子(對於那N-1個盤子來說)放到B中
- 把C中所有的盤子都放到B中
對於這個的第一步,我們又可以……
由此,我們就可以寫出代碼:
代碼
其中的moveTopTo
和moveDisks
方法為需要填補的內容
public class Tower {
private Stack<Integer> disks;
// create three towers (i from 0 to 2)
public Tower(int i) {
disks = new Stack<Integer>();
}
// Add a disk into this tower
public void add(int d) {
if (!disks.isEmpty() && disks.peek() <= d) {
System.out.println("Error placing disk " + d);
} else {
disks.push(d);
}
}
// @param t a tower
// Move the top disk of this tower to the top of t.
public void moveTopTo(Tower t) {
// Write your code here
if (t.disks.isEmpty() || (!disks.isEmpty() && t.disks.peek() >= disks.peek())) {
t.disks.push(disks.pop());
}
}
// @param n an integer
// @param destination a tower
// @param buffer a tower
// Move n Disks from this tower to destination by buffer tower
public void moveDisks(int n, Tower destination, Tower buffer) {
// Write your code here
if (n <= 0) {
return;
} else if (n == 1) {
moveTopTo(destination);
} else {
moveDisks(n - 1, buffer, destination); //將源塔前幾個盤子都放到輔助塔中
moveDisks(1, destination, buffer); //此時源塔只剩下一個最大的盤子,將它放到目標塔中
buffer.moveDisks(n - 1, destination, this); //最后再將輔助塔的全部盤子(因為此時目標塔上已經有了一個盤子,故輔助塔的盤子數為n-1)都放到目標塔上
}
}
public Stack<Integer> getDisks() {
return disks;
}
}
/**
* Your Tower object will be instantiated and called as such:
* Tower[] towers = new Tower[3];
* for (int i = 0; i < 3; i++) towers[i] = new Tower(i);
* for (int i = n - 1; i >= 0; i--) towers[0].add(i);
* towers[0].moveDisks(n, towers[2], towers[1]);
* print towers[0], towers[1], towers[2]
*/
寫在最后
這次可能寫的思路有點啰嗦,不過自己感覺理了一遍以后思路更加清晰了,這個方法是用的遞歸,我也看了非遞歸的方法,不過感覺非遞歸的思想感覺比這個更難理解,所以就放棄了,先把思路復制到這里,等以后有空的話再試着理一理吧。
其實算法非常簡單,當盤子的個數為n時,移動的次數應等於2^n – 1(有興趣的可以自己證明試試看)。后來一位美國學者發現一種出人意料的簡單方法,只要輪流進行兩步操作就可以了。首先把三根柱子按順序排成品字型,把所有的圓盤按從大到小的順序放在柱子A上,根據圓盤的數量確定柱子的排放順序:若n為偶數,按順時針方向依次擺放 A B C;若n為奇數,按順時針方向依次擺放 A C B。
⑴按順時針方向把圓盤1從現在的柱子移動到下一根柱子,即當n為偶數時,若圓盤1在柱子A,則把它移動到B;若圓盤1在柱子B,則把它移動到C;若圓盤1在柱子C,則把它移動到A。
⑵接着,把另外兩根柱子上可以移動的圓盤移動到新的柱子上。即把非空柱子上的圓盤移動到空柱子上,當兩根柱子都非空時,移動較小的圓盤。這一步沒有明確規定移動哪個圓盤,你可能以為會有多種可能性,其實不然,可實施的行動是唯一的。
⑶反復進行⑴⑵操作,最后就能按規定完成漢諾塔的移動。
所以結果非常簡單,就是按照移動規則向一個方向移動金片:
如3階漢諾塔的移動:A→C,A→B,C→B,A→C,B→A,B→C,A→C
來自漢諾塔_百度百科