资讯专栏INFORMATION COLUMN

用并查集(find-union)实现迷宫算法以及最短路径求解

xiangchaobin / 2413人阅读

摘要:本人邮箱欢迎转载转载请注明网址代码已经全部托管有需要的同学自行下载引言迷宫对于大家都不会陌生那么迷宫是怎么生成已经迷宫要如何找到正确的路径呢用代码又怎么实现带着这些问题我们继续往下看并查集朋友圈有一种算法就做并查集什么意思呢比如现在有零

本人邮箱:
欢迎转载,转载请注明网址 http://blog.csdn.net/tianshi_kco
github: https://github.com/kco1989/kco
代码已经全部托管github有需要的同学自行下载

引言

迷宫对于大家都不会陌生.那么迷宫是怎么生成,已经迷宫要如何找到正确的路径呢?用java代码又怎么实现?带着这些问题.我们继续往下看.

并查集(find-union) 朋友圈

有一种算法就做并查集(find-union).什么意思呢?比如现在有零散的甲乙丙丁戊五个人.他们之间刚开始互相不认识.用代码解释就是find(person1, person2) == false,之后在某一次聚合中,认识了,认识了,认识了等等,那么就可以用代码解释如下.

    union("甲", "乙");
    union("乙", "丙");
    union("丙", "戊");
    union("丙", "丁");

那么这个时候就可以通过认识到了在通过认识到.
这是甲乙丙丁戊通过朋友或者朋友的朋友最终都互相认识.换另一种说法就是如果要认识,那么必须先通过认识,再通过去认识就行了.

迷宫

对于迷宫生成,其实更上面朋友圈有点类似.生成步骤如下

首先,先创建一个n*m的二维密室.每个单元格四方都是墙.

随机选择密室中的一个单元格.之后在随机选择一面要打通的墙壁.

判断要打通的墙壁是否为边界.是则返回步骤3,不是则继续

判断步骤的单元个和要打通的墙壁的对面是否联通(用find算法)

如果两个单元格不联通,则把步骤2选中的墙壁打通(用union算法).否则返回步骤2.

判断迷宫起点和终点是否已经联通,是则迷宫生成结束,否则返回步骤2.

对于并查集(find-union)的实现,网络上有不少实现,这里不展开说明了.

实现代码 墙壁
public enum Wall {
    /**
     * 墙壁
     */
    BLOCK,
    /**
     * 通道
     */
    ACCESS
}

墙壁,是一个枚举变量,用于判断当前的墙壁是否可以通行.

单元格
public class Box {
    private Wall[] walls;

    public Box(){
        walls = new Wall[4];
        for (int i = 0; i < walls.length; i ++){
            walls[i] = Wall.BLOCK;
        }
    }

    public void set(Position position, Wall wall){
        walls[position.getIndex()] = wall;
    }

    public Wall get(Position position){
        return walls[position.getIndex()];
    }
}

一个单元格(小房间)是有四面墙组成的,刚开始四面墙都是墙壁.

方向
public enum Position {
    TOP(0), RIGHT(1), DOWN(2), LEFT(3);

    public int index;

    Position(int index) {
        this.index = index;
    }

    public static Position indexOf(int index){
        int pos = index % Position.values().length;
        switch (pos){
            case 0:
                return TOP;
            case 1:
                return RIGHT;
            case 2:
                return DOWN;
            case 3:
            default:
                return LEFT;
        }
    }

    public Position anotherSide(){
        switch (this){
            case TOP:
                return DOWN;
            case RIGHT:
                return LEFT;
            case DOWN:
                return TOP;
            case LEFT:
            default:
                return RIGHT;
        }
    }

    public int getIndex() {
        return index;
    }
}

方向,枚举类,用于判断单元格的那一面墙壁需要打通.

迷宫生成类

这里我使用find-union的两种实现方式实现,

一种是用数组的方式MazeArrayBuilder

一种使用map的方式实现MazeMapBuilder
所以我把迷宫生成的一些共同方法和属性抽取出现,编写了一个抽象类AbstractMazeBuilder.然后再在MazeArrayBuilderMazeMapBuilder做具体的实现.

现在我们来看看AbstractMazeBuilder这个类

public abstract class AbstractMazeBuilder {
    /**
     * 迷宫行列最大值
     */
    public static final int MAX_ROW_LINE = 200;
    /**
     * 行
     */
    protected int row;
    /**
     * 列
     */
    protected int line;
    /**
     * 迷宫格子集合,每个格子有四面墙
     */
    protected Box[][] boxes;
    /**
     * 求解迷宫中间变量
     */
    protected int[][] solverPath;
    /**
     * 迷宫时候已经算出最佳路径
     */
    protected boolean isSolver;
    /**
     * 使用贪婪算法,算出最佳路径集合
     */
    protected List bestPath;
    protected Random random;

