迷宫算法全部解(搜索算法与剪枝思考)

迷宫嘛,就是包含一个起始点(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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章