生成一个简单的迷宫
主要功能
通过java代码实现二阶,三阶,四阶迷宫的生成和遍历.
代码实现
package com.example.springboot01.util;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 对于 n 阶迷宫,一共有 n^2-1 + (n-1)^2 面可拆除墙,至少需要拆除其中的 n^2-1 面墙才能形成所有单元格都相连的迷宫
* 5阶以下,全遍历
* 主要原理:
* 1.生成 n 阶迷宫的完整图,由空格符号( ), 或符号(|), 下划线符号(_)三种符号组成
* 例: 完整4阶迷宫,横坐标是testPrint2()方法中的 j, 纵坐标是 i
* 012345678
* _ _ _ _ 0
* |_|_|_|_|1
* |_|_|_|_|2
* |_|_|_|_|3
* |_|_|_|_|4
* 2.确定可拆卸的墙,墙由两个相邻的单元格表示,比如墙(0,1)表示单元格 0 和单元格 1 之间的墙,除了最外围一圈的墙,其他都可拆卸,一共 n^2-1 + (n-1)^2 面可拆除墙
* 3.确定至少需要拆多少面墙,才能形成一个所有单元格都连通的迷宫.至少拆 n^2-1 面墙
* 4.将所有的拆墙方案排列出来, m 面墙里面拆除 n 面墙,一共有 m!/(n!*(m-n)!) 种方案,不是 m!/n! 种,因为它是无序的.
* 5.循环遍历所有方案,并剔除其中不可行的方案
* 6.第一步剔除,如果拆墙方案中不涉及所有的单元格,剔除掉.例3阶迷宫的拆墙方案需要涉及到 0-8 所有的单元格.原因是如果有一个单元格不涉及,表示
* 这个单元格和其他单元格不是互通的.
* 7.第二步剔除,使用union()方法,将拆墙方案里面涉及的单元格进行集合合并,再使用find()方法,判断是否所有单元格同属于一个集合,也就是 0 集合.
* 是的话,打印迷宫,否的话,剔除掉.
* 8.打印迷宫前,需要将墙的表示方法进行转换,之前表示墙是用两个两个相邻单元格表示的,现在需要按单元格的坐标系来表示,方便打印迷宫时排除掉
* 需要拆除的墙
* 9.测试结果: 二阶一共有 4 种迷宫, 三阶一共有 180 种迷宫, 四阶一共有 70364 种迷宫, 五阶这种方案就不行了...
* PS: 测试类在最下面,提示MyLinkedList类不存在可以替换为LinkedList,一样的,修改第一个变量 private int size = 16; 可以控制迷宫的阶数.
*/
public class MazeGenerate {
// size = n阶 * n阶
private int size = 16;
// sqrt = n阶
private int sqrt = (int)Math.sqrt(size);
// 用于存放每个单元格对应的集合
private int[] array = new int[size];
// 用于存放需要拆除的墙
private MyQueue<String> queue = new MyQueue<String>();
public int size() {
return size;
}
/**
* 将 j 所在集合替换为 i 所在集合
* 将 j 纳入 i 所在的集合
* 如果 j 之前属于 A 集合,那么 A 集合里面的所有元素都要纳入 i 所在的集合
* @param i
* @param j
*/
public void union(int i, int j) {
// 不能简单的将 i 赋值给 array[j], 会覆盖掉之前的赋值
// 比如[0,1, 1,2, 2,5, 3,4, 3,6, 4,5, 6,7, 7,8]这个拆墙方案是可行的,但是4,5会覆盖掉2,5,所以不能直接赋值
if(array[j] == j) {
array[j] = find(i);
}
else {
// 近墨者黑,只要j为0集合,那么就将i也设置为0集合,并且i原先所属的集合也设置为0集合
if(array[j] == 0) {
setZero(i);
}
else if(array[i] == 0) { // 对于 i 也同样如此,将 j 的集合设置为 0
setZero(j);
}
}
}
/**
* 递归将 i 指向的元素所属集合设置为 0 所在的单元格
* @param i
*/
public void setZero(int i) {
if(array[i] != i) {
setZero(array[i]);
}
array[i] = 0;
}
/**
* 返回 i 所在集合
*
* @param i
* @return
*/
public int find(int i) {
if(array[i] == i) {
return i;
}
return find(array[i]);
}
/**
* 排序
* 用于循环砌墙时,判断哪些墙不用砌
* @param list
*/
public void sort(ArrayList<String> list) {
for(int i=0; i<list.size(); i++) {
for(int j=i+1; j<list.size(); j++) {
String[] strArr = list.get(i).split(",");
int ii = Integer.parseInt(strArr[0]);
int jj = Integer.parseInt(strArr[1]);
String[] strArr2 = list.get(j).split(",");
int ii2 = Integer.parseInt(strArr2[0]);
int jj2 = Integer.parseInt(strArr2[1]);
if(ii > ii2 || (ii == ii2 && jj > jj2)) {
String temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
}
}
}
/**
* 方法一:无序
* data.size()个数据里面随机选择num个数据,有多少种方式
*
* @param data list 所有可拆除的墙
* @param <E> e
* @param num int 需要拆除的墙的数量
* @return List<List<E>>
*/
public <E> MyLinkedList<MyLinkedList<E>> arrangeSelect1(List<E> data, int num) {
long startTime = System.currentTimeMillis();
int size = data.size();
// 一共 2^size-1 个无序全排列组合
long count = (long) (Math.pow(2, size) - 1);
MyLinkedList<MyLinkedList<E>> arrangeAllSet = new MyLinkedList<>();
// 这边的两个for循环可以将 size 个数里面取 num 个数的所有组合全部列出来.
// 原理和 size 个数里面取 num 个数是一样的,只不过它替换为了二进制而已
// 变成了长度为 size 的二进制数里面,分布 num 个 1,一共有多少种分布法
// count对应于长度为 size 的最大二进制数 2^size - 1
// j 其实就是将二进制数里面的 1 的下标
// 举个例子吧,5个数{a, b, c, d, e}里面取2个数,有多少种取法,比如 ab, ac, ad..
// 解:
// 第一步,等价转换为二进制解法,5个数 00000 里面选择2个 0 替换为 1,有多少种替换法
// 第二步,可以写一个循环,从 00000 一直遍历到 11111,这样所有的替换法肯定都在里面.过程大概是这样的, 00001, 00010, 00011, 00100, 00101, ...
for (long i = 1; i <= count; i++) {
// 第三步,对于遍历的结果,我们需要的是 00011, 00101 这样的有两个 1 的值,那我们如何判断它里面有两个 1 ,而不是三个 1 呢
// 第四步,这时就需要一个函数 f6(),来对当前二进制数进行检测,看它里面有几个 1, f6()函数原理我就不多解释了.
if(f6(i) == num) {
// 第五步,找到一个二进制数是我们需要的了,比如 00011, 那我们怎么再对应到原始的{a,b,c,d,e}呢,两者的唯一关联就是长度都是5,并且有序,
// 所以我们可以为 00011 编一个下标,我们从右往左依次为 0,1,2,3,..,这样可以得到下标为 0 和 1 的组合是满足我们的要求的,对应的原始组合就是 ab 了.
MyLinkedList<E> arrangeSet = new MyLinkedList<>();
// 第六步, 00011 的下标我们可以看出来,计算机却不能,所以我们需要循环遍历 00011 中的每个元素,通过左移和右移来让计算机知道哪个元素是 1,并记录下来.
// 左移右移为什么能判断出来是1,看前面的博客吧.
for (int j = 0; j < size; j++) {
if ((i << (63 - j)) >> 63 == -1) {
arrangeSet.add(data.get(j));
}
}
// 第七步,当所有的循环结束后,我们就得到了我们想要的排列组合.
arrangeAllSet.add(arrangeSet);
}
}
Util.println(Thread.currentThread() + " costTime: " + (System.currentTimeMillis() - startTime));
return arrangeAllSet;
}
/**
* 返回 x 对应的二进制里面有几个1,时间复杂度 O(1)
* @param x
* @return
*/
public long f6(long x){
x = (x & 0x5555555555555555L) + ((x & 0xaaaaaaaaaaaaaaaaL) >> 1);
x = (x & 0x3333333333333333L) + ((x & 0xccccccccccccccccL) >> 2);
x = (x & 0x0f0f0f0f0f0f0f0fL) + ((x & 0xf0f0f0f0f0f0f0f0L) >> 4);
x = (x & 0x00ff00ff00ff00ffL) + ((x & 0xff00ff00ff00ff00L) >> 8);
x = (x & 0x0000ffff0000ffffL) + ((x & 0xffff0000ffff0000L) >> 16);
x = (x & 0x00000000ffffffffL) + ((x & 0xffffffff00000000L) >> 32);
return x;
}
/**
* 这边的打印会打印出完整迷宫图案,
* 如果queue中有需要拆除的墙,打印会跳过
*/
public void testPrint2() {
for(int i=0; i<(sqrt+1); i++) { // 行
for(int j=0; j<(2*sqrt+1); j++) { // 列
if(i % 2 == 0) { // 奇数行
if(j % 2 == 0) { //奇数列
if(i == 0) { // 第一行
printIncase(i, j, " ");
}
else {
printIncase(i, j, "|");
}
}
else { // 偶数列
printIncase(i, j, "_");
}
}
else { // 偶数行
if(j % 2 == 0) { //奇数列
printIncase(i, j, "|");
}
else { // 偶数列
printIncase(i, j, "_");
}
}
}
Util.println();
}
}
/**
* 对于需要拆除的墙,打印空格
* @param i
* @param j
* @param toPrintStr
*/
public void printIncase(int i, int j, String toPrintStr) {
if(queue.size() > 0) {
String cursor = queue.peek();
String[] strArr = cursor.split(",");
int ii = Integer.parseInt(strArr[0]);
int jj = Integer.parseInt(strArr[1]);
if(i == ii && j == jj) {
toPrintStr = " ";
queue.poll();
}
}
Util.print(toPrintStr);
}
/**
* 获取所有的可拆的墙
*/
public List<String> getAllWalls() {
ArrayList<String> allWalls = new ArrayList<String>();
// 保存下来所有的墙
// 一共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("{" + i + ", " + l + "}");
allWalls.add(i + "," + l);
continue;
}
// 排除掉最下面的墙
if ((size - Math.sqrt(size)) <= i) {
allWalls.add(i + "," + k);
continue;
}
allWalls.add(i + "," + k);
allWalls.add(i + "," + l);
}
return allWalls;
}
/**
* 依据拆墙方案,将所有单元格进行集合归类
* 如果最后所有单元格同属于一个集合,表示拆墙方案是可行的
* 否则表示有两个单元格之间是不连通的,拆墙方案不合格
* @param arrangeList1
*/
public void removeUnqualified(MyLinkedList<MyLinkedList<String>> arrangeList1) {
MyLinkedList<MyLinkedList<String>>.Itr<MyLinkedList<String>> itr = arrangeList1.iterator();
// 对每种拆卸情况进行union/find算法分析,判断能否可行
while(itr.hasNext()) {
MyLinkedList<String> itemList = itr.next();
// 重新初始化各个元素所属的集合
for (int j = 0; j < size(); j++) {
array[j] = j;
}
// 将 size-1 个组合进行合并,就是拆墙
MyLinkedList<String>.Itr<String> itr1 = itemList.iterator();
while(itr1.hasNext()) {
String[] strArray = itr1.next().split(",");
union(Integer.parseInt(strArray[0]), Integer.parseInt(strArray[1]));
}
// 墙拆完了,判断是否所有的元素都在同一个集合里面,不是的话,移除掉
for(int j=1; j<(size()-1); j++) {
if(find(j) != find(j+1)) {
itr.remove();
break;
}
}
}
}
public void removeUnqualified2(MyLinkedList<MyLinkedList<String>> arrangeList1) {
MyLinkedList<MyLinkedList<String>>.Itr<MyLinkedList<String>> itr = arrangeList1.iterator();
// 过滤掉不包含所有元素的拆墙方案
while(itr.hasNext()) {
MyLinkedList<String> itemList = itr.next();
Set<String> set = new HashSet<String>();
// 将 size-1 个组合进行合并,就是拆墙
MyLinkedList<String>.Itr<String> itr1 = itemList.iterator();
while(itr1.hasNext()) {
String[] strArray = itr1.next().split(",");
set.add(strArray[0]);
set.add(strArray[1]);
}
// 如果不包含所有元素,就移除掉
if(set.size() < size()) {
itr.remove();
}
}
}
@Test
public void test() {
// 获取到所有的可拆卸的墙
List<String> allWalls = getAllWalls();
// 进行无序的全排列组合,找出所有的可能拆卸情况
MyLinkedList<MyLinkedList<String>> arrangeList1 = arrangeSelect1(allWalls, (size()-1));;
// 先排除掉不包含所有元素的拆墙方案
removeUnqualified2(arrangeList1);
// 移除掉不符合的拆卸情况
removeUnqualified(arrangeList1);
//Util.println(arrangeList1.size());
MyLinkedList<MyLinkedList<String>>.Itr<MyLinkedList<String>> itr = arrangeList1.iterator();
while(itr.hasNext()) {
ArrayList<String> strArray = new ArrayList<String>();
// 拆除首尾节点的墙
strArray.add("1,0");
strArray.add(sqrt + "," + (2*sqrt));
MyLinkedList<String> itemList = itr.next();
MyLinkedList<String>.Itr<String> itr1 = itemList.iterator();
while(itr1.hasNext()) {
String[] strArr = itr1.next().split(",");
int ii = Integer.parseInt(strArr[0]);
int jj = Integer.parseInt(strArr[1]);
if(sqrt == (jj-ii)) {
jj = (jj % sqrt) * 2 + 1;
}
else if(1 == (jj-ii)) {
jj = (jj % sqrt) * 2;
}
ii = ii/sqrt + 1;
strArray.add(ii + "," + jj);
}
// 对queue进行坐标转换并排序
sort(strArray);
queue = new MyQueue<String>();
for(String item: strArray) {
queue.add(item);
}
// 画图
testPrint2();
}
}
}
主要步骤
* 1.生成 n 阶迷宫的完整图,由空格符号( ), 或符号(|), 下划线符号(_)三种符号组成
* 例: 完整4阶迷宫,横坐标是testPrint2()方法中的 j, 纵坐标是 i
* 012345678
* _ _ _ _ 0
* |_|_|_|_|1
* |_|_|_|_|2
* |_|_|_|_|3
* |_|_|_|_|4
* 2.确定可拆卸的墙,墙由两个相邻的单元格表示,比如墙(0,1)表示单元格 0 和单元格 1 之间的墙,除了最外围一圈的墙,其他都可拆卸,一共 n^2-1 + (n-1)^2 面可拆除墙
* 3.确定至少需要拆多少面墙,才能形成一个所有单元格都连通的迷宫.至少拆 n^2-1 面墙
* 4.将所有的拆墙方案排列出来, m 面墙里面拆除 n 面墙,一共有 m!/(n!*(m-n)!) 种方案,不是 m!/n! 种,因为它是无序的.
* 5.循环遍历所有方案,并剔除其中不可行的方案
* 6.第一步剔除,如果拆墙方案中不涉及所有的单元格,剔除掉.例3阶迷宫的拆墙方案需要涉及到 0-8 所有的单元格.原因是如果有一个单元格不涉及,表示
* 这个单元格和其他单元格不是互通的.
* 7.第二步剔除,使用union()方法,将拆墙方案里面涉及的单元格进行集合合并,再使用find()方法,判断是否所有单元格同属于一个集合,也就是 0 集合.
* 是的话,打印迷宫,否的话,剔除掉.
* 8.打印迷宫前,需要将墙的表示方法进行转换,之前表示墙是用两个两个相邻单元格表示的,现在需要按单元格的坐标系来表示,方便打印迷宫时排除掉
* 需要拆除的墙
* PS: 测试类在最下面,提示MyLinkedList类不存在可以替换为LinkedList,一样的,修改第一个变量 private int size = 16; 可以控制迷宫的阶数.
打印结果
效果图
作者图
感觉不是同一个东西啊
网上找到一个别人实现迷宫的代码
用并查集(find-union)实现迷宫算法以及最短路径求解
总结
- 数据结构与算法分析第8章不相交集类中,作者介绍了unon/find数据结构,说可以用来生成迷宫图,好奇之下,自己动手写了个程序,目前只能实现4阶迷宫的遍历,5阶迷宫的数据量太大(墙的所有拆除方案一共有一万多亿种)遍历不了.
- 二阶一共有 4 种迷宫, 三阶一共有 180 种迷宫, 四阶一共有 70364 种迷宫, 五阶这种方案就不行了…
- 里面的全排列组合结果遍历是网上百度的,可以参考一下.
- 国外有专门研究代码生成迷宫的网站http://www.astrolog.org/labyrnth/algrithm.htm 有兴趣的可以看一下,我没看懂.
- 作者没贴具体的迷宫生成代码,不知道他怎么做的.