codingInterview 用棧來求解漢諾塔問題

用棧來求解漢諾塔問題

【題目】

漢諾塔問題比較經典,這裏修改一下游戲規則:現在限制不能從左側的塔直接移動到右側,也不能從最右側直接移動到最左側,而是必須經過中間。求當塔又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

【要求】

用以下兩種方法解決:

  • 方法一:遞歸的方法
  • 方法二:非遞歸的方法,用棧來模擬漢諾塔的三個塔

【解答】

方法一:遞歸的方法

首先:如果只剩下最上層的塔需要移動,則有如下處理:

  1. 如果希望從“左”移到“中”,打印“Move 1 from left to mid”。
  2. 如果希望從“中”移到“左”,打印“Move 1 from mid to left 。
  3. 如果希望從“中”移到“右”,打印“Move 1 from mid to right
  4. 如果希望從“右”移到“中”,打印“Move 1 from right to mid
  5. 如果希望從“左”移到“右”,打印“Move 1 from left to mid”。“Move 1 from mid to right” 。
  6. 如果希望從“右”移到“左”,打印“Move 1 from right to mid”和“Move 1 from mid to left”。

以上就是遞歸的中之條件,也就是隻剩上層塔時的打印過程
接下來,我們分析剩下多層塔的情況

如果剩下N層,從最上到最下依次爲1~N,則有如下判斷:

  1. 如果剩下的N層塔都在“左”,希望全部移到“中”,則有三個步驟:
    1. 將1~N-1層塔先從“左”移到“右”,明顯交給遞歸過程。
    2. 將第N層塔從“左”移到“中”。
    3. 再將1-N-1層塔全部從“右”移到“中”,明顯交給遞歸過程
  2. 如果把剩下的N層塔全部從“中”移到“左”,從“中”移到“右”,從“右”移到“中”,過程與1同理,一樣分爲三步。
  3. 如果剩下的塔都在“左”,希望全部移到“右”則有五個步驟。
    1. 將1-N-1層塔先全部從“左”移到“右”,明顯交給遞歸過程。
    2. 將第N層塔從“左”移到“中”。
    3. 將1~N-1層塔全部從“右”移到“左”,明顯交給遞歸過程。
    4. 將第N層塔從“中”移到“右”。
    5. 將1~N-1層塔全部從“左”移到“右”,明顯交給遞歸過程。
  4. 如果剩下的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:

  1. 根據小壓大原則,L->M的動作不會重複發生。
  2. 根據相近不相鄰原則,M->L的動作也不該發生。
  3. 根據小壓大原則,M->R和R->M只會有一個達標。

假設前一步的動作是M->L:

  1. 根據小壓大原則,M->L的動作不會重複發生。
  2. 根據相近不相鄰原則,L->M的動作也不該發生。
  3. 根據小壓大原則,M->R和R->M只會有一個達標。

假設前一步的動作是M->R:

  1. 根據小壓大原則,M->R的動作不會重複發生。
  2. 根據相近不相鄰原則,R->M的動作也不該發生。
  3. 根據小壓大原則,L->M和M->L只會有一個達標。

假設前一步的動作是R->M:

  1. 根據小壓大原則,R->M的動作不會重複發生。
  2. 根據相近不相鄰原則,M->R的動作也不該發生。
  3. 根據小壓大原則,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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章