生成一个复杂的迷宫


目录

  • 生成一个复杂的迷宫
  • 主要功能
  • 代码实现
  • 主要步骤
  • 打印结果
  • 性能测试
  • 总结
  • ~~后续:整理代码打成jar包~~
  • 最新后续:目前 jar 包已经上传到 Maven 中央仓库,可以直接引用了
  • 参考资料


主要功能

通过java代码实现两千阶以内迷宫的随机生成.

代码实现

package com.example.springboot01.util;

import org.junit.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.*;

/**
 * 2000阶以下迷宫随机生成
 * 原理:
 * 1.获取所有可拆卸的墙
 * 2.从可拆卸的墙里面选择一面墙,判断该墙所对应的两个单元格是连通,不连通的话拆墙;否则随机获取下一面墙.
 * 3.重复步骤2,直到所有单元格全部连通.(生成的迷宫都是标准的,每次拆除的墙的数量都是 n^2-1 面,原因是随机取墙的次数足够多,足以覆盖所有的墙.
 * 不会出现拆除的墙少于 n^2-1 的原因是如果拆除的墙少于 n^2-1,那么肯定会有单元格是孤立的.
 * 不会出现拆除的墙多余 n^2-1 的原因是,只要满足拆的墙对应的两个单元格之前是不连通的,又拆的墙的数量为 n^2-1时,迷宫就已经生成了,不用再循环.
 * PS:测试入口test()在最下面,修改第一个变量private int size = 100;的值就可以生成不同阶数的迷宫啦.
 */
public class MazeGenerate4 {
    // 迷宫的大小
    private int size = 100;
    // 迷宫的阶数
    private int sqrt = (int)Math.sqrt(size);
    // 用于存储每个单元格所属的集合
    private int[] array = new int[size];
    // 用于存储已经合并过的单元格
    private HashSet<Integer> allCell = new HashSet<Integer>();

    /**
     * 返回 i 所在集合的最大值
     *
     * @param i 单元格编号
     * @return
     */
    public int find(int i) {
        int result = i;
        while (array[result] != -1) {
            result = array[result];
        }
        return result;
    }

    /**
     * 将 i 和 j 所在集合进行合并
     *
     * @param i 单元格编号
     * @param j 单元格编号
     */
    public void union(int i, int j) {
        int result1 = find(i);
        int result2 = find(j);
        if (result1 == result2){
            return;
        }
        if(result1 > result2) {
            array[result2] = result1;
            allCell.add(result2);
        }
        else {
            array[result1] = result2;
            allCell.add(result1);
        }
    }

    /**
     * 获取所有的可拆的墙
     */
    public List<Wall> getAllWalls() {
        ArrayList<Wall> allWalls = new ArrayList<>();
        // 保存下来所有的墙
        // 一共size个元素
        for (int i = 0; i < (size - 1); i++) {
            // 右边的墙
            int k = i + 1;
            // 下面的墙
            int l = i + (int) Math.sqrt(size);

            // 排除掉最右边的墙
            if ((i + 1) % ((int) Math.sqrt(size)) == 0) {
                allWalls.add(new Wall(i, l));
                continue;
            }

            // 排除掉最下面的墙
            if ((size - Math.sqrt(size)) <= i) {
                allWalls.add(new Wall(i, k));
                continue;
            }
            allWalls.add(new Wall(i, k));
            allWalls.add(new Wall(i, l));

        }
        return allWalls;
    }

    /**
     *随机生成迷宫
     *
     * @param data  list
     * @return List<Wall>
     */
    public Set<String> generateMaze(List<Wall> data) {
        Random random = new Random();
        // 用于存储待拆除的墙
        HashSet<String> toDelWalls = new HashSet<String>();
        // 拆除首尾节点的墙
        toDelWalls.add("1,0");
        toDelWalls.add(sqrt + "," + (2 * sqrt));

        // 初始化各个单元格所属的集合
        for (int j = 0; j < size; j++) {
            array[j] = -1;
        }
        int count = 0;
        while(isContinue()) {
            count++;
            // 随机获取一面墙
            int wallCode = random.nextInt(data.size());
            Wall wall = data.get(wallCode);
            int firstCellCode = wall.getFirstCellCode();
            int secondCellCode = wall.getSecondCellCode();
            // 判断墙对应的两个单元格是否是连通的
            if(find(firstCellCode) == find(secondCellCode)) {
                // 如果连通,墙不需要拆除
                continue;
            }
            else {
                // 如果不连通,拆除墙
                // (数组删除已解决)TODO 需要优化,如果能删除,可以减少一半的循环量.因为是ArrayList类型数据,删除代价比较大
                //data.remove(wallCode);
                int maxIndex = data.size() - 1;
                data.set(wallCode, data.get(maxIndex));
                data.remove(maxIndex);
                // 将两个单元格连通
                union(firstCellCode, secondCellCode);
                // 添加到需要拆除的墙集合里面
                toDelWalls.add(wall.getCoordinate());
            }
        }
        Util.println("count: " + count);
        return toDelWalls;
    }

