漢諾塔問題總結

一、基本遞歸實現

下面是普遍的漢諾塔問題的遞歸解法代碼

public class Hanoi{
    public static void hanoi(int n, String x, String y, String z){
        if (n == 1){
            System.out.println(x + " -> " + z);
        }else {
            hanoi(n - 1, x, z, y);
            System.out.println(x + " -> " + z);
            hanoi(n - 1, y, x, z);
        }
    }
    public static void main(String[] args) {
        hanoi(3, "x", "y", "z");
    }
}

輸出如下:

x -> z
x -> y
z -> y
x -> z
y -> x
y -> z
x -> z


二、基本非遞歸實現

非遞歸實現的方式本質就是嘗試使用棧來模擬遞歸

1.創建一個保存狀態的類

public class State {
    public int n; // 當前層數
    public String x; // 起始柱
    public String y; // 輔助柱
    public String z; // 目標柱

    public State(int n, String x, String y, String z) {
        this.n = n;
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

2.實現主程序

public class Hanoi {

    public void hanoi(int n, String x, String y, String z) {
        Stack<State> s = new Stack<>();
        s.push(new State(n, x, y, z));
        State state;
        while (!s.empty() && (state = s.pop()) != null) {
            if (state.n == 1) {
                System.out.println(state.x + " -> " + state.y);
            } else {
                // 棧結構先進後出,所以需要逆序進棧,這部分是理解重點,用棧模擬遞歸
                s.push(new State(state.n - 1, state.y, state.x, state.z));
                s.push(new State(1, state.x, state.y, state.z));
                s.push(new State(state.n - 1, state.x, state.z, state.y));
            }
        }
    }

    public static void main(String[] args) {
        Hanoi hanoi = new Hanoi();
        System.out.println();
        System.out.println("非遞歸方式:");
        Hanoi.hanoi(3, "x", "y", "z");
    }
}

3.輸出結果

非遞歸方式:
x -> z
x -> y
z -> y
x -> z
y -> x
y -> z
x -> z


三、漢諾塔問題擴展

我們嘗試更改一下題目要求,不只是需要輸出交換步驟,我們還需要記錄交換過程中的三個柱子的圓盤存在情況

1.創建柱子類

該類用於表示漢諾塔的每一個柱子,並且這個類將記錄每個柱子上的圓盤情況

public class HanoiPillar {
    public int n; // 記錄傳遞hanoi的圓盤數量
    public String name; // 柱子名稱
    public ArrayList<Integer> arr = new ArrayList<>(); //用於記錄當前柱子上所存在的圓盤

    // 初始化A柱
    public HanoiPillar(int n, String name) {
        this.n = n;
        this.name = name;
        for (int i = n; i > 0; i--) {
            this.arr.add(i);
        }
    }

    // 初始化B柱和C柱
    public HanoiPillar(String name) {
        this.name = name;
    }

    // 判斷該柱子上方是否爲頂部盤子
    public boolean top() {
        boolean result = false;
        if (!arr.isEmpty() && arr.size() != 0 && arr.get(arr.size() - 1) == 1) {
            result = true;
        }
        return result;
    }

    public void moveTo(HanoiPillar hanoiPillar) {
        hanoiPillar.arr.add(this.getDiskSize());
        this.arr.remove(this.arr.size() - 1);
        System.out.println(this.name + " -> " + hanoiPillar.name);
    }

    // 得到當前柱子的圓盤的列表,轉化爲String
    public String getStore() {
        StringBuilder result = new StringBuilder();
        if (this.arr.size() > 0) {
            for (int i = this.arr.size() - 1; i >= 0; i--) {
                result.append(this.arr.get(i)).append(",");
            }
        }
        return result.length() == 0 ? "null" : result.toString().trim();
    }

    // 得到該柱子中最小的圓盤的數值。以1、2、3、4、......、n來表示各個圓盤的大小。並且方便比較
    public Integer getDiskSize() {
        return this.arr.get(this.arr.size() - 1);
    }
}

2.實現主程序

public class Hanoi {
    private int n;
    private int step;
    private HanoiPillar a;
    private HanoiPillar b;
    private HanoiPillar c;

    public void hanoi(int n, String a, String b, String c) {
        this.step = (int) (Math.pow(2, n) - 1);
        this.a = new HanoiPillar(n, a);
        this.b = new HanoiPillar(b);
        this.c = new HanoiPillar(c);
        this.n = n;
        if (n % 2 != 0) {
            HanoiPillar tmp = this.b;
            this.b = this.c;
            this.c = tmp;
        }
        while (this.step > 0) {

            // 進行top的移動
            if (this.a.top()) {
                list();
                this.a.moveTo(this.b);
                this.step--;
            } else if (this.b.top()) {
                list();
                this.b.moveTo(this.c);
                this.step--;
            } else if (this.c.top()) {
                list();
                this.c.moveTo(this.a);
                this.step--;
            }

            // 因爲step爲奇數,而最後完成後step=0;
            // 同時可以理解爲最後一次移動一定是top移動,所以需要進行一次循環判斷
            if (this.step == 0) {
                break;
            }

            // 執行第二步移動
            if (this.a.top()) {
                move2(this.b, this.c);
            } else if (this.b.top()) {
                move2(this.a, this.c);
            } else if (this.c.top()) {
                move2(this.a, this.b);
            }
        }
        list();
    }

    private void list() {
        if (n % 2 == 0) {
            System.out.print(this.a.name + "柱:" + this.a.getStore() +
                    "		" + this.b.name + "柱:" + this.b.getStore() +
                    "		" + this.c.name + "柱:" + this.c.getStore() + "			");
        } else {
            System.out.print(this.a.name + "柱:" + this.a.getStore() +
                    "		" + this.c.name + "柱:" + this.c.getStore() +
                    "		" + this.b.name + "柱:" + this.b.getStore() + "			");
        }

    }

    // 執行第二部移動
    private void move2(HanoiPillar a, HanoiPillar b) {
        if (a.arr.size() == 0) { // a柱爲空,則將b上層的盤子移到a
            list();
            b.moveTo(a);
            this.step--;
        } else if (b.arr.size() == 0) { // b柱爲空,則將a上層的盤子移到b
            list();
            a.moveTo(b);
            this.step--;
        } else if (a.getDiskSize() > b.getDiskSize()) { // 由於b盤子小於a盤子,所以將b的top盤子移動到a的top盤子
            list();
            b.moveTo(a);
            this.step--;
        } else { // 由於a盤子小於b盤子,所以將a的top盤子移動到b的top盤子
            list();
            a.moveTo(b);
            this.step--;
        }
    }

    public static void main(String[] args) {
        Hanoi hanoi = new Hanoi();
        Hanoi.hanoi(3, "A", "B", "C");
    }
}

3.輸出結果

A柱:1,2,3,		B柱:null		C柱:null       A -> C
A柱:2,3,		B柱:null		C柱:1,			    A -> B
A柱:3,		        B柱:2,		        C柱:1,			    C -> B
A柱:3,		        B柱:1,2,		C柱:null			A -> C
A柱:null		B柱:1,2,		C柱:3,			    B -> A
A柱:1,		        B柱:2,		        C柱:3,			    B -> C
A柱:1,		        B柱:null		C柱:2,3,		    A -> C
A柱:null		B柱:null		C柱:1,2,3,	


四、漢諾塔問題改編(遞歸實現)

今天在做《程序員代碼面試指南:IT名企算法與數據結構題目最優解(第二版)》時,遇到了一個稍微複雜一些的漢諾塔問題,但是理解之後發現本體只是在上面簡易遞歸的基礎上進行優化。

1.題目要求

【題目】
 *  漢諾塔問題比較經典,這裏修改一下遊戲規則:
 *  現在限制不能從最左側的塔直接移動到最右側,也不能從最右側直接移動到最左側,而是必須經過中間。
 *  求當塔有N層的時候,打印最優移動過程和最優移動總步數。
 *  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”。
 *
 *  例如,當塔數爲兩層時,最上層的塔記爲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 wi11 move 8 steps.

2.情況分析

首先我們嘗試使用遞歸方式實現,然後進行常見情況分析

  1. 假設剩餘N層塔都在,希望全都移到,則有下面三個步驟

    1. 將1~N-1層從移到,該過程爲遞歸
    2. 將N層從移到
    3. 將1~N-1層從移到,該過程爲遞歸
  2. 假設剩餘N層塔都是從移到,或者從移到,或者從有,其實原理與情況1相同,所以不做贅述

  3. 假設剩餘N層塔都在,希望都移到,則有下面五個步驟

    1. 將1~N-1層從移到,該過程爲遞歸
    2. 將N層從移到
    3. 將1~N-1層從移到,此過程爲遞歸
    4. 將N層從移到
    5. 將1~N-1層從移到,此過程爲遞歸

3.實現主程序

public class Hanoi {

    public int hanoiProblem (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) {
        Hanoi hanoi = new Hanoi();
        hanoi.hanoiProblem(3, "x", "y", "z");
    }
}

4.輸出結果

Move 1 from x to y
Move 1 from y to z
Move 2 from x to y
Move 1 from z to y
Move 1 from y to x
Move 2 from y to z
Move 1 from x to y
Move 1 from y to z
Move 3 from x to y
Move 1 from z to y
Move 1 from y to x
Move 2 from z to y
Move 1 from x to y
Move 1 from y to z
Move 2 from y to x
Move 1 from z to y
Move 1 from y to x
Move 3 from y to z
Move 1 from x to y
Move 1 from y to z
Move 2 from x to y
Move 1 from z to y
Move 1 from y to x
Move 2 from y to z
Move 1 from x to y
Move 1 from y to z


五、漢諾塔問題改編(非遞歸實現)

1.題目分析

我們把左、中、右三個地點抽象成棧,依次記爲LS、MS和RS。最初所有的塔都在LS上。那麼如上4個動作就可以看作是:某一個棧(from)把棧頂元素彈出,然後壓入到另一個棧裏(to),作爲這一個棧(to)的棧頂。

例如,如果是7層塔,在最初時所有的塔都在LS上,LS從棧頂到棧底就依次是1~7,如果現在發生了“左”到“中”的動作,這個動作對應的操作是LS棧將棧頂元素1彈出,然後1壓入到MS棧中,成爲MS的棧頂。其他操作同理。

一個動作能發生的先決條件是不違反小壓大的原則
from棧彈出的元素num如果想壓入到to棧中,那麼num的值必須小於當前to棧的棧頂。還有一個原則不是很明顯,但也是非常重要的,叫相鄰不可逆原則,解釋如下:

  1. 我們把4個動作依次定義爲:L->MM->LM->RR->M
  2. 很明顯,L->MM->L過程互爲逆過程,M->RR->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->RR->M只會有一個達標。
假設前一步的動作是M->L
  1. 根據小壓大原則,M->L的動作不會重複發生。
  2. 根據相鄰不可逆原則,L->M的動作也不該發生。
  3. 根據小壓大原則,M->RR->M只會有一個達標。
假設前一步的動作是M->R
  1. 根據小壓大原則,M->R的動作不會重複發生。
  2. 根據相鄰不可逆原則,R->M的動作也不該發生。
  3. 根據小壓大原則,L->MM->L只會有一個達標。
假設前一步的動作是R->M
  1. 根據小壓大原則,R->M的動作不會重複發生。
  2. 根據相鄰不可逆原則,M->R的動作也不該發生。
  3. 根據小壓大原則,L->MM->L只會有一個達標。

綜上所述,每一步只會有一個動作達標。那麼只要每走一步都根據這兩個原則考查所有的動作就可以,哪個動作達標就走哪個動作,反正每次都只有一個動作滿足要求,按順序走下來即可

2.實現主程序

public class Hanoi {

    public int hanoiProblem(int num, String left, String mid, String right) {
        Stack<Integer> lS = new Stack<>();
        Stack<Integer> mS = new Stack<>();
        Stack<Integer> rS = new Stack<>();
        // 初始化這四個棧,可以避免棧溢出問題,同時可以使while循環中的四個函數無論誰在前,都必定會限制性 l -> m
        lS.push(Integer.MAX_VALUE);//最大值:2147483647(2的7次方-1)
        mS.push(Integer.MAX_VALUE);
        rS.push(Integer.MAX_VALUE);
        for (int i = num; i > 0; i--) {//將數字(最小數字在棧頂)壓入左棧[1,2,3]
            lS.push(i);
        }
        //調用枚舉,記錄上一步操作 創建一個數組而不是直接創建一個Action對象是爲了使用引用傳遞而不是值傳遞
        Action[] record = {Action.No};
        int step = 0;
        //size();stack類從vector繼承的方法;返回此向量中的組件數
        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, left, mid);
        }
        return step;
    }

