[土味]自制迷宫

生成一个简单的迷宫

主要功能

通过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)实现迷宫算法以及最短路径求解

总结

  1. 数据结构与算法分析第8章不相交集类中,作者介绍了unon/find数据结构,说可以用来生成迷宫图,好奇之下,自己动手写了个程序,目前只能实现4阶迷宫的遍历,5阶迷宫的数据量太大(墙的所有拆除方案一共有一万多亿种)遍历不了.
  2. 二阶一共有 4 种迷宫, 三阶一共有 180 种迷宫, 四阶一共有 70364 种迷宫, 五阶这种方案就不行了…
  3. 里面的全排列组合结果遍历是网上百度的,可以参考一下.
  4. 国外有专门研究代码生成迷宫的网站http://www.astrolog.org/labyrnth/algrithm.htm 有兴趣的可以看一下,我没看懂.
  5. 作者没贴具体的迷宫生成代码,不知道他怎么做的.

参考资料

<<数据结构与算法分析>>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值