    /**
     * 判断是否需要继续拆墙
     * @return
     */
    public boolean isContinue() {
        // 第一个单元格和最后一个单元格是否连通
        if(find(0) != (size - 1)) {
            return true;
        }

        // 是否所有的单元格都连通
        if(allCell.size() < size - 1) {
            return true;
        }
        return false;
    }

    /**
     * 打印迷宫到控制台
     * @param toDelWalls
     */
    public void printMaze(Set<String> toDelWalls) {
        for(int i=0; i<(sqrt+1); i++) {      // 行
            for(int j=0; j<(2*sqrt+1); j++) {    // 列
                String toPrintStr = "";
                String temp = i + "," + j;
                if(i % 2 == 0) {    // 奇数行
                    if(j % 2 == 0) {    //奇数列
                        if(i == 0) {    // 第一行
                            toPrintStr = " ";
                        }
                        else {
                            toPrintStr = "|";
                        }
                    }
                    else {              // 偶数列
                        toPrintStr = "_";
                    }
                }
                else {                  // 偶数行
                    if(j % 2 == 0) {    //奇数列
                        toPrintStr = "|";
                    }
                    else {              // 偶数列
                        toPrintStr = "_";
                    }
                }
                if(toDelWalls.contains(temp)) {
                    toPrintStr = " ";
                    toDelWalls.remove(temp);
                }
                Util.print(toPrintStr);
            }
            Util.println();
        }
    }

