迷宮算法之迷宮生成和迷宮尋路算法
三種迷宮生成算法
- DFS(即深度優先)算法生成,分為遞歸和非遞歸方法
- 十字分割算法生成,分為遞歸和非遞歸方法
- 隨機 Prim 算法生成,一種非遞歸方法
兩種迷宮尋路算法
- DFS 尋路,本文采用非遞歸實現
- A* 尋路,一種非遞歸方法
一些說明
- 代碼實現語言:C++
- 環境:Win10 + VS2019
- 迷宮同一要求:長寬均為奇數 N,最外圍一圈是牆,入口坐標(0, 1),出口坐標(N-1, N-2)
- 由 EasyX 制作的迷宮算法可視化程序:https://codebus.cn/teternity/post/MazeAlgorithm
三種迷宮生成算法
最外圍是牆或入口,因此操作范圍是:(1, 1) 到 (N-2, N-2)
DFS 算法生成(一種挖牆算法)
-
初始時全是牆
-
x,y 均在 1~N-2 中的奇數隨機選取一點(均為奇數),將其挖開,並將該點入棧
-
四個方向隨機選取一個方向,設當前挖開坐標為(x, y),若該方向上滿足 (x + dx*2, y + dy*2) 是牆(dx 和 dy 代表方向,取值為 1 或 -1),則挖開 (x + dx, y + dy),並重設當前點為 (x + dx*2, y + dy*2),將當前點入棧
-
以 Cur 為當前點,重復操作步驟 2
-
若 Cur 不能挖開周圍的牆,則棧執行 pop 操作,並將 Cur 重置為此時棧中的 top 元素
-
直到棧為空,說明挖牆操作結束
生成形態:
源碼(包含迭代和非迭代版,注釋 EasyX 的地方是用 EasyX 繪圖):
#include <iostream>
#include <ctime>
#include <stack>
#include <vector>
#include <algorithm>
#include <easyx.h>
using namespace std;
// 迷宮格子狀態
enum CellState:int { PATH = 0, WALL, FLAG };
// 迷宮格二維點結構
struct Point2
{
int x, y;
Point2(int _x, int _y) :x(_x), y(_y) {}
};
// 迷宮大小(要求為奇數)
const int mazeSize = 21;
// 迷宮生成接口--遞歸版
void DFS_generator(int _x, int _y, std::vector<std::vector<int>>& maze)
{
// 定義方向容器
std::vector<std::vector<int>> dir{ {1,0},{-1,0},{0,1},{0,-1} };
// 隨機打亂方向
std::random_shuffle(dir.begin(), dir.end());
// 遞歸生成迷宮
maze[_x][_y] = PATH;
for (int i = 0; i < 4; ++i)
{
if (_x + 2 * dir[i][0] >= 1 && _x + 2 * dir[i][0] <= mazeSize - 2 && _y + 2 * dir[i][1] >= 1 && _y + 2 * dir[i][1] <= mazeSize - 2
&& maze[_x + 2 * dir[i][0]][_y + 2 * dir[i][1]] == WALL)
{
maze[_x + dir[i][0]][_y + dir[i][1]] = PATH;
DFS_generator(_x + 2 * dir[i][0], _y + 2 * dir[i][1], maze);
}
}
}
// 迷宮生成接口--迭代版
void DFS_iterative_generator(std::vector<std::vector<int>>& maze)
{
// 定義棧容器
std::stack<Point2> sp;
// 定義方向容器
std::vector<std::vector<int>> dir{ {1,0},{-1,0},{0,1},{0,-1} };
// 要求參數為奇數
Point2 temp((rand() % (mazeSize - 2) + 1) | 1, (rand() % (mazeSize - 2) + 1) | 1);
sp.push(temp);
// 后續迭代生成迷宮,並繪制
while (!sp.empty())
{
if (maze[temp.x][temp.y] != PATH)
maze[temp.x][temp.y] = PATH;
// 隨機打亂方向
std::random_shuffle(dir.begin(), dir.end());
int i = 0;
for (; i < 4; ++i)
{
if (temp.x + 2 * dir[i][0] >= 1 && temp.x + 2 * dir[i][0] <= mazeSize - 2 && temp.y + 2 * dir[i][1] >= 1 && temp.y + 2 * dir[i][1] <= mazeSize - 2
&& maze[temp.x + 2 * dir[i][0]][temp.y + 2 * dir[i][1]] == WALL)
{
maze[temp.x + dir[i][0]][temp.y + dir[i][1]] = PATH;
temp.x += 2 * dir[i][0];
temp.y += 2 * dir[i][1];
sp.push(temp);
break;
}
}
if (i == 4) sp.pop();
if (!sp.empty()) temp = sp.top();
}
}
// main 函數
int main()
{
srand((unsigned)time(nullptr));
// 入口出口
Point2 start(0, 1);
Point2 end(mazeSize - 1, mazeSize - 2);
// 二維迷宮容器
std::vector<std::vector<int>> maze;
// 初始化迷宮
for (int i = 0; i < mazeSize; ++i) maze.push_back(std::vector<int>());
for (int i = 0; i < mazeSize; ++i)
for (int j = 0; j < mazeSize; ++j)
maze[i].push_back(WALL);
maze[start.x][start.y] = maze[end.x][end.y] = PATH;
// 生成迷宮(迭代和非迭代二選一生成)
DFS_generator((rand() % (mazeSize - 2) + 1) | 1, (rand() % (mazeSize - 2) + 1) | 1, maze);
// DFS_iterative_generator(maze);
// 打印迷宮
for (int j = 0; j < mazeSize; ++j)
{
for (int i = 0; i < mazeSize; ++i)
cout << maze[i][j] << " ";
cout << endl;
}
// EasyX
{
auto ret = _getwch();
const int width = 15;
initgraph(mazeSize * width, mazeSize * width);
setlinecolor(DARKGRAY);
setfillcolor(LIGHTGRAY);
for (int j = 0; j < mazeSize; ++j)
for (int i = 0; i < mazeSize; ++i)
if (maze[i][j] == WALL)
fillrectangle(i * width, j * width, i * width + width - 1, j * width + width - 1);
// saveimage(_T("D:\\maze.png"));
ret = _getwch();
closegraph();
}
return 0;
}
十字分割算法生成(是一種十字補牆算法)
-
初始時除了四周全通路
-
x,y 均在 1~N-2 中隨機選取一點(均為偶數),然后十字建牆
-
在建好的四面牆(不包含選取點)中隨機選擇三面,找奇數點開洞,使得四個子空間連通
-
對四個子空間重復操作,直到子空間不可分割為止(淡黃色是開洞位置)(何時不可分割?長度 或 寬度 不大於 1 時)
生成形態:
源碼(包含迭代和非迭代版,注釋 EasyX 的地方是用 EasyX 繪圖):
#include <iostream>
#include <ctime>
#include <stack>
#include <vector>
#include <algorithm>
#include <easyx.h>
using namespace std;
// 迷宮格子狀態
enum CellState:int { PATH = 0, WALL, FLAG };
// 迷宮格二維點結構
struct Point2
{
int x, y;
Point2(int _x, int _y) :x(_x), y(_y) {}
};
// 四維點,用於分割矩形區間
struct Point4
{
int x1, x2;
int y1, y2;
Point4(int _x1, int _x2, int _y1, int _y2) :x1(_x1), x2(_x2), y1(_y1), y2(_y2) {}
};
// 迷宮大小(要求為奇數)
const int mazeSize = 21;
// 迷宮生成接口--遞歸版
void Division_generator(int _l, int _r, int _t, int _b, std::vector<std::vector<int>>& maze)
{
// 間隔大於 1 時可分割
if (_r - _l > 1 && _b - _t > 1)
{
int i = 0;
// 要求分割點 px,py 為偶數
int px = ((rand() % (_r - _l) + _l + 1) | 1) - 1;
int py = ((rand() % (_b - _t) + _t + 1) | 1) - 1;
while (px + i <= _r || px - i >= _l || py + i <= _b || py - i >= _t)
{
if (px + i <= _r) maze[px + i][py] = WALL;
if (px - i >= _l) maze[px - i][py] = WALL;
if (py + i <= _b) maze[px][py + i] = WALL;
if (py - i >= _t) maze[px][py - i] = WALL;
++i;
}
// 定義方向容器,隨機在三面牆上開洞
// 要求開洞位置是奇數
std::vector<int> dir{ 0,1,2,3 };
std::random_shuffle(dir.begin(), dir.end());
for (int i = 0; i < 3; ++i)
{
if (dir[i] == 0)
{
int xx = (rand() % (px - _l) + _l) | 1;
maze[xx][py] = PATH;
}
else if (dir[i] == 1)
{
int xx = (rand() % (_r - px) + px) | 1;
maze[xx][py] = PATH;
}
else if (dir[i] == 2)
{
int yy = (rand() % (py - _t) + _t) | 1;
maze[px][yy] = PATH;
}
else if (dir[i] == 3)
{
int yy = (rand() % (_b - py) + py) | 1;
maze[px][yy] = PATH;
}
}
// 遞歸分割
Division_generator(_l, px - 1, _t, py - 1, maze);
Division_generator(px + 1, _r, _t, py - 1, maze);
Division_generator(_l, px - 1, py + 1, _b, maze);
Division_generator(px + 1, _r, py + 1, _b, maze);
}
}
// 迷宮生成接口--迭代版
void Division_iterative_generator(std::vector<std::vector<int>>& maze)
{
// 定義棧容器
std::stack<Point4> sp;
// 定義方向容器
std::vector<int> dir{ 0,1,2,3 };
// 要求參數為奇數
Point4 temp(1, mazeSize - 2, 1, mazeSize - 2);
sp.push(temp);
// 后續迭代生成迷宮
while (!sp.empty())
{
sp.pop();
if (temp.x2 - temp.x1 > 1 && temp.y2 - temp.y1 > 1)
{
int i = 0;
int px = ((rand() % (temp.x2 - temp.x1) + temp.x1 + 1) | 1) - 1;
int py = ((rand() % (temp.y2 - temp.y1) + temp.y1 + 1) | 1) - 1;
while (px + i <= temp.x2 || px - i >= temp.x1 || py + i <= temp.y2 || py - i >= temp.y1)
{
if (px + i <= temp.x2) maze[px + i][py] = WALL;
if (px - i >= temp.x1) maze[px - i][py] = WALL;
if (py + i <= temp.y2) maze[px][py + i] = WALL;
if (py - i >= temp.y1) maze[px][py - i] = WALL;
++i;
}
// 隨機在三面牆上開洞,要求開洞位置是奇數
std::random_shuffle(dir.begin(), dir.end());
for (int i = 0; i < 3; ++i)
{
if (dir[i] == 0)
{
int xx = (rand() % (px - temp.x1) + temp.x1) | 1;
maze[xx][py] = PATH;
}
else if (dir[i] == 1)
{
int xx = (rand() % (temp.x2 - px) + px) | 1;
maze[xx][py] = PATH;
}
else if (dir[i] == 2)
{
int yy = (rand() % (py - temp.y1) + temp.y1) | 1;
maze[px][yy] = PATH;
}
else if (dir[i] == 3)
{
int yy = (rand() % (temp.y2 - py) + py) | 1;
maze[px][yy] = PATH;
}
}
// 將三個方塊區間入棧
sp.push(Point4(px + 1, temp.x2, py + 1, temp.y2));
sp.push(Point4(temp.x1, px - 1, py + 1, temp.y2));
sp.push(Point4(px + 1, temp.x2, temp.y1, py - 1));
temp.x2 = px - 1;
temp.y2 = py - 1;
sp.push(temp);
}
else if (!sp.empty()) { temp = sp.top(); }
}
}
// main 函數
int main()
{
srand((unsigned)time(nullptr));
// 入口出口
Point2 start(0, 1);
Point2 end(mazeSize - 1, mazeSize - 2);
// 二維迷宮容器
std::vector<std::vector<int>> maze;
// 初始化迷宮
for (int i = 0; i < mazeSize; ++i) maze.push_back(std::vector<int>());
for (int i = 0; i < mazeSize; ++i)
for (int j = 0; j < mazeSize; ++j)
(i == 0 || j == 0 || i == mazeSize - 1 || j == mazeSize - 1) ? maze[i].push_back(WALL) : maze[i].push_back(PATH);
maze[start.x][start.y] = maze[end.x][end.y] = PATH;
// 生成迷宮(迭代和非迭代二選一生成)
Division_generator(1, mazeSize - 2, 1, mazeSize - 2, maze);
// Division_iterative_generator(maze);
// 打印迷宮
for (int j = 0; j < mazeSize; ++j)
{
for (int i = 0; i < mazeSize; ++i)
cout << maze[i][j] << " ";
cout << endl;
}
// EasyX
{
auto ret = _getwch();
const int width = 15;
initgraph(mazeSize * width, mazeSize * width);
setlinecolor(DARKGRAY);
setfillcolor(LIGHTGRAY);
for (int j = 0; j < mazeSize; ++j)
for (int i = 0; i < mazeSize; ++i)
if (maze[i][j] == WALL)
fillrectangle(i * width, j * width, i * width + width - 1, j * width + width - 1);
// saveimage(_T("D:\\maze.png"));
ret = _getwch();
closegraph();
}
return 0;
}
隨機 Prim 算法生成(一種非遞歸方法)
-
初始時全是牆
-
構建一牆一通路形式
-
隨機選擇一個通路,並將周圍牆入容器,標記該通路
-
在牆容器中隨機選取一堵牆,如果牆兩邊的通路沒有同時被標記,則打通該牆,並將原來未被標記的通路周圍的牆加入容器,然后將該通路標記,最后移除該牆
-
重復操作 4,直到牆容器為空,說明該算法完成,最后將被標記的通路清除標記
生成形態:
源碼(注釋 EasyX 的地方是用 EasyX 繪圖):
#include <iostream>
#include <ctime>
#include <stack>
#include <vector>
#include <algorithm>
#include <easyx.h>
using namespace std;
// 迷宮格子狀態
enum CellState:int { PATH = 0, WALL, FLAG };
// 迷宮格二維點結構
struct Point2
{
int x, y;
Point2(int _x, int _y) :x(_x), y(_y) {}
};
// 迷宮大小(要求為奇數)
const int mazeSize = 21;
// 迷宮生成接口
void Prim_generator(std::vector<std::vector<int>>& maze)
{
// 構建牆隔開通路的迷宮,奇數點為通路
for (int i = 1; i <= mazeSize - 2; i += 2)
for (int j = 1; j <= mazeSize - 2; j += 2)
maze[i][j] = PATH;
// 維護一個牆容器
std::vector<Point2> vp;
// 先隨機找一個通路
Point2 temp((rand() % (mazeSize - 2) + 1) | 1, (rand() % (mazeSize - 2) + 1) | 1);
// 將周圍牆入棧
if (temp.x - 1 >= 2) vp.push_back(Point2(temp.x - 1, temp.y));
if (temp.x + 1 <= mazeSize - 3) vp.push_back(Point2(temp.x + 1, temp.y));
if (temp.y - 1 >= 2) vp.push_back(Point2(temp.x, temp.y - 1));
if (temp.y + 1 <= mazeSize - 3) vp.push_back(Point2(temp.x, temp.y + 1));
// 標記該通路
maze[temp.x][temp.y] = FLAG;
int pos = 0;
// 后續迭代生成迷宮
while (!vp.empty())
{
// 在牆容器中隨機選取一堵牆
pos = rand() % vp.size();
temp = vp[pos];
// 記錄該牆是否打通
bool flag = false;
// 后續 if else 判斷牆所隔離通路在左右還是上下,並判斷是否打通
if (maze[temp.x + 1][temp.y] == WALL)
{
if (maze[temp.x][temp.y - 1] != maze[temp.x][temp.y + 1])
{
maze[temp.x][temp.y] = PATH;
// 對新加入的通路進行標記
if (maze[temp.x][temp.y - 1] == FLAG) { maze[temp.x][temp.y + 1] = FLAG; ++temp.y; }
else { maze[temp.x][temp.y - 1] = FLAG; --temp.y; }
flag = true;
}
}
else
{
if (maze[temp.x - 1][temp.y] != maze[temp.x + 1][temp.y])
{
maze[temp.x][temp.y] = PATH;
// 對新加入的通路進行標記
if (maze[temp.x - 1][temp.y] == FLAG) { maze[temp.x + 1][temp.y] = FLAG; ++temp.x; }
else { maze[temp.x - 1][temp.y] = FLAG; --temp.x; }
flag = true;
}
}
// 如果打通了牆,將進加入的通路周圍的牆入容器
if (flag)
{
if (temp.x - 1 >= 2 && maze[temp.x - 1][temp.y] == WALL) vp.push_back(Point2(temp.x - 1, temp.y));
if (temp.x + 1 <= mazeSize - 3 && maze[temp.x + 1][temp.y] == WALL) vp.push_back(Point2(temp.x + 1, temp.y));
if (temp.y - 1 >= 2 && maze[temp.x][temp.y - 1] == WALL) vp.push_back(Point2(temp.x, temp.y - 1));
if (temp.y + 1 <= mazeSize - 3 && maze[temp.x][temp.y + 1] == WALL) vp.push_back(Point2(temp.x, temp.y + 1));
}
// 移除該牆
vp[pos] = *(vp.end() - 1);
vp.pop_back();
}
// 將被標記的通路還原
for (auto& v1 : maze)
for (auto& v2 : v1)
if (v2 == FLAG) v2 = PATH;
}
// main 函數
int main()
{
srand((unsigned)time(nullptr));
// 入口出口
Point2 start(0, 1);
Point2 end(mazeSize - 1, mazeSize - 2);
// 二維迷宮容器
std::vector<std::vector<int>> maze;
// 初始化迷宮
for (int i = 0; i < mazeSize; ++i) maze.push_back(std::vector<int>());
for (int i = 0; i < mazeSize; ++i)
for (int j = 0; j < mazeSize; ++j)
maze[i].push_back(WALL);
maze[start.x][start.y] = maze[end.x][end.y] = PATH;
// 生成迷宮
Prim_generator(maze);
// 打印迷宮
for (int j = 0; j < mazeSize; ++j)
{
for (int i = 0; i < mazeSize; ++i)
cout << maze[i][j] << " ";
cout << endl;
}
// EasyX
{
auto ret = _getwch();
const int width = 15;
initgraph(mazeSize * width, mazeSize * width);
setlinecolor(DARKGRAY);
setfillcolor(LIGHTGRAY);
for (int j = 0; j < mazeSize; ++j)
for (int i = 0; i < mazeSize; ++i)
if (maze[i][j] == WALL)
fillrectangle(i * width, j * width, i * width + width - 1, j * width + width - 1);
// saveimage(_T("D:\\maze.png"));
ret = _getwch();
closegraph();
}
return 0;
}
兩種迷宮尋路算法
DFS 尋路,采用非遞歸實現
該尋路方法,應該算是比較簡單算法,學習數據結構棧
時一般就會接觸該算法
從起點開始,將當前點入棧,向某一方向前進。若周圍方向均不能繼續前進,則回退,直到找到終點或棧為空,就不再貼圖
生成形態:
源碼(注釋 EasyX 的地方是用 EasyX 繪圖):
#include <iostream>
#include <ctime>
#include <list>
#include <stack>
#include <vector>
#include <algorithm>
#include <easyx.h>
using namespace std;
// 迷宮格子狀態
enum CellState:int { PATH = 0, WALL, FLAG };
// 迷宮格二維點結構
struct Point2
{
int x, y;
Point2(int _x, int _y) :x(_x), y(_y) {}
bool operator == (const Point2& p2) { return x == p2.x && y == p2.y; }
};
// 迷宮大小(要求為奇數)
const int mazeSize = 21;
// 迷宮生成接口
void Prim_generator(std::vector<std::vector<int>>& maze)
{
// 構建牆隔開通路的迷宮,奇數點為通路
for (int i = 1; i <= mazeSize - 2; i += 2)
for (int j = 1; j <= mazeSize - 2; j += 2)
maze[i][j] = PATH;
// 維護一個牆容器
std::vector<Point2> vp;
// 先隨機找一個通路
Point2 temp((rand() % (mazeSize - 2) + 1) | 1, (rand() % (mazeSize - 2) + 1) | 1);
// 將周圍牆入棧
if (temp.x - 1 >= 2) vp.push_back(Point2(temp.x - 1, temp.y));
if (temp.x + 1 <= mazeSize - 3) vp.push_back(Point2(temp.x + 1, temp.y));
if (temp.y - 1 >= 2) vp.push_back(Point2(temp.x, temp.y - 1));
if (temp.y + 1 <= mazeSize - 3) vp.push_back(Point2(temp.x, temp.y + 1));
// 標記該通路
maze[temp.x][temp.y] = FLAG;
int pos = 0;
// 后續迭代生成迷宮
while (!vp.empty())
{
// 在牆容器中隨機選取一堵牆
pos = rand() % vp.size();
temp = vp[pos];
// 記錄該牆是否打通
bool flag = false;
// 后續 if else 判斷牆所隔離通路在左右還是上下,並判斷是否打通
if (maze[temp.x + 1][temp.y] == WALL)
{
if (maze[temp.x][temp.y - 1] != maze[temp.x][temp.y + 1])
{
maze[temp.x][temp.y] = PATH;
// 對新加入的通路進行標記
if (maze[temp.x][temp.y - 1] == FLAG) { maze[temp.x][temp.y + 1] = FLAG; ++temp.y; }
else { maze[temp.x][temp.y - 1] = FLAG; --temp.y; }
flag = true;
}
}
else
{
if (maze[temp.x - 1][temp.y] != maze[temp.x + 1][temp.y])
{
maze[temp.x][temp.y] = PATH;
// 對新加入的通路進行標記
if (maze[temp.x - 1][temp.y] == FLAG) { maze[temp.x + 1][temp.y] = FLAG; ++temp.x; }
else { maze[temp.x - 1][temp.y] = FLAG; --temp.x; }
flag = true;
}
}
// 如果打通了牆,將進加入的通路周圍的牆入容器
if (flag)
{
if (temp.x - 1 >= 2 && maze[temp.x - 1][temp.y] == WALL) vp.push_back(Point2(temp.x - 1, temp.y));
if (temp.x + 1 <= mazeSize - 3 && maze[temp.x + 1][temp.y] == WALL) vp.push_back(Point2(temp.x + 1, temp.y));
if (temp.y - 1 >= 2 && maze[temp.x][temp.y - 1] == WALL) vp.push_back(Point2(temp.x, temp.y - 1));
if (temp.y + 1 <= mazeSize - 3 && maze[temp.x][temp.y + 1] == WALL) vp.push_back(Point2(temp.x, temp.y + 1));
}
// 移除該牆
vp[pos] = *(vp.end() - 1);
vp.pop_back();
}
// 將被標記的通路還原
for (auto& v1 : maze)
for (auto& v2 : v1)
if (v2 == FLAG) v2 = PATH;
}
// DFS 尋路
std::list<Point2> DFS_find(const Point2& start, const Point2& end, std::vector<std::vector<int>>& maze)
{
// 定義方向容器(右、下、左、上)
std::vector<std::vector<int>> dir{ {1,0},{0,1},{-1,0},{0,-1} };
// 定義棧容器
std::list<Point2> sp;
Point2 temp = start;
sp.push_back(temp);
// 與生成算法相似,迭代 DFS 遍歷
while (!sp.empty())
{
int i = 0;
// 若可向其他方向前進,則繼續
for (; i < 4; ++i)
{
if (temp.x + dir[i][0] >= 0 && temp.x + dir[i][0] <= mazeSize - 1 && temp.y + dir[i][1] >= 0 && temp.y + dir[i][1] <= mazeSize - 1
&& maze[temp.x + dir[i][0]][temp.y + dir[i][1]] == PATH)
{
// 對走過的路進行標記
maze[temp.x][temp.y] = FLAG;
temp.x += dir[i][0];
temp.y += dir[i][1];
sp.push_back(temp);
if (temp == end) return sp;
break;
}
}
// 無路可走時回溯
if (i == 4)
{
maze[temp.x][temp.y] = FLAG;
sp.pop_back();
if (!sp.empty()) temp = sp.back();
}
}
// 將被標記的通路還原
for (auto& v1 : maze)
for (auto& v2 : v1)
if (v2 == FLAG) v2 = PATH;
return sp;
}
// main 函數
int main()
{
srand((unsigned)time(nullptr));
// 入口出口
Point2 start(0, 1);
Point2 end(mazeSize - 1, mazeSize - 2);
// 二維迷宮容器
std::vector<std::vector<int>> maze;
// 初始化迷宮
for (int i = 0; i < mazeSize; ++i) maze.push_back(std::vector<int>());
for (int i = 0; i < mazeSize; ++i)
for (int j = 0; j < mazeSize; ++j)
maze[i].push_back(WALL);
maze[start.x][start.y] = maze[end.x][end.y] = PATH;
// 生成迷宮
Prim_generator(maze);
// 打印迷宮
for (int j = 0; j < mazeSize; ++j)
{
for (int i = 0; i < mazeSize; ++i)
cout << maze[i][j] << " ";
cout << endl;
}
// 尋路
auto sp = DFS_find(start, end, maze);
// 打印路徑
cout << endl << "尋路路徑:" << endl;
int i = 0;
for (auto p2 : sp)
{
if (i++ == 5)
{
i = 0;
cout << "(" << p2.x << ", " << p2.y << ")" << endl;
continue;
}
cout << "(" << p2.x << ", " << p2.y << ")" << "、";
}
cout << endl;
// EasyX
{
auto ret = _getwch();
const int width = 15;
initgraph(mazeSize * width, mazeSize * width);
setlinecolor(DARKGRAY);
setfillcolor(LIGHTGRAY);
for (int j = 0; j < mazeSize; ++j)
for (int i = 0; i < mazeSize; ++i)
if (maze[i][j] == WALL)
fillrectangle(i * width, j * width, i * width + width - 1, j * width + width - 1);
setfillcolor(YELLOW);
for (auto p2 : sp) fillrectangle(p2.x * width, p2.y * width, p2.x * width + width - 1, p2.y * width + width - 1);
// saveimage(_T("D:\\maze.png"));
ret = _getwch();
closegraph();
}
return 0;
}
A* 尋路,一種非遞歸方法
講解 A* 尋路算法的文章很多,這里推薦:https://mp.weixin.qq.com/s/FYKR_1yBKR4GJTn0fFIuAA
一些說明:
- 用 F 值評估最短路徑,公式 F = G + H
- G 代表從起點走到當前點的步數,在代碼中等於 父節點 的 G 值加 1
- H 值是當前點到終點距離的評估,可簡單認為是無阻礙時到終點所需的步數 |dx| + |dy|
- openList 和 closeList 應該如何選用容器?
closeList
:只需要將節點加入而不需要其他操作,因此可以使用 vector,list 等容器openList
:由於需要從該容器中取出 F 值最小的節點,因此可以考慮使用能夠自排序的容器,如 priority_queue,multiset 等- 本文使用 list 及 multiset 來實現,且從終點向起點尋路,最終返回一個包含尋路路徑的 list 容器
生成形態:
源碼(注釋 EasyX 的地方是用 EasyX 繪圖):
#include <iostream>
#include <ctime>
#include <set>
#include <list>
#include <stack>
#include <vector>
#include <algorithm>
#include <easyx.h>
using namespace std;
// 迷宮格子狀態
enum CellState:int { PATH = 0, WALL, FLAG };
// 迷宮格二維點結構
struct Point2
{
int x, y;
Point2(int _x, int _y) :x(_x), y(_y) {}
bool operator == (const Point2& p2) const { return x == p2.x && y == p2.y; }
};
// 迷宮大小(要求為奇數)
const int mazeSize = 21;
// 迷宮生成接口
void Prim_generator(std::vector<std::vector<int>>& maze)
{
// 構建牆隔開通路的迷宮,奇數點為通路
for (int i = 1; i <= mazeSize - 2; i += 2)
for (int j = 1; j <= mazeSize - 2; j += 2)
maze[i][j] = PATH;
// 維護一個牆容器
std::vector<Point2> vp;
// 先隨機找一個通路
Point2 temp((rand() % (mazeSize - 2) + 1) | 1, (rand() % (mazeSize - 2) + 1) | 1);
// 將周圍牆入棧
if (temp.x - 1 >= 2) vp.push_back(Point2(temp.x - 1, temp.y));
if (temp.x + 1 <= mazeSize - 3) vp.push_back(Point2(temp.x + 1, temp.y));
if (temp.y - 1 >= 2) vp.push_back(Point2(temp.x, temp.y - 1));
if (temp.y + 1 <= mazeSize - 3) vp.push_back(Point2(temp.x, temp.y + 1));
// 標記該通路
maze[temp.x][temp.y] = FLAG;
int pos = 0;
// 后續迭代生成迷宮
while (!vp.empty())
{
// 在牆容器中隨機選取一堵牆
pos = rand() % vp.size();
temp = vp[pos];
// 記錄該牆是否打通
bool flag = false;
// 后續 if else 判斷牆所隔離通路在左右還是上下,並判斷是否打通
if (maze[temp.x + 1][temp.y] == WALL)
{
if (maze[temp.x][temp.y - 1] != maze[temp.x][temp.y + 1])
{
maze[temp.x][temp.y] = PATH;
// 對新加入的通路進行標記
if (maze[temp.x][temp.y - 1] == FLAG) { maze[temp.x][temp.y + 1] = FLAG; ++temp.y; }
else { maze[temp.x][temp.y - 1] = FLAG; --temp.y; }
flag = true;
}
}
else
{
if (maze[temp.x - 1][temp.y] != maze[temp.x + 1][temp.y])
{
maze[temp.x][temp.y] = PATH;
// 對新加入的通路進行標記
if (maze[temp.x - 1][temp.y] == FLAG) { maze[temp.x + 1][temp.y] = FLAG; ++temp.x; }
else { maze[temp.x - 1][temp.y] = FLAG; --temp.x; }
flag = true;
}
}
// 如果打通了牆,將進加入的通路周圍的牆入容器
if (flag)
{
if (temp.x - 1 >= 2 && maze[temp.x - 1][temp.y] == WALL) vp.push_back(Point2(temp.x - 1, temp.y));
if (temp.x + 1 <= mazeSize - 3 && maze[temp.x + 1][temp.y] == WALL) vp.push_back(Point2(temp.x + 1, temp.y));
if (temp.y - 1 >= 2 && maze[temp.x][temp.y - 1] == WALL) vp.push_back(Point2(temp.x, temp.y - 1));
if (temp.y + 1 <= mazeSize - 3 && maze[temp.x][temp.y + 1] == WALL) vp.push_back(Point2(temp.x, temp.y + 1));
}
// 移除該牆
vp[pos] = *(vp.end() - 1);
vp.pop_back();
}
// 將被標記的通路還原
for (auto& v1 : maze)
for (auto& v2 : v1)
if (v2 == FLAG) v2 = PATH;
}
// A*尋路容器
class A_Container
{
struct Node
{
Point2 p2;
int gVal, hVal, fVal;
Node* parent;
Node(const Point2& _p2, int _g, int _h, Node* _p) :p2(_p2), gVal(_g), hVal(_h), fVal(_g + _h), parent(_p) {}
};
public:
A_Container(const Point2& _start) :destPos(_start) {}
~A_Container()
{
for (auto& no : openList) delete no;
for (auto& no : closeList) delete no;
}
// 將節點加入 openList
void pushOpenList(const Point2& _p2)
{
int gVal = 0;
Node* par = nullptr;
if (!closeList.empty()) { par = *(--closeList.end()); gVal = par->gVal + 1; }
Node* temp = new Node(_p2, gVal, abs(_p2.x - destPos.x) + abs(_p2.y - destPos.y), par);
if (_p2.x == destPos.x && _p2.y == destPos.y) p_destNode = temp;
openList.insert(temp);
temp = nullptr;
}
// 從 openList 中取出 fVal 值最小的節點加入 closeList
auto getMinNode()
{
auto it = openList.begin();
Point2 ret = (*it)->p2;
closeList.push_back(*it);
openList.erase(it);
return ret;
}
// 獲取尋路終點,用於回溯得到最短路徑
Node* getDestNode() { return p_destNode; }
// 用於 multiset 比較 的函數對象
struct Compare
{
bool operator ()(const Node* _n1, const Node* _n2) const { return _n1->fVal < _n2->fVal; }
};
private:
Point2 destPos; // 目標位置
std::list<Node*> closeList; // closeList 容器
std::multiset<Node*, Compare> openList; // openList 自動排序
Node* p_destNode = nullptr; // 用於返回終點的位置指針
};
// A* 尋路
list<Point2> A_find(const Point2& start, const Point2& end, std::vector<std::vector<int>>& maze)
{
// 從目標位置開始尋找起點,之后回溯即可得到起點到終點的路徑
Point2 temp = end;
// 創建高效容器
A_Container cont(start);
// 將目標位置加入 openList
cont.pushOpenList(temp);
// 后續迭代進行,根據 A* 尋路規則,利用高效容器尋找最短路徑
while (true)
{
// 將 openList 中 F 值最小的節點放到 closeList 中
temp = cont.getMinNode();
// 標記節點加入到 openList 中的節點
maze[temp.x][temp.y] = FLAG;
// 后續 if 將周圍通路加入 openList
if (temp.x - 1 >= 0 && maze[temp.x - 1][temp.y] == PATH)
{
maze[temp.x - 1][temp.y] = FLAG;
cont.pushOpenList(Point2(temp.x - 1, temp.y));
if (start == Point2(temp.x - 1, temp.y)) break;
}
if (temp.y - 1 >= 0 && maze[temp.x][temp.y - 1] == PATH)
{
maze[temp.x][temp.y - 1] = FLAG;
cont.pushOpenList(Point2(temp.x, temp.y - 1));
if (start == Point2(temp.x, temp.y - 1)) break;
}
if (temp.x + 1 <= mazeSize - 1 && maze[temp.x + 1][temp.y] == PATH)
{
maze[temp.x + 1][temp.y] = FLAG;
cont.pushOpenList(Point2(temp.x + 1, temp.y));
if (start == Point2(temp.x + 1, temp.y)) break;
}
if (temp.y + 1 <= mazeSize - 1 && maze[temp.x][temp.y + 1] == PATH)
{
maze[temp.x][temp.y + 1] = FLAG;
cont.pushOpenList(Point2(temp.x, temp.y + 1));
if (start == Point2(temp.x, temp.y + 1)) break;
}
}
// 將被標記的通路還原
for (auto& v1 : maze)
for (auto& v2 : v1)
if (v2 == FLAG) v2 = PATH;
// 找到起點后,獲取高效容器中的起點,回溯得到最短路徑
auto st = cont.getDestNode();
list<Point2> retList;
while (st != nullptr)
{
retList.push_back({ st->p2 });
st = st->parent;
}
return retList;
}
// main 函數
int main()
{
srand((unsigned)time(nullptr));
// 入口出口
Point2 start(0, 1);
Point2 end(mazeSize - 1, mazeSize - 2);
// 二維迷宮容器
std::vector<std::vector<int>> maze;
// 初始化迷宮
for (int i = 0; i < mazeSize; ++i) maze.push_back(std::vector<int>());
for (int i = 0; i < mazeSize; ++i)
for (int j = 0; j < mazeSize; ++j)
maze[i].push_back(WALL);
maze[start.x][start.y] = maze[end.x][end.y] = PATH;
// 生成迷宮
Prim_generator(maze);
// 打印迷宮
for (int j = 0; j < mazeSize; ++j)
{
for (int i = 0; i < mazeSize; ++i)
cout << maze[i][j] << " ";
cout << endl;
}
// 尋路
auto sp = A_find(end, start, maze);
// 打印路徑
cout << endl << "尋路路徑:" << endl;
int i = 0;
for (auto p2 : sp)
{
if (i++ == 5)
{
i = 0;
cout << "(" << p2.x << ", " << p2.y << ")" << endl;
continue;
}
cout << "(" << p2.x << ", " << p2.y << ")" << "、";
}
cout << endl;
// EasyX
{
auto ret = _getwch();
const int width = 15;
initgraph(mazeSize * width, mazeSize * width);
setlinecolor(DARKGRAY);
setfillcolor(LIGHTGRAY);
for (int j = 0; j < mazeSize; ++j)
for (int i = 0; i < mazeSize; ++i)
if (maze[i][j] == WALL)
fillrectangle(i * width, j * width, i * width + width - 1, j * width + width - 1);
setfillcolor(YELLOW);
for (auto p2 : sp) fillrectangle(p2.x * width, p2.y * width, p2.x * width + width - 1, p2.y * width + width - 1);
// saveimage(_T("D:\\maze.png"));
ret = _getwch();
closegraph();
}
return 0;
}