用棧來求解漢諾塔問題
【題目】
漢諾塔問題比較經典,這裏修改一下游戲規則:現在限制不能從左側的塔直接移動到右側,也不能從最右側直接移動到最左側,而是必須經過中間。求當塔又N層的時候,打印最優移動過程和最有移動總步數。
例如,當塔數爲兩層時,最上層的塔記爲1,最下層的塔記爲2,則打印:
Move 1 from left to mid
Move 1 from mid right
Move 2 form to mid
Move 1 from right to mid
Move 1 from mid to left
Move 2 from mid to right
Move 1 from left to mid
Move 1 from mid to right
It will move steps
【要求】
用以下兩種方法解決:
- 方法一:遞歸的方法
- 方法二:非遞歸的方法,用棧來模擬漢諾塔的三個塔
【解答】
方法一:遞歸的方法
首先:如果只剩下最上層的塔需要移動,則有如下處理:
- 如果希望從“左”移到“中”,打印“Move 1 from left to mid”。
- 如果希望從“中”移到“左”,打印“Move 1 from mid to left 。
- 如果希望從“中”移到“右”,打印“Move 1 from mid to right
- 如果希望從“右”移到“中”,打印“Move 1 from right to mid
- 如果希望從“左”移到“右”,打印“Move 1 from left to mid”。“Move 1 from mid to right” 。
- 如果希望從“右”移到“左”,打印“Move 1 from right to mid”和“Move 1 from mid to left”。
以上就是遞歸的中之條件,也就是隻剩上層塔時的打印過程
接下來,我們分析剩下多層塔的情況
如果剩下N層,從最上到最下依次爲1~N,則有如下判斷:
- 如果剩下的N層塔都在“左”,希望全部移到“中”,則有三個步驟:
- 將1~N-1層塔先從“左”移到“右”,明顯交給遞歸過程。
- 將第N層塔從“左”移到“中”。
- 再將1-N-1層塔全部從“右”移到“中”,明顯交給遞歸過程
- 如果把剩下的N層塔全部從“中”移到“左”,從“中”移到“右”,從“右”移到“中”,過程與1同理,一樣分爲三步。
- 如果剩下的塔都在“左”,希望全部移到“右”則有五個步驟。
- 將1-N-1層塔先全部從“左”移到“右”,明顯交給遞歸過程。
- 將第N層塔從“左”移到“中”。
- 將1~N-1層塔全部從“右”移到“左”,明顯交給遞歸過程。
- 將第N層塔從“中”移到“右”。
- 將1~N-1層塔全部從“左”移到“右”,明顯交給遞歸過程。
- 如果剩下的N層都在“右”,希望全部移到“左”過程與情況3同理,一樣是分解爲5部。
以上遞歸過程經過邏輯化簡之後的代碼請參考如下hanoProblem1方法。
public class HanoiProblem1 {
public int hanoiProblem1(int num, String left, String mid, String right) {
if (num < 1) {
return 0;
}
return process(num, left, mid, right, left, right);
}
public int process(int num, String left, String mid, String right, String from, String to) {
if (num == 1) {
if (from.equals(mid) || to.equals(mid)) {
System.out.println("Move 1 from " + from + " to " + to);
return 1;
} else {
System.out.println("Move 1 from " + from + " to " + mid);
System.out.println("Move 1 from " + mid + " to " + to);
return 2;
}
}
if (from.equals(mid) || to.equals(mid)) {
String another = (from.equals(left) || to.equals(left)) ? right : left;
int part1 = process(num - 1, left, mid, right, from, another);
int part2 = 1;
System.out.println("Move " + num + " from " + from + " to " + to);
int part3 = process(num - 1, left, mid, right, another, to);
return part1 + part2 + part3;
} else {
int part1 = process(num - 1, left, mid, right, from, to);
int part2 = 1;
System.out.println("Move " + num + " from " + from + " to " + mid);
int part3 = process(num - 1, left, mid, right, to, from);
int part4 = 1;
System.out.println("Move " + num + " from " + mid + " to " + to);
int part5 = process(num - 1, left, mid, right, from, to);
return part1 + part2 + part3 + part4 + part5;
}
}
public static void main(String[] args) {
HanoiProblem1 test = new HanoiProblem1();
test.hanoiProblem1(3, "left", "mid", "right");
}
}
方法二:非遞歸的方法——用棧來模擬整個過程。
修改後的漢諾塔問題不能讓任何塔從“左”直接移動到“右”,也不能從“右”直接移到“左”,而是要通過中間過程。也就是說,實際動作只有4個:“左”到“中”、“中”到“左”、“中”到“右”。
現在我們把左、中、右三個地點抽象成棧,依次記爲LS、MS和RS。最初所有的塔都在LS上。那麼如上4個動作就可以看作是:某一個棧(from)把棧頂元素彈出,然後壓入另一個棧裏(to),作爲這一棧(to)的棧頂
例如,如果是7層塔,再最初時所有的塔都在LS上,LS從棧頂到棧底依次時1~7,如果現在發生了“左”到“中”的動作,這個動作對應的操作時LS將棧頂元素1彈出,然後1壓入到MS棧中,成爲MS的棧頂。其他同理。
一個動作能發生的先決條件時不違反小壓大的原則。
form棧彈出的元素num如果想要壓入to棧中,那麼num的值必須小於當前to棧中的扎頂。
還有一個原則不是很明顯,但也是非常重要的,教相鄰不可逆原則,解釋如下:
1. 我們把4個動作依次定義爲:L->M、M->L、M->R和R->M。
2. 很明顯,L->M和M->L過程會爲逆過程,M->R和R->M互爲逆過程。
3. 在修改後的漢諾塔遊戲中,如果想走出最少步數,那麼任何兩個相鄰的動作都不爲逆過程的。舉個例子:如果上一步的動作是L->M,那麼這一步絕對不能爲M->L,直觀的解釋爲:你在上一步把一個棧頂數從“左”移到“中”,這一步你爲什麼要移回去呢?這必然不是取得最小步數的方法。同理,M->R動作和R->M動作也不可能相鄰發生。
有了小壓大和相鄰不可逆原則後,我們可以推導出兩個十分有用的結論——非遞歸的方法核心
1. 遊戲的第一個動作一定是L->M,這是顯而易見的。
2. 在走出最少步數過程中的任何時刻,4個動作只有一個動作不違反小壓大和相鄰不可逆原則,另外三個動作一定會違反。
對於結論2,現在進行簡單的證明
因爲有的第一個動作已經確定是L->M,則以後的每一步都會有前一步的動作。
假設前一步的動作是L->M:
- 根據小壓大原則,L->M的動作不會重複發生。
- 根據相近不相鄰原則,M->L的動作也不該發生。
- 根據小壓大原則,M->R和R->M只會有一個達標。
假設前一步的動作是M->L:
- 根據小壓大原則,M->L的動作不會重複發生。
- 根據相近不相鄰原則,L->M的動作也不該發生。
- 根據小壓大原則,M->R和R->M只會有一個達標。
假設前一步的動作是M->R:
- 根據小壓大原則,M->R的動作不會重複發生。
- 根據相近不相鄰原則,R->M的動作也不該發生。
- 根據小壓大原則,L->M和M->L只會有一個達標。
假設前一步的動作是R->M:
- 根據小壓大原則,R->M的動作不會重複發生。
- 根據相近不相鄰原則,M->R的動作也不該發生。
- 根據小壓大原則,L->M和M->L只會有一個達標。
綜上所述,每一步只會有一個動作達標,那麼只要沒走一步都根據這兩個原則考察所有的東歐工作就可以,哪個動作達標就走哪個動作,反正每次都只有一個動作滿足要求,按順序走下來即可。
**非遞歸的具體過程請參考hanolProblem2方法
public class HanoiProblem2 {
public enum Action {
No, LToM, MToL, MToR, RToM
}
public int hanoiProblem2(int num, String left, String mid, String right) {
Stack<Integer> lS = new Stack<Integer>();
Stack<Integer> mS = new Stack<Integer>();
Stack<Integer> rS = new Stack<Integer>();
lS.push(Integer.MAX_VALUE);
mS.push(Integer.MAX_VALUE);
rS.push(Integer.MAX_VALUE);
for (int i = num; i > 0; i--) {
lS.push(i);
}
Action[] record = { Action.No };
int step = 0;
while (rS.size() != num + 1) {
step += fStackToStack(record, Action.MToL, Action.LToM, lS, mS, left, mid);
step += fStackToStack(record, Action.LToM, Action.MToL, mS, lS, mid, left);
step += fStackToStack(record, Action.RToM, Action.MToR, mS, rS, mid, right);
step += fStackToStack(record, Action.MToR, Action.RToM, rS, mS, right, mid);
}
return step;
}
public static int fStackToStack(Action[] record, Action preNoAct, Action nowAct, Stack<Integer> fStack,
Stack<Integer> tStack, String from, String to) {
if (record[0] != preNoAct && fStack.peek() < tStack.peek()) {
tStack.push(fStack.pop());
System.out.println("Move " + tStack.peek() + " from " + from + " to " + to);
record[0] = nowAct;
return 0;
}
return 0;
}
}