迷宮生成算法(1)深度優先搜索
接下來一段時間,想要研究下隨機迷宮生成算法,打算在有空可時候偶爾更新一下這方面的學習過程。隨機迷宮的生成算法有很多種,比如遞歸回溯,遞歸分割,隨機Prime等等。今天是第一次嘗試隨機迷宮生成,就先試一下用遞歸的方法通過深度優先搜索來生成隨機迷宮。
首先我們來明確一下基本觀念,迷宮可以通過一個二維數組來表示,二維數組中的元素就表示存在於迷宮中的位置,他們可能是可以行走的路,也有可能是不能進入的障礙物或者圍欄。我們只要通過兩種不同的字符就可以標記障礙物和通道,比如我們使用false來表示一個位置是障礙物,而使用true來表示位置可以通行。在下面的例子中我們就沿用這個規定,整個迷宮可以使用一個二維的布爾型數組來表示。
接下來我們確定一下對迷宮的看法,我們認爲一個迷宮只能有唯一解,也就是說從起點到終點不會有兩條不一樣的路線。而遍歷迷宮的過程可以被看成是一個拆牆的過程,如果拆了一個牆會導致兩個已經被標記爲通道的方塊連接,那麼拆這面牆就是不合法的,這個條件是遞歸回溯過程中最重要的判定條件。
最後來看一下遞歸回溯算法的過程:
- 將初始位置設置爲當前位置(入棧),然後將它標記爲通路
- 從上,下,左,右四個方向尋找當前方塊的相鄰方塊,判斷找到的相鄰方塊周圍是否有其他通路,如果有則繼續尋找其他相鄰方塊,如果沒有則選擇該方塊爲當前方塊(入棧)
- 標記當前方塊爲通路,然後重複進行上一個過程
- 如果當前方塊的相鄰方塊都不滿足條件,則恢復上一個方塊爲當前方塊(出棧),並繼續執行過程2
以上就是深度優先遍歷生成隨機迷宮的基本步驟,接下來我們看代碼實現:
定義一個迷宮類,它的私有數據成員保存着記錄迷宮狀態的二維數組,查找方向,迷宮尺寸,入口點等信息:
class Maze
{
public:
//構造函數,通過傳入的參數創建並創建並初始化二維數組
Maze(int row, int column);
ostream& print();
//設置迷宮入口的內聯函數,注意這個入口並不是遍歷起點,而是在邊界上挖出的一個洞
void setEntry(int x,int y)
{
if (isValidEntry(x,y)) {
mazePtr[x - 1][y - 1] = true;
if (x == 1) {
startX = 2;
startY = y;
}
if (x == row) {
startX = row-1;
startY = y;
}
if (y == 1) {
startX = x;
startY = 2;
}
if (y == column) {
startX = 2;
startY = column-1;
}
//cout << mazePtr[x - 1][y - 1];
}
}
//創建迷宮
bool createMaze();
private:
bool isInRange(int x, int y);
bool isValidEntry(int x, int y);
//遞歸回溯的算法的核心實現
bool dig(int x, int y);
int startX;
int startY;
int row;
int column;
vector<pair<int,int>> direction = { {1,0},{0,1},{-1,0},{0,-1} };
unique_ptr<bool*[]> mazePtr;
};
然後我們在類外完成對構造函數的定義,他接受行列兩個參數來動態創建一個布爾型二維數組,並與此同時初始化爲false,允許通過初始化列表來爲動態分配的內存初始化是C++11引入的新特性,這裏爲了方便管理堆內存,採用了智能指針類型unique_ptr來管理動態分配的內存,這種智能指針類型也是C++11新增的:
Maze::Maze(int row, int column):row(row),column(column)
{
mazePtr.reset(new bool*[row]);
for (int i = 0; i < row; i++) {
mazePtr[i] = new bool[column]{false};
}
}
定義成員函數createMaze,這個函數用來執行迷宮生成算法,通過調用另一個私有成員dig來完成:
bool Maze::createMaze()
{
int x = startX;
int y = startY;
if (!dig(x, y)) {
return false;
}
return true;
}
然後就是實現dig函數,正如他的名字所顯示的那樣,該函數所做的就是從一個起點開始挖掘障礙物,只要條件允許(存在不鄰接多個路徑的新方塊)他總是儘可能的去多挖掘新的方塊:
bool Maze::dig(int x, int y)
{
if (!isInRange(x, y)) {
return false;
}
mazePtr[x - 1][y - 1] = true;
//隨機改變方向
//random_shuffle(direction.begin(),direction.end());
for (int i = 0; i < 4; i++) {
int tmpX = x + 2 * direction[i].first;
int tmpY = y + 2 * direction[i].second;
if (!isInRange(tmpX, tmpY)) {
continue;
}
if (mazePtr[tmpX-1][tmpY-1] == false) {
mazePtr[x+direction[i].first-1][y+direction[i].second-1] = true;
//print() << endl;
if (!dig(tmpX, tmpY)) {
mazePtr[x + direction[i].first - 1][y + direction[i].second - 1] = false;
continue;
}
}
}
return true;
}
剩下的就是一些提供輔助功能的成員函數了,比如判斷指定方塊是否超出迷宮邊界,計算入口是否合法,通過入口尋找遍歷起點,打印迷宮狀態等等,他們的實現如下:
bool Maze::isInRange(int x, int y)
{
if (x <= 0 or x >= row) {
return false;
}
if (y <= 0 or y >= column) {
return false;
}
return true;
}
bool Maze::isValidEntry(int x, int y)
{
if ((x == 1 and y == 1) ||
(x == 1 and y == column) ||
(x == row and y == 1) ||
(x == row and y == column)) {
return false;
}
if (x == 1 or x == row or y == 1 or y == column) {
return true;
}
return false;
}
ostream& Maze::print()
{
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
cout << mazePtr[i][j] << " ";
}
cout << endl;
}
return cout;
}
調用這段代碼可以得到下圖這種迷宮:
顯然這樣生成的迷宮不具備足夠的隨機性,接着在上面的遞歸回溯中添加一些小的改變,讓每次遍歷方向隨機選擇,這樣就可以得到下面這樣的代碼:
#include<iostream>
#include<vector>
#include<string>
#include<cctype>
#include<iterator>
#include<stdexcept>
#include<memory>
#include<math.h>
#include<algorithm>
using namespace std;
class Maze
{
public:
Maze(int row, int column);
ostream& print();
void setEntry(int x,int y)
{
if (isValidEntry(x,y)) {
mazePtr[x - 1][y - 1] = true;
if (x == 1) {
startX = 2;
startY = y;
}
if (x == row) {
startX = row-1;
startY = y;
}
if (y == 1) {
startX = x;
startY = 2;
}
if (y == column) {
startX = 2;
startY = column-1;
}
//cout << mazePtr[x - 1][y - 1];
}
}
bool createMaze();
private:
bool isInRange(int x, int y);
bool isValidEntry(int x, int y);
bool dig(int x, int y);
int startX;
int startY;
int row;
int column;
vector<pair<int,int>> direction = { {1,0},{0,1},{-1,0},{0,-1} };
unique_ptr<bool*[]> mazePtr;
};
bool Maze::isInRange(int x, int y)
{
if (x <= 0 or x >= row) {
return false;
}
if (y <= 0 or y >= column) {
return false;
}
return true;
}
bool Maze::dig(int x, int y)
{
if (!isInRange(x, y)) {
return false;
}
mazePtr[x - 1][y - 1] = true;
//每次遍歷四個方向之前,對方向進行一次隨機洗牌
random_shuffle(direction.begin(),direction.end());
for (int i = 0; i < 4; i++) {
int tmpX = x + 2 * direction[i].first;
int tmpY = y + 2 * direction[i].second;
if (!isInRange(tmpX, tmpY)) {
continue;
}
if (mazePtr[tmpX-1][tmpY-1] == false) {
mazePtr[x+direction[i].first-1][y+direction[i].second-1] = true;
//print() << endl;
if (!dig(tmpX, tmpY)) {
mazePtr[x + direction[i].first - 1][y + direction[i].second - 1] = false;
continue;
}
}
}
return true;
}
bool Maze::createMaze()
{
int x = startX;
int y = startY;
if (!dig(x, y)) {
return false;
}
return true;
}
bool Maze::isValidEntry(int x, int y)
{
if ((x == 1 and y == 1) ||
(x == 1 and y == column) ||
(x == row and y == 1) ||
(x == row and y == column)) {
return false;
}
if (x == 1 or x == row or y == 1 or y == column) {
return true;
}
return false;
}
ostream& Maze::print()
{
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
cout << mazePtr[i][j] << " ";
}
cout << endl;
}
return cout;
}
Maze::Maze(int row, int column):row(row),column(column)
{
mazePtr.reset(new bool*[row]);
for (int i = 0; i < row; i++) {
mazePtr[i] = new bool[column]{false};
}
}
int main(int argc, char*argv[])
{
Maze maze(15,15);
maze.setEntry(1, 2);
//cout << "設置入口後:" << endl;
//maze.print();
maze.createMaze();
maze.print();
system("pause");
return 0;
}
這次結果如下,隨機性增強了很多:
這次就先做着寫,接下來在慢慢補充改進。