    public AbstractMazeBuilder(int row, int line){
        if (row < 3 || row > MAX_ROW_LINE || line < 3 || line > MAX_ROW_LINE){
            throw new IllegalArgumentException("row/line 必须大于3,小于" + MAX_ROW_LINE);
        }
        this.row = row;
        this.line = line;

        isSolver = false;
        boxes = new Box[row][line];
        solverPath = new int[row][line];
        bestPath = new ArrayList();
        random = new Random();

        for (int i = 0; i < row; i ++){
            for (int j = 0; j < line; j ++){
                boxes[i][j] = new Box();
                solverPath[i][j] = Integer.MAX_VALUE;
            }
        }
    }

    /**
     * 查询与point点联通的最大格子的值
     * @param point point
     * @return 查询与point点联通的最大格子的值
     */
    protected abstract int find(MazePoint point);

    /**
     * 联通point1和point2点
     * @param point1 point1
     * @param point2 point2
     */
    protected abstract void union(MazePoint point1, MazePoint point2);

    /**
     * 判断时候已经生成迷宫路径
     * @return 判断时候已经生成迷宫路径
     */
    protected abstract boolean hasPath();

    /**
     * 生成迷宫
     */
    public void makeMaze(){

        while (hasPath()){
            // 生成 当前点, 当前点联通的方向, 当前点联通的方向对应的点
            ThreeTuple tuple = findNextPoint();
            if (tuple == null){
                continue;
            }
            union(tuple.one, tuple.three);
            breakWall(tuple.one, tuple.two);
            breakWall(tuple.three, tuple.two.anotherSide());
        }
        breakWall(new MazePoint(0,0), Position.LEFT);
        breakWall(new MazePoint(row - 1, line - 1), Position.RIGHT);
    }

    /**
     * 生成 当前点, 当前点联通的方向, 当前点联通的方向对应的点
     * @return
     * ThreeTuple.one 当前点
     * ThreeTuple.two 当前点联通的方向
     * ThreeTuple.three 当前点联通的方向对应的点
     */
    private ThreeTuple findNextPoint() {
        MazePoint currentPoint = new MazePoint(random.nextInt(row), random.nextInt(line));
        Position position = Position.indexOf(random.nextInt(Position.values().length));
        MazePoint nextPoint = findNext(currentPoint, position);
        if (nextPoint == null || find(currentPoint) == find(nextPoint)){
            return null;
        }
        return new ThreeTuple(currentPoint, position, nextPoint);
    }

    /**
     * 打通墙
     * @param point   当前点
     * @param position 当前点的方向
     */
    private void breakWall(MazePoint point, Position position) {
        boxes[point.x][point.y].set(position, Wall.ACCESS);
    }

    /**
     * 通过当前点以及对应当前点的方向找到下一个点
     * @param currentPoint 当前点
     * @param position 方向
     * @return 下个点,若该点在迷宫内,这返回,否则返回null
     */
    private MazePoint findNext(MazePoint currentPoint, Position position) {
        MazePoint nextPoint;
        switch (position){
            case TOP:
                nextPoint = new MazePoint(currentPoint.x - 1, currentPoint.y);
                break;
            case RIGHT:
                nextPoint = new MazePoint(currentPoint.x, currentPoint.y + 1);
                break;
            case DOWN:
                nextPoint = new MazePoint(currentPoint.x + 1, currentPoint.y);
                break;
            case LEFT:
            default:
                nextPoint = new MazePoint(currentPoint.x, currentPoint.y - 1);
                break;
        }
        if (nextPoint.x < 0 || nextPoint.x >= row || nextPoint.y < 0 || nextPoint.y >= line){
            return null;
        }
        return nextPoint;
    }

    public Box getBoxes(int x, int y) {
        return boxes[x][y];
    }

    public int getRow() {
        return row;
    }

    public int getLine() {
        return line;
    }

    /**
     * 求解迷宫路径
     * @return 迷宫路径
     */
    public List solvePath(){
        // 1 迷宫时候已经求解完成,是的话,则直接返回,不必再次计算
        if (isSolver){
            return bestPath;
        }
        // 2 否则计算迷宫最佳路径
        bestPath = new ArrayList();
        solverPath(new MazePoint(0, 0), 0);
        addPath(new MazePoint(row - 1, line - 1));
        Collections.reverse(bestPath);
        isSolver = true;
        return bestPath;
    }