    public static int fStackToStack(Action[] record,
                                    Action preNoAet,
                                    Action nowAct,
                                    Stack<Integer> fStack,
                                    Stack<Integer> tStack,
                                    String from,
                                    String to) {
        // fStack.peek() < tStack.peek() 必然可以保證 record[0] != nowAct 兩條件互斥
        if (record[0] != preNoAet && fStack.peek() < tStack.peek()) {//發生移動且必須小的數字往大的數字上移動
            tStack.push(fStack.pop());//fStack 移動到 tStack 且刪掉from的棧頂元素
            System.out.println("Move " + tStack.peek() + " from " + from + " to " + to);
            record[0] = nowAct;
            return 1;
        }
        return 0;
    }

    public static void main(String[] args) {
        Hanoi Hanoi = new Hanoi();
        int step = hanoi.hanoiProblem(3, "左", "中", "右");
        System.out.println("總共需要" + step + "步");
    }

    enum Action {
        No, // 無操作
        LToM, // 從左移到中
        MToL, // 從中移到左
        MToR, // 從中移到右
        RToM // 從右移到中
    }
}

3.輸出結果

Move 1 from 左 to 中
Move 1 from 中 to 右
Move 2 from 左 to 中
Move 1 from 左 to 中
Move 1 from 中 to 左
Move 2 from 中 to 右
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 3 from 左 to 中
Move 1 from 左 to 中
Move 1 from 中 to 左
Move 2 from 左 to 中
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 2 from 中 to 左
Move 1 from 左 to 中
Move 1 from 中 to 左
Move 3 from 中 to 右
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 2 from 左 to 中
Move 1 from 左 to 中
Move 1 from 中 to 左
Move 2 from 中 to 右
Move 1 from 左 to 中
Move 1 from 中 to 右
總共需要26步

參考視頻
https://www.bilibili.com/video/av31023017?from=search&seid=15595573244367663980

參考文章
https://blog.csdn.net/weixin_42636076/article/details/81031580
https://www.jb51.net/article/128701.htm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章