    /**
     * 将迷宫保存到TXT文本中
     * @param toDelWalls
     * @param fileName
     */
    public void saveToText(Set<String> toDelWalls, String fileName) {
        File file = new File(fileName);
        try {
            Writer writer = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8");
            StringBuilder builder = new StringBuilder();

            for(int i=0; i<(sqrt+1); i++) {      // 行
                for(int j=0; j<(2*sqrt+1); j++) {    // 列
                    String toPrintStr = "";
                    String temp = i + "," + j;
                    if(i % 2 == 0) {    // 奇数行
                        if(j % 2 == 0) {    //奇数列
                            if(i == 0) {    // 第一行
                                toPrintStr = " ";
                            }
                            else {
                                toPrintStr = "|";
                            }
                        }
                        else {              // 偶数列
                            toPrintStr = "_";
                        }
                    }
                    else {                  // 偶数行
                        if(j % 2 == 0) {    //奇数列
                            toPrintStr = "|";
                        }
                        else {              // 偶数列
                            toPrintStr = "_";
                        }
                    }
                    if(toDelWalls.contains(temp)) {
                        toPrintStr = " ";
                        toDelWalls.remove(temp);
                    }
                    builder.append(toPrintStr);
                    //Util.print(toPrintStr);
                }
                builder.append("\r\n");
                //Util.println();
            }
            writer.write(builder.toString());
            writer.close();
        }
        catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 保存关于墙所有的信息
     */
    public class Wall {
        // 墙对应的第一个单元格
        private int firstCellCode = 0;
        // 墙对应的第二个单元格
        private int secondCellCode = 0;
        // 横坐标
        private int x = 0;
        // 纵坐标
        private int y = 0;
        // x,y组成的坐标系
        private String coordinate = "";

        public Wall(int firstCellCode, int secondCellCode) {
            this.firstCellCode = firstCellCode;
            this.secondCellCode = secondCellCode;
            if (sqrt == (secondCellCode - firstCellCode)) {
                this.y = (secondCellCode % sqrt) * 2 + 1;
            } else if (1 == (secondCellCode - firstCellCode)) {
                this.y = (secondCellCode % sqrt) * 2;
            }
            this.x = firstCellCode / sqrt + 1;

            this.coordinate = x + "," + y;
        }

        public int getFirstCellCode() {
            return firstCellCode;
        }

        public int getSecondCellCode() {
            return secondCellCode;
        }

        public String getCoordinate() {
            return coordinate;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }
    }


    @Test
    public void test() {
        long startTime = System.currentTimeMillis();
        // 1.获取所有可拆卸的墙的信息
        List<Wall> allWalls = getAllWalls();

        long getWallTime = System.currentTimeMillis();
        Util.println("getWallTime: " + (getWallTime - startTime) + "ms");

        // 2.随机生成迷宫,并记录需要被拆除的墙
        Set<String> toDelWalls = generateMaze(allWalls);

        long generateMazeTime = System.currentTimeMillis();
        Util.println("generateMazeTime: " + (generateMazeTime - getWallTime) + "ms");

        // 3.打印迷宫到控制台
        printMaze(toDelWalls);
        // 输出迷宫到TXT文本
        //saveToText(toDelWalls, "d:" + File.separator + "10.txt");

    }

}

主要步骤

* 1.获取所有可拆卸的墙
 * 2.从可拆卸的墙里面选择一面墙,判断该墙所对应的两个单元格是连通,不连通的话拆墙;否则随机获取下一面墙.
 * 3.重复步骤2,直到所有单元格全部连通.(生成的迷宫都是标准的,每次拆除的墙的数量都是 n^2-1 面,原因是随机取墙的次数足够多,足以覆盖所有的墙.
 * 不会出现拆除的墙少于 n^2-1 的原因是如果拆除的墙少于 n^2-1,那么肯定会有单元格是孤立的.
 * 不会出现拆除的墙多余 n^2-1 的原因是,只要满足拆的墙对应的两个单元格之前是不连通的,又拆的墙的数量为 n^2-1时,迷宫就已经生成了,不用再循环.
 * PS:测试入口test()在最下面,修改第一个变量private int size = 100;的值就可以生成不同阶数的迷宫啦.

迷宫生成流程图

java 迷宫生成 java迷宫小游戏随机地图_java

打印结果

效果图(80*80)

java 迷宫生成 java迷宫小游戏随机地图_java_02

性能测试

阶数

10阶

100阶

1000阶

耗时

1ms

21ms

3762ms

总结

  1. 上一篇的迷宫其实不算迷宫了,主要是遍历上.
  2. 在看了weixin_34367845这位大佬的迷宫实现代码后,终于理解了迷宫随机生成的算法,然后修改了一下程序,现在可以输出2000 * 2000的迷宫了,耗时25s左右,再大就不行了,可能和ArrayList的扩容有关.1000 * 1000的迷宫生成只要3s多,还是比较快的.
  3. 这里面的find/union算法直接拷贝的weixin_34367845大佬的,自己写的不行,然后优化了一下isContinue()方法,减少了不必要的循环.
  4. 因为是随机拆墙,所以已经拆除掉的墙可以排除掉,这就涉及到数组的删除,由于直接删除代价比较大,所以可以通过将数组最后一个元素和被删除元素对换位置,然后直接删除最后一个元素来实现.1000阶的迷宫生成可以节省1s左右,2000阶可以节省8秒左右,提升效果还是比较明显的.
  5. 这边贴一下weixin_34367845大佬的迷宫生成步骤吧,防止又要重新看代码,大佬的代码写的还是很漂亮的,代码链接在参考资料里面.
  6. 2000阶迷宫的txt文本下载链接也放在下面吧,可以用notepadd++打开.就当留个纪念,前后搞了差不多一个星期了.
大佬迷宫生成原理:
1.生成 n 阶完整迷宫,给各个单元格从左到右,从上到下依次编号
2.以左上角单元格为坐标原点建立坐标系
3.在所有单元格里面随机选择一个单元格 A
4.为这个单元格随机选择一个拆墙方向
5.获取到墙后面的单元格 B
6.判断单元格 B 是否合理,即不能在迷宫外面,也不能与 A 同属于一个集合(是否属于同一个集合通过find()方法判断)
7.如果 B 合理,则拆除 A 单元格与 B 单元格之间的墙;如果 B 不合理,则重复 3-7 步骤,直到 每个单元格都相互连通.


我之前的随机算法是一次性选择 n^2-1 面墙,然后全部拆除后判断是否所有单元格连通
人家的随机算法是一次选择 1 面墙进行拆除,每次拆完判断是否所有单元格连通,不是的话继续拆除下一面墙
所以我的算法可能永远得不到答案,但人家的肯定会有答案,因为


import com.example.demo.MazeGenerate;
import java.io.File;

public class Test13 {

    public static void main(String[] args) {
        // 初始化迷宫,指定阶数
        MazeGenerate mazeGenerate = new MazeGenerate(10);
        // 打印迷宫到控制台
        mazeGenerate.printToConsole();
        // 输出迷宫到Txt文本
        mazeGenerate.saveToText("d:" + File.separator + "11.txt");
    }

}

最新后续:目前 jar 包已经上传到 Maven 中央仓库,可以直接引用了