迷宮算法全部解(搜索算法與剪枝思考)

迷宮嘛,就是包含一個起始點(startx,starty)和一個終點(endx,endy),中間包含被牆堵住無法移動的區域({(x,y)....}),以及大小(也就是所謂的邊界),,長(m),寬(n)

(不規則迷宮可以通過添加點到無法移動的集合形成)


下面就是生成這樣一個迷宮的方法:

    int m;//長
    int n;//寬
    int maze[][];//迷宮,有障礙值爲1,無爲0

    int startx;//起點x
    int starty;//起點y
    int endx;//終點x
    int endy;//終點y

    /**
     * 生成迷宮
     * @throws IOException
     */
    public void createMaze() throws IOException {

        System.out.println("輸入 m");
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        m=Integer.parseInt(br.readLine().trim());
        System.out.println("輸入 n");
        n=Integer.parseInt(br.readLine().trim());
        maze=new int[m][n];

        while(true){
            System.out.println("輸入障礙橫縱座標,以空格隔開,退出請輸入'esc'");
            String temp=br.readLine().trim();
            if(temp.trim().equals("esc")){
                break;
            }

            int x=Integer.parseInt(temp.split(" ")[0]);
            int y=Integer.parseInt(temp.split(" ")[1]);
            maze[x][y]=1;
        }
        String s;
        System.out.println("輸入起始點座標,以空格隔開");
        s=br.readLine().trim();
        startx=Integer.parseInt(s.split(" ")[0]);
        starty=Integer.parseInt(s.split(" ")[1]);
        System.out.println("輸入終止點座標,以空格隔開");
        s=br.readLine().trim();
        endx=Integer.parseInt(s.split(" ")[0]);
        endy=Integer.parseInt(s.split(" ")[1]);

        if(maze[startx][starty]==1||maze[endx][endy]==1){
            System.out.println("想搞事?");
        }

        br.close();
}
其中我忽略了很多限制條件的判斷,實在太麻煩了,我推薦參考的人記得自己限制下傳入參數。


接下去就是使用回朔法來遍歷這個迷宮,找到道路。

思路如下:

1、 這個方向有路可走,我沒走過
2、 往這個方向前進
3、 是死衚衕,往回走,回到上一個路口
4、 重複第一步,直到找着出口

使用數組構造棧用以存儲走過的道路,以下是每一步的結構和棧的構造:

public class Step {
    private int x;
    private int y;
    private int steps;//步長

    public Step() {
    }

    public Step(int x, int y, int steps) {
        this.x = x;
        this.y = y;
        this.steps = steps;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getSteps() {
        return steps;
    }

    public void setSteps(int steps) {
        this.steps = steps;
    }

    @Override
    public String toString() {
        return "Step{" +
                "x=" + x +
                ", y=" + y +
                ", steps=" + steps +
                '}';
    }
}
步長是我預留的參數,因爲不是每一步都一定只能走1的,這涉及可能會出現的拓撲結構中線也許會存在權值或是步長會自增長的一些問題等等。

此例中我會將步長均傳入1。

棧結構(數組實現):

public class ArrayStack{
    private Object[] contents;
    private int top;//top標記下一個入棧的位置,同時也表示棧的容量大小
    private static int SIZE = 1000000;//初始大小

    public ArrayStack()
    {
        contents = new Object[SIZE];
        top = 0;
    }

    public void expand(){//藉助於申請一個輔助空間,每次擴展容量一倍
        Object[] larger = new Object[size()*2];
        for(int index = 0;index < top;index++)
            larger[index] =  contents[index];
        contents = larger;
    }

    public int size() {
        return top;
    }

    public boolean isEmpty() {
        return (size() == 0);
    }

    public void push(Object element) {
        if(top == contents.length)
            expand();
        contents[top] = element;
        top++;
    }

    public Object pop() {
        if(isEmpty())
        {
            System.out.println("stack is empty!");
            System.exit(1);
        }
        Object result = contents[top-1];
        contents[top-1] = null;//出棧
        top--;
        return result;
    }

    public Object peek() {
        Object result;
        if(isEmpty())
            result = null;
        else
            result = contents[top-1];
        return result;
    }

