用並查集(find-union)實現迷宮算法以及最短路徑求解

本人郵箱: <kco1989@qq.com>
歡迎轉載,轉載請註明網址 http://blog.csdn.net/tianshi_kco
github: https://github.com/kco1989/kco
代碼已經所有託管github有須要的同窗自行下載java

引言

迷宮對於你們都不會陌生.那麼迷宮是怎麼生成,已經迷宮要如何找到正確的路徑呢?用java代碼又怎麼實現?帶着這些問題.咱們繼續往下看.git

並查集(find-union)

朋友圈

有一種算法就作並查集(find-union).什麼意思呢?好比如今有零散的甲乙丙丁戊五我的.他們之間剛開始互相不認識.用代碼解釋就是find(person1, person2) == false,以後在某一次聚合中,認識了,認識了,認識了等等,那麼就能夠用代碼解釋以下.github

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

那麼這個時候就能夠經過認識到了在經過認識到.
這是甲乙丙丁戊經過朋友或者朋友的朋友最終都互相認識.換另外一種說法就是若是要認識,那麼必須先經過認識,再經過去認識就好了.算法

迷宮

對於迷宮生成,其實更上面朋友圈有點相似.生成步驟以下數組

  1. 首先,先建立一個n*m的二維密室.每一個單元格四方都是牆.微信

  2. 隨機選擇密室中的一個單元格.以後在隨機選擇一面要打通的牆壁.網絡

  3. 判斷要打通的牆壁是否爲邊界.是則返回步驟3,不是則繼續dom

  4. 判斷步驟的單元個和要打通的牆壁的對面是否聯通(用find算法)maven

  5. 若是兩個單元格不聯通,則把步驟2選中的牆壁打通(用union算法).不然返回步驟2.ide

  6. 判斷迷宮起點和終點是否已經聯通,是則迷宮生成結束,不然返回步驟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的兩種實現方式實現,

  1. 一種是用數組的方式MazeArrayBuilder

  2. 一種使用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<MazePoint> 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<MazePoint>();
        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<MazePoint, Position, MazePoint> 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<MazePoint, Position, MazePoint> 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<MazePoint, Position, MazePoint>(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<MazePoint> solvePath(){
        // 1 迷宮時候已經求解完成,是的話,則直接返回,沒必要再次計算
        if (isSolver){
            return bestPath;
        }
        // 2 不然計算迷宮最佳路徑
        bestPath = new ArrayList<MazePoint>();
        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 中還包括了迷宮的求解.

迷宮的求解

迷宮的求解,通常我會使用如下兩種方法

  1. 右手規則,從起點出發,遇到牆壁,則向右手邊轉,按照這個規則.通常是能夠找到出口的.不過若是迷宮有閉環,則沒法求解,並且解出來的路徑也不是最短路徑.

  2. 迷宮最短路徑算法.

    1. 從起點出發,計數count=0

    2. 遍歷該點的任意方向,若是是牆壁,則忽略,否則count++,進入下一個聯通的格子

    3. 判斷當前格子的的count(默認值通常是比較大的數)是否比傳入的參數大,是說明該格子是一條捷徑,將當前各自的count=入參,繼續第2步;不然,說明該點已經被探索過且不是一條捷徑,忽略

    4. 若是反覆,暴力遍歷全部單元格,便可以求出最短路徑

    5. 遍歷完以後,從出口開始找,此時出口的數字,表示從入口走到出口須要的最小步數.依次減1,找到下一個格子,直到找打入口.則最短路徑就生成了.

附加運行結果

生成的迷宮圖片

打賞

若是以爲個人文章寫的還過得去的話,有錢就捧個錢場,沒錢給我捧我的場(幫我點贊或推薦一下)
微信打賞
支付寶打賞

相關文章
相關標籤/搜索