生成一个复杂的迷宫
目录
- 生成一个复杂的迷宫
- 主要功能
- 代码实现
- 主要步骤
- 打印结果
- 性能测试
- 总结
- ~~后续:整理代码打成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;的值就可以生成不同阶数的迷宫啦.
迷宫生成流程图
打印结果
效果图(80*80)
性能测试
阶数 | 10阶 | 100阶 | 1000阶 |
耗时 | 1ms | 21ms | 3762ms |
总结
- 上一篇的迷宫其实不算迷宫了,主要是遍历上.
- 在看了weixin_34367845这位大佬的迷宫实现代码后,终于理解了迷宫随机生成的算法,然后修改了一下程序,现在可以输出2000 * 2000的迷宫了,耗时25s左右,再大就不行了,可能和ArrayList的扩容有关.1000 * 1000的迷宫生成只要3s多,还是比较快的.
- 这里面的find/union算法直接拷贝的weixin_34367845大佬的,自己写的不行,然后优化了一下isContinue()方法,减少了不必要的循环.
- 因为是随机拆墙,所以已经拆除掉的墙可以排除掉,这就涉及到数组的删除,由于直接删除代价比较大,所以可以通过将数组最后一个元素和被删除元素对换位置,然后直接删除最后一个元素来实现.1000阶的迷宫生成可以节省1s左右,2000阶可以节省8秒左右,提升效果还是比较明显的.
- 这边贴一下weixin_34367845大佬的迷宫生成步骤吧,防止又要重新看代码,大佬的代码写的还是很漂亮的,代码链接在参考资料里面.
- 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 中央仓库,可以直接引用了