(上图是使用随机化Prim算法生成一个200x200的迷宫的过程)
Github项目地址:maze
前言
本文中的迷宫指的是最常见的那种迷宫:迷宫整体轮廓是二维矩形,迷宫里的格子是正方形的,格子上下左右各相邻另外一个格子(边和角除外),迷宫内部没有环路,也没有无法到达的格子,起点在一个角(本文中为左上角),终点在另一个角(本文中为右下角),从起点到终点有且仅有一条路径:
每个格子都可以抽象成图上的一个点,而相邻且连通的格子间的路径可以抽象成图像的一个边,不难发现这实际上是一棵树。
常见的迷宫生成算法有4种:深度优先算法、随机化Kruskal算法、随机化Prim算法和递归分割算法。对这4种算法再分类可以分为2类:前3种归为一类,最后1种自成一类。为什么我在这里将前3种归为一类呢?因为前3种算法有着相似的流程:
将所有相邻格子连接起来,形成一个无向图G={V,E}。构造G的一个子图G'={V',E'},G'要求是一颗树,且V'=V
前3种算法的不同之处在于第2步构造G'时所使用的方法不同。
数据结构
前面我们已经看到了,迷宫可以抽象为一颗树,在此我们使用邻接表来存储树,邻接表类AdjacencyList定义如下(省略了实现和非关键代码):
class AdjacencyList
{
public:
AdjacencyList(int row = 0, int column = 0);
void connect(int i, int j);
void disconnect(int i, int j);
int row() const { return m_row; }
int column() const { return m_column; }
std::vector
std::vector
private:
int m_row;
int m_column;
std::vector
std::vector
};
迷宫中的格子抽象成的点按照从左向右,从上向下进行编号,row()和column()用来获得迷宫的大小,connect()用来连接两个相邻的点,disconnect()用来断开两个连接的点,neighbor()用来获取与某个点相连接的点,surround()用来获取与某个点相邻(不一定连接在一起)的点。
深度优先算法
算法过程图示:
使用深度优先算法构造得到的G'实际上就是G的深度优先搜索树,与普通的深度优先算法不同的是,选择下一个要染灰的点时需要加入一些随机性,否则迷宫每次都会生成得一摸一样。算法代码为:
AdjacencyList DeepFirstSearch::generate()
{
enum Color
{
White,
Gray,
Black
};
AdjacencyList result(m_row, m_column);
vector
vector
current.reserve(static_cast
color[0] = Gray;
current.push_back(0);
while (current.size())
{
int last = current.back();
random_shuffle(result.surround(last).begin(), result.surround(last).end());
auto iter = result.surround(last).cbegin();
for (; iter != result.surround(last).cend(); iter++)
{
if (color[static_cast
{
color[static_cast
result.connect(last, *iter);
current.push_back(*iter);
break;
}
}
// all adjacent points are found
if (iter == result.surround(last).cend())
{
current.pop_back();
color[static_cast
}
}
return result;
}
随机化Kruskal算法
算法过程图示:
Kruskal原本是构造最小生成树的算法,但迷宫中的边都没有权重(或者说权重都是0),因此在把新边加入树中时随机选择一个就可以。在选择边时需要引入随机性,否则每次都会得到相同的结果。算法代码为:
AdjacencyList Kruskal::generate()
{
AdjacencyList result(m_row, m_column);
UnionFind uf(m_row * m_column);
vector
for (int i = 0; i < m_row * m_column; i++)
{
for (auto iter : result.surround(i))
{
// avoid duplicate edge
if (i > iter)
{
edges.push_back(pair
}
}
}
random_shuffle(edges.begin(), edges.end());
for (auto iter : edges)
{
if(!uf.connected(iter.first, iter.second))
{
uf.connect(iter.first, iter.second);
result.connect(iter.first, iter.second);
}
}
return result;
}
随机化Prim算法
算法过程图示:
Prim同样是构造最小生成树的算法,注意事项和Kruskal相同。算法代码为:
AdjacencyList Prim::generate()
{
AdjacencyList result(m_row, m_column);
vector
linked[0] = true;
set
paths.insert(pair
paths.insert(pair
static default_random_engine e(static_cast
while (!paths.empty())
{
// random select a path in paths
int pos = static_cast
auto iter = paths.begin();
advance(iter, pos);
// connect the two node of path
result.connect(iter->first, iter->second);
// get the node not in linked
int current = 0;
if (!linked[static_cast
{
current = iter->first;
}
else
{
current = iter->second;
}
// add the node to linked
linked[static_cast
// add all not accessed path to paths, and delete all invalid path from paths
for (auto i : result.surround(current))
{
pair
if (!linked[static_cast
{
paths.insert(currentPath);
}
else
{
paths.erase(paths.find(currentPath));
}
}
}
return result;
}
递归分割算法
算法过程图示:
如果说前面3种算法是通过“拆墙”来构造迷宫,那么递归分割算法就是通过“建墙”来构造迷宫了。在当前要处理的矩形中随机选择一个点,然后以这个点为中心向上下左右4个方向各建一堵墙,其中3堵墙都要留门,不然就会出现无法到达的区域。对被这4堵墙划分成的4个矩形递归执行这个过程,就能得到一个迷宫。算法代码为:
AdjacencyList RecursiveDivision::generate()
{
AdjacencyList result(m_row, m_column);
result.connectAllSurround();
divide(result, 0, 0, m_column - 1, m_row - 1);
return result;
}
void RecursiveDivision::divide(AdjacencyList &list, int left, int top, int right, int bottom)
{
// the x range of input is [left, right]
// the y range of input is [top, bottom]
if ((right - left < 1) || (bottom - top < 1))
{
return;
}
static default_random_engine e(static_cast
int x = static_cast
int y = static_cast
vector
for (int i = left; i <= right; i++)
{
int p = y * m_column + i;
int q = (y + 1) * m_column + i;
toDisconnect.emplace_back(p, q);
}
for (int i = top; i <= bottom; i++)
{
int p = i * m_column + x;
int q = i * m_column + x + 1;
toDisconnect.emplace_back(p, q);
}
// the position of no gap wall relative to (x, y), 0:top 1:bottom 2:left 3:right
int position = e() % 4;
int gapPos[4] = {0};
// get the gap position
gapPos[0] = static_cast
gapPos[1] = static_cast
gapPos[2] = static_cast
gapPos[3] = static_cast
for (int i = 0; i <= 3; i++)
{
if (position != i)
{
int p = 0;
int q = 0;
if (i <= 1) // the gap is in top or bottom
{
p = gapPos[i] * m_column + x;
q = gapPos[i] * m_column + x + 1;
}
else // the gap is in left or right
{
p = y * m_column + gapPos[i];
q = (y + 1) * m_column + gapPos[i];
}
pair
toDisconnect.erase(find(toDisconnect.begin(), toDisconnect.end(), pair));
}
}
for (auto &pair : toDisconnect)
{
list.disconnect(pair.first, pair.second);
}
divide(list, left, top, x, y);
divide(list, x + 1, top, right, y);
divide(list, left, y + 1, x, bottom);
divide(list, x + 1, y + 1, right, bottom);
}
参考链接
Maze generation algorithm:https://en.wikipedia.org/wiki/Maze_generation_algorithm