題目描述
漢諾塔問題比較經典,這裏修改一下遊戲規則:現在限制不能從最左側的塔直接移動到最右側,也不能從最右側直接移動到最左側,而是必須經過中間。求當塔有n層的時候,打印最優移動過程和最優移動總步數。
輸入描述:
輸入一個數n,表示塔層數
輸出描述:
按樣例格式輸出最優移動過程和最優移動總步數
示例1
輸入
2
輸出
Move 1 from left to mid
Move 1 from mid to right
Move 2 from left 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 8 steps.
說明
當塔數爲兩層時,最上層的塔記爲1,最下層的塔記爲2
備註:
1 ≤ n ≤ 12
Solution
首先來看下遞歸解法:
import java.util.*;
public class Main {
private static String left = "left";
private static String mid = "mid";
private static String right = "right";
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
System.out.println("It will move " + hanoiProblem(n) + " steps.");
}
/**
* 遞歸解法
*
* @param num 漢諾塔層數
* @return 返回層數
*/
public static int hanoiProblem(int num) {
if (num < 1) {
return 0;
}
return process(num, left, right);
}
public static int process(int num, 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)) {
// another 爲除去目標和來源的另一個
String another = (from.equals(left)) || to.equals(left) ? right : left;
// 1. 先把 1 ~ n-1 層複製到另一個
int count1 = process(num - 1, from, another);
// 2. 把第 n 層複製到目的地,因爲目標和來源中有一個是 mid, 所以只需一步
int count2 = 1;
System.out.println("Move " + num + " from " + from + " to " + to);
// 3. 把 1 ~ n-1 層從另一個複製回 to
int count3 = process(num - 1, another, to);
return count1 + count2 + count3;
} else {
// 要麼從左到右,要麼從右到左,所以 another 一定是 mid
// 1.先把 1 ~ n-1 從 from 複製到 to
int count1 = process(num - 1, from, to);
// 2.把 n 從 from 複製到 mid
int count2 = 1;
System.out.println("Move " + num + " from " + from + " to " + mid);
// 3.把 1 ~ n-1 從 to 複製到 mid,完成全部n層從 from 到 mid 的複製
// 4.把 1 ~ n-1 從 mid 複製到 from
int count3 = process(num - 1, to, from);
// 5.把 n 從 mid 複製到 to
int count5 = 1;
System.out.println("Move " + num + " from " + mid + " to " + to);
// 6.把 1 ~ n-1 從 from 複製到 mid,再從 mid 複製到 to
int count6 = process(num - 1, from, to);
return count1 + count2 + count3 + count5 + count6;
}
}
}
接下來來看非遞歸解法,也就是用棧來解決。
/**
* 非遞歸解法:使用棧來模擬
*/
public enum Action {
No, LtoM, MtoL, MtoR, RtoM
}
private static Action record;
/**
* 非遞歸解法
*
* @param num 盤子數量
* @return
*/
public static int hanoiProblemByStack(int num) {
Stack<Integer> lStack = new Stack<>();
Stack<Integer> mStack = new Stack<>();
Stack<Integer> rStack = new Stack<>();
// 先壓入最大值,可以避免一開始和空棧比較時的報錯處理
lStack.push(Integer.MAX_VALUE);
mStack.push(Integer.MAX_VALUE);
rStack.push(Integer.MAX_VALUE);
for (int i = num; i > 0; i--) { // 一開始盤子都在 left 棧
lStack.push(i);
}
record = Action.No; // record 用於記錄前一個步驟
int step = 0;
// right 棧的大小爲 num + 1,則說明任務完成,可以結束了
while (rStack.size() != num + 1) { // 1 2 3 4
// 總共可能的方法有四種:從left->mid, mid->left, mid->right, right->mid
// 但要注意的是,爲了最優的方法,當前方法和前一個方法不能互逆,比如當前方法是left->mid,
// 則前一個方法不能是mid->left,否則當前方法就是執行完,那麼這兩個方法就等於白跑了
step += fStackTotStack(Action.MtoL, Action.LtoM, lStack, mStack, left, mid);
step += fStackTotStack(Action.MtoR, Action.RtoM, rStack, mStack, right, mid);
step += fStackTotStack(Action.LtoM, Action.MtoL, mStack, lStack, mid, left);
step += fStackTotStack(Action.RtoM, Action.MtoR, mStack, rStack, mid, right);
}
return step;
}
/**
* 判斷從棧 fStack 的棧頂移動到 tStack 是否可行,可行則移動
*
* @param preNoAct 當前動作不允許的前置動作
* @param nowAct 當前動作
* @param fStack 來源棧
* @param tStack 目標棧
* @param from 來源位置
* @param to 目標位置
* @return
*/
public static int fStackTotStack(Action preNoAct, Action nowAct,
Stack<Integer> fStack, Stack<Integer> tStack,
String from, String to) {
// 判斷前一個方法是否當前方法的互逆方法,不是的話,方可執行;
// 判斷來源的棧頂和目標的棧頂孰大孰小,必須滿足小頂壓入大頂
if (record != preNoAct && fStack.peek() < tStack.peek()) {
tStack.push(fStack.pop());
System.out.println("Move " + tStack.peek() + " from " + from + " to " + to);
record = nowAct;
return 1;
}
return 0;
}