关于生成迷宫的锯齿形

来源:6-8 生成随机性更强的迷宫

tobeabee

2022-10-25

老师,我发现对于本节课的代码,不管是在让random queue更随机前还是更随机后,我们生成的迷宫都有大量锯齿形,请问这是什么原因造成的?我们又可以如何规避这个问题?
图片描述

写回答

1回答

liuyubobobo

2022-10-26

如何更“随机”本身是一个非常大的话题了。我可以提供一些思路:


首先,你需要分析一下,为什么会产生这种情况?(自己试试看?有必要的话,实际跟踪一下现在的程序,看一看到底为什么会产生这种“锯齿状”的形状?)


==========


其核心原因是,我们的算法本质还是广度优先遍历,只不过广度优先遍历的顺序随机了而已。广度优先遍历就会导致,在一个节点的时候,会优先把这个节点周围没有遍历的节点都“占住”,回忆一下,如果我们使用纯粹的广度优先遍历生成的迷宫,本质是不是就是一个“大号的锯齿”?

//img.mukewang.com/szimg/63583a0c092e384605170473.jpg


怎么解决?一个显然的思路是,广度优先遍历应该和深度优先遍历结合起来。也就是,我们在一个节点,去根据广度优先遍历“占住”他的四个方向的节点以后,应该有一定的概率基于某一个方向做深度优先遍历。


以下的代码是我尝试的基于这个思路修改的 run 函数。其中我把从一个节点向外扩展四个方向封装成了一个 expand 函数,这样方便调用。你可以看到,在 expand 函数中,有一定的概率(我设置的是 90%)基于扩展的一个方向的节点继续扩展(即深度优先遍历)。


注意,为了进一步引入随机性,expand 中对四个方向的遍历顺序每次也是随机的。具体参考以下代码:


    private void run(){
        setRoadData(-1, -1);
        
        RandomQueue<Position> queue = new RandomQueue<Position>();
        Position first = new Position(data.getEntranceX(), data.getEntranceY()+1);
        queue.add(first);
        data.visited[first.getX()][first.getY()] = true;
        data.openMist(first.getX(), first.getY());
        
        while(queue.size() != 0){
            Position curPos = queue.remove();
            expand(curPos, queue);
        }
        
        setRoadData(-1, -1);
    }
    
    void expand(Position cur, RandomQueue<Position> q){
        
        // 进一步随机,每次遍历四个方向的顺序是随机的,而非固定的
        ArrayList<Integer> dirs = new ArrayList<>();
        for(int i = 0; i < 4; i ++) dirs.add(i);
        Collections.shuffle(dirs);
        
        for(int i = 0 ; i < 4 ; i ++){
            int newX = cur.getX() + d[dirs.get(i)][0]*2;
            int newY = cur.getY() + d[dirs.get(i)][1]*2;
            if(data.inArea(newX, newY) && !data.visited[newX][newY]){
                Position newPosition = new Position(newX, newY);
                q.add(newPosition);
                data.visited[newX][newY] = true;
                data.openMist(newX, newY);
                setRoadData(cur.getX() + d[dirs.get(i)][0], cur.getY() + d[dirs.get(i)][1]);
                
                //90% 的概率基于这个方向再往深度走一层
                if(Math.random() < 0.9) expand(newPosition, q);
            }
        }
    }


加入这一机制之后,这一情况就好转很多。在我的环境下的一次运行结果如下:

//img.mukewang.com/szimg/63583bed096c385216141658.jpg


如果你对此还不满意,还可以思考更多随机的策略,比如:


1)锯齿本质就是长度为 1 就到思路的通路。我们是否可能在可能的情况下,让每条通路的长度至少为 2?(更进一步,至少为 3?至少为 4)


2)即使如此,锯齿还是无法避免的,因为当空间过于“拥挤”的时候,通路没地儿可去,只能走一步就退回来了。那么在这种情况下,我们是否能直接就把这个锯齿的通路扔掉?(整个迷宫的通路不在占据 board 上每一个间隔的格子了。)


3)我们现在深度拓展的概率是 90%,这个概率是个定值,我们是否可能让这个值也是随机的(或者变化的)?(注意,因为你不需要产生锯齿,所以这个概率值在初始应该比较高,以多走一层防止锯齿的出现,所以让这个概率值完全的随机也不是好主意。一个简单的方式是让这个概率值递减,这有点儿类似模拟退火的思想。)


等等等等。


这个程序可供拓展的地方非常多,印象里,这一章的最后,我也会介绍更多和迷宫生成相关的拓展内容。整体,当来到更加“真实世界”的程序中的时候,很多问题是 open 的。没有一定的对或者一定的不对。但是,当你尝试解决一个问题的时候,你需要做的是:


1)用更加数学(或者形式化的方式)定义问题本身。(比如你的描述,“锯齿形”的迷宫,就不是形式化的。我将其定义成“长度为 1 的通路”,就是更形式画的。)


2)一旦问题定义清晰了,很多时候可能得解决方案也就出现了。(一但能定义清楚了我们其实不希望有那么多“长度为 1 的通路”,我们就可以尽量让通路的长度至少为 2。)


继续加油!:)

1
1
tobeabee
非常感谢!
2022-10-26
共1条回复

7个经典应用诠释Java算法精髓

课程重应用、重实践、重思维,真正应用于实际工作开发中

1888 学习 · 112 问题

查看课程