    /**
     * 从终点逆推,添加最佳路径
     * @param point 当前点
     */
    private void addPath(MazePoint point) {
        bestPath.add(point);
        // 遍历当前点的每个方向,如果该方向能联通,这步数跟当前点的步数相差1步,这添加改点,递归计算下去
        for (Position position :Position.values()){
            MazePoint next = findNext(point, position);
            if (next == null || getBoxes(point.x, point.y).get(position) == Wall.BLOCK){
                continue;
            }
            if (solverPath[point.x][point.y] - 1 == solverPath[next.x][next.y]){
                addPath(next);
                return;
            }
        }
    }

    /**
     * 递归求解迷宫最佳路径
     * @param point 当前点
     * @param count 从开始走到当前点所需要的步数
     */
    private void solverPath(MazePoint point, int count) {
        // 判断当前点的步数时候小于现在走到这个点的步数,
        // 如果当前点的步数比较小,则直接返回
        if (solverPath[point.x][point.y] <= count){
            return;
        }
        // 否则表示当前点,有更短的路径
        solverPath[point.x][point.y] = count;
        // 再遍历当前点的每个方向
        for (Position position : Position.values()){
            MazePoint next = findNext(point, position);
            // 如果下一个点不在迷宫内,或当前点对应的方向是一面墙壁,则跳过继续编写下一个方向
            if (next == null || getBoxes(point.x, point.y).get(position) == Wall.BLOCK){
                continue;
            }
            // 否则,步数加1, 递归计算
            solverPath(next, count + 1);
        }
    }

    public static class MazePoint{
        public final int x;
        public final int y;

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

        @Override
        public String toString() {
            return "MazePoint{" +
                    "x=" + x +
                    ", y=" + y +
                    "}";
        }
    }
}

代码上有注释,理解起来还是比较容易.MazeArrayBuilderMazeMapBuilder的实现就参考github了.

AbstractMazeBuilder 中还包括了迷宫的求解.

迷宫的求解

迷宫的求解,一般我会使用以下两种方法

右手规则,从起点出发,遇到墙壁,则向右手边转,按照这个规则.一般是可以找到出口的.不过如果迷宫有闭环,则无法求解,而且解出来的路径也不是最短路径.

迷宫最短路径算法.

从起点出发,计数count=0

遍历该点的任意方向,如果是墙壁,则忽略,不然count++,进入下一个联通的格子

判断当前格子的的count(默认值一般是比较大的数)是否比传入的参数大,是说明该格子是一条捷径,将当前各自的count=入参,继续第2步;否则,说明该点已经被探索过且不是一条捷径,忽略

如果反复,暴力遍历所有单元格,即可以求出最短路径

遍历完之后,从出口开始找,此时出口的数字,表示从入口走到出口需要的最小步数.依次减1,找到下一个格子,直到找打入口.则最短路径就生成了.

附加运行结果

打赏

如果觉得我的文章写的还过得去的话,有钱就捧个钱场,没钱给我捧个人场(帮我点赞或推荐一下)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69918.html

相关文章

  • 用队列求解迷宫短路及其应用(围住神经猫)

    摘要:对应迷宫数组为实现语言实现求解方块类型方块行号方块列号上一个方块在队列中位置顺序队进队顺时针迷宫路径如下运行结果应用围住神经猫游戏使用写的项目源码下载体验最后附上我喜欢的歌的英文翻译心做 问题 给定一个M×N的迷宫图,求一条从指定入口到出口的最短路径.假设迷宫图如图所示(M=8, N=8) showImg(https://segmentfault.com/img/bVRjIk?w=26...

    Achilles 评论0 收藏0
  • [Leetcode] Graph Valid Tree 判断一个图是否为树

    摘要:只有一个连通分量还没有环,就是树,无根树。无向图边的两端是对称的,无向图讲究连通这个概念,没有方向,没有拓扑,很像集合,所以非常适合用并查集来解决。并查集写法参考注意方法用找的老大哥,合并的是的老大哥。 Graph Valid Tree Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each...

    xbynet 评论0 收藏0
  • 王者编程大赛之五 — 短路

    摘要:由于是从顶点到的最短路径,则有。算法流程根据最短路径的最优子结构性质,提出了以最短路径长度递增,逐次生成最短路径的算法。相关文章王者编程大赛之一王者编程大赛之二蓄水池王者编程大赛之三背包王者编程大赛之四约瑟夫环 首发于 樊浩柏科学院 自如年底就会拥有 50W 间房子,大家知道每间房房子都是需要配置完才能出租给自如客的,整个房租的配置过程是很复杂的,每天都需要大量的物流师傅将家电、家具...

    yuanzhanghu 评论0 收藏0

发表评论

0条评论

xiangchaobin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<