    public void printStack(){
        for(int i=0;i<size();i++){
            System.out.print(contents[i].toString()+";");
        }
        System.out.println();
    }
}

爲什麼用棧,原因很簡單,走下一步到終點或是走到死衚衕都會回溯到上個狀態,即後進的那一步先出去,沒什麼比棧更合適了。


接下去是求出所有迷宮解的算法,爲了防止重複走過同一點(重複走過一點那其實這個解是沒有意義的,因爲你可以靠循環走過這個點構造無數個解,所以這裏求出的解其實存在唯一性,即每個點至多經過一次),走過之後點的值會變成-1:

/**
     *基本回溯法
     * @param x
     * @param y
     * @param steps 步長
     */
    public void move1(int x,int y,int steps){
        if(x==endx&&y==endy){
            Step step = new Step(x, y,steps);
            arrayStack.push(step);
            arrayStack.printStack();

            arrayStack.pop();//回溯  注意這裏也要回溯
        }else {
            if (maze[x][y] == 0) {
                maze[x][y] = -1;//走過

                Step step = new Step(x, y,steps);
                arrayStack.push(step);//記錄

                if (x - steps >= 0) {
                    move1(x - steps, y,steps);
                }//向左
                if (y - steps >= 0) {
                    move1(x, y - steps,steps);
                }//向上
                if (x + steps <= m - 1) {
                    move1(x + steps, y,steps);
                }//向右
                if (y + steps <= n - 1) {
                    move1(x, y + steps,steps);
                }//向下

                maze[x][y] = 0;
                arrayStack.pop();//回溯
            }
        }
    }
讓我們想一下,其實我們可以早在走到下一步時候就可以知道接下去有哪些步可以走,即剪去搜下去也不會有結果的點(本例中爲值爲1或者-1的點),於是我們可以得到下面的優化:

  /**
     *剪去下個結點是無法通過的枝
     * @param x
     * @param y
     * @param steps 步長
     */
    public void move2(int x,int y,int steps){
        if(x==endx&&y==endy){
            Step step = new Step(x, y,steps);
            arrayStack.push(step);
            arrayStack.printStack();

            arrayStack.pop();//回溯
        }else {
            maze[x][y]=-1;//走過

            Step step = new Step(x, y,steps);
            arrayStack.push(step);//記錄

            if (x - steps >= 0 ) {
                if(maze[x-steps][y]==0) {
                    //可以向左
                    move2(x - steps, y,steps);
                }
            }//向左
            if (y - 1 >= 0 ) {
                if(maze[x][y-steps]==0) {
                    //可以向上
                    move2(x, y-steps,steps);
                }
            }//向上
            if (x + 1 <= m - 1 ) {
                if(maze[x+steps][y]==0) {
                    //可以向右
                    move2(x+steps, y,steps);
                }
            }//向右
            if (y + steps <= n - 1) {
                if(maze[x][y+steps]==0) {
                    //可以向上
                    move2(x, y+steps,steps);
                }
            }//向下

            maze[x][y] = 0;
            arrayStack.pop();//回溯
        }
    }

(這裏說一下,其實我測試下來的結果是有點沮喪的,第二種並沒有比第一種快,但我個人感覺是優化的額,也許咩有吧。。。)


這樣的話,其實完全可以從這麼多解中找到合計步長最小的值作爲最優解,你也可以理解爲迷宮搜索算法的最優解。


接下去的剪枝優化也很好理解了,假定有一個上界爲無窮大,使用解的最小值不停更新這個上界。如果某一次在求解的過程中發現步長已經超過了這個上界,那麼也就沒有必要繼續搜索下去了,因爲繼續下去也肯定不會是最優解了,相當於剪去此點以及之後搜索到終點的全部解,這樣就可以省下許多搜索時間。

(偷個懶沒寫代碼吐舌頭


接下去說點概念性的東西:

搜索算法,絕大部分需要用到剪枝. 然而,不是所有的枝條都可以剪掉,這就需要通過設計出合理的判斷方法,以決定某一分支的取捨. 在設計判斷方法的時候,需要遵循一定的原則.
剪枝的原則
1、正確性
   正如上文所述,枝條不是愛剪就能剪的. 如果隨便剪枝,把帶有最優解的那一分支也剪掉了的話,剪枝也就失去了意義. 所以,剪枝的前提是一定要保證不丟失正確的結果.
2、準確性
   在保證了正確性的基礎上,我們應該根據具體問題具體分析,採用合適的判斷手段,使不包含最優解的枝條儘可能多的被剪去,以達到程序“最優化”的目的. 可以說,剪枝的準確性,是衡量一個優化算法好壞的標準.
3、高效性
   設計優化程序的根本目的,是要減少搜索的次數,使程序運行的時間減少. 但爲了使搜索次數儘可能的減少,我們又必須花工夫設計出一個準確性較高的優化算法,而當算法的準確性升高,其判斷的次數必定增多,從而又導致耗時的增多,這便引出了矛盾. 因此,如何在優化與效率之間尋找一個平衡點,使得程序的時間複雜度儘可能降低,同樣是非常重要的. 倘若一個剪枝的判斷效果非常好,但是它卻需要耗費大量的時間來判斷、比較,結果整個程序運行起來也跟沒有優化過的沒什麼區別,這樣就太得不償失了.



剪枝算法按照其判斷思路可大致分成兩類:可行性剪枝及最優性剪枝.

結合上面來說,剪去不會走到的點其實就算是可行性剪枝,因爲他的意義是剪去不可行的枝。

通過上界來確定最優解,剪去比上界更大的枝,即最優性剪枝。(下界同理)


以上。。。

發佈了30 篇原創文章 · 獲贊 11 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章