1. 程式人生 > 實用技巧 >DFS和回溯法實現走迷宮問題

DFS和回溯法實現走迷宮問題

今天記錄一下用DFS和回溯法實現走迷宮問題,輸出一個迷宮中所有可能的路徑。

迷宮我們用一個二維的字元陣列表示,其中的0表示路,1表示牆。

為了方便起見,我們從txt檔案中讀入這個陣列,txt檔案中的內容如下所示:

接下來我們寫一下從檔案中讀入這個陣列的程式碼

vector<string> initMaze(string fileName = "maze.txt") {
	ifstream fin;
	fin.open(fileName);
	if (!fin) {
		cout << "open '" << fileName << "' failed!" << endl;
		return {};
	}
	vector<string> res;

	string line;
	while (getline(fin, line)) {
		trim(line);
		res.push_back(line);
	}

	return res;
}




由於檔案中的字元之間可能存在空格,於是我們寫一個函式過濾掉這些空格,有一說一,C++在很多細節處理方面是遠沒有python方便的

void trim(string& s)
{
	int index = 0;
	if (!s.empty()) {
		while ((index = s.find(' ', index)) != string::npos) {
			s.erase(index, 1);
		}
	}
}




然後我們準備好要用到的資料結構,由於其中很多地方都要用到座標,所以我們寫一個座標的結構體

typedef struct pos {
	int x;
	int y;

	pos(int x, int y) : x(x), y(y){}
	pos() : x(-1), y(-1) {}
	pos(int v[2]) : x(v[0]), y(v[1]) {}

	pos operator+(pos p) {
		pos tmp = pos(this->x + p.x, this->y + p.y);
		return tmp;
	}

	pos operator+(int v[2]) {
		pos tmp = pos(this->x + v[0], this->y + v[1]);
		return tmp;
	}

	pos operator-(pos p) {
		pos tmp = pos(this->x - p.x, this->y - p.y);
		return tmp;
	}

	pos operator-(int v[2]) {
		pos tmp = pos(this->x - v[0], this->y - v[1]);
		return tmp;
	}

	bool operator==(pos p) {
		return this->x == p.x && this->y == p.y;
	}

	bool operator==(int v[2]) {
		return this->x == v[0] && this->y == v[1];
	}
}pos;

其中我們主要是定義了一些建構函式和過載了一些運算子。其實C++中的結構體和類基本用法是一樣的,唯一的區別就是class預設是private,struct預設是public。




接下來我們寫一下走迷宮過程的dfs函式

int dfs(stack<pos>& s, pos now, pos end, vector<vector<int>>& flag, vector<string>& maze, vector<vector<pos>>& path) {
	if (now == end) {
		copyStack2Vector(s, path);
		return 1;
	}
	else {
		flag[now.x][now.y] = 1;//標記當前位置走過

		int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };//上、下、左、右
		for (int i = 0; i < 4; i++) {
			pos next = now + dir[i];
			if (!judge(next, end, flag, maze)) 
				continue;//如果下一個節點不合法, 那麼直接繼續判斷下一個可能的位置
			
			s.push(next);

			if (dfs(s, next, end, flag, maze, path)) {
				s.pop();
			}
		}

		flag[now.x][now.y] = 0;
		s.pop();//說明這邊走不通了,把棧頂的位置彈出
		
		return 0;
	}
}




這裡需要詳細解釋一下,首先我們看一下遞迴函式的引數
stack<pos>& s, pos now, pos end, vector<vector<int>>& flag, vector<string>& maze, vector<vector<pos>>& path

由於走迷宮過程中可能走到某一步的時候發現無法繼續前進了,那麼我們只能往回退一步(回溯)。那麼我們就需要用一種資料結構去記錄我們走過的路徑,很明顯,棧是符合我們要求的(後進先出)。所以這裡的 stack& s 就是記錄目前我們走過的路徑。

pos now 是我們當前的位置,我們每次往前走一步就要更新這個變數。pos end 是最後的終點位置,始終保持不變。

vector<vector>& flag是用來記錄哪些位置是我們走過的,這個主要是為了避免我們又走到當前棧裡面存在的位置上。所以一旦一個位置走過,我們就在這個數組裡面標記一下。
vector& maze是我們的迷宮矩陣,裡面的字元只能是0或者1,flag的維度和maze的維度相同。

vector<vector>& path 是用來記錄我們找到的路徑,每當我們走到終點的時候,我們就把棧裡面的所有位置組成一條路徑,再新增到path中。

然後我們再解釋一下其中的程式碼。
既然是遞迴形式的程式碼,那麼第一步自然是先寫好邊界條件,就好比n皇后問題一樣,第一步是確定遞迴到什麼時候結束,這裡明顯是當我們到了終點就結束。
然後把此時棧裡面的路徑新增到結果path中。

每次進來,先把當前位置標記為已經在當前的路徑當中,然後接下來就是試探性往上下左右某一個方向走一步,所以我們需要判斷下一個位置是否可以走,這裡是用judge函式判斷的,其定義如下:

bool judge(pos p, pos end, vector<vector<int>>& flag, vector<string>& maze) {
	return p.x >= 0 && p.x <= end.x && p.y >= 0 && p.y <= end.y &&
		maze[p.x][p.y] != '1' && flag[p.x][p.y] == 0;//沒有越界,沒有走過,有路
}

如果當前位置沒有在迷宮外邊,並且當前位置的迷宮中標記為0,同時flag陣列沒有標記過該位置,也就是還沒在當前已經走過的路徑中,所以是可以走的

回到dfs函式,如果當前該位置不可以走,那麼就去判斷下一個位置,一旦可以走,就把下一個位置新增到棧中,然後從下一個位置開始遞迴。
如果遞迴返回的是1,說明成功過一次,那麼就彈出當前棧頂位置,去判斷下一個位置。

如果當前位置的周圍的4個位置都走不通,那麼就說明當前位置是不能走的,於是我們把當前位置從路徑中去掉,也就是最後面的

flag[now.x][now.y] = 0;
s.pop();//說明這邊走不通了,把棧頂的位置彈出
		
return 0;

針對我們上面那個迷宮,程式輸出了740種走法,如下所示:

0 0 0 0 0 0 0 0 0 1 0 0
0 1 1 1 0 1 1 1 0 0 1 0
0 1 0 0 0 1 0 0 1 0 0 0
0 0 0 1 0 0 0 0 0 0 1 0
1 0 1 0 0 0 1 0 0 0 0 0
1 0 0 0 1 0 0 0 0 1 0 0

其中@表示起始位置,$表示終點位置,中間的+ - |字元表示路徑。



最後,完整程式如下:

// Maze.cpp : 此檔案包含 "main" 函式。程式執行將在此處開始並結束。
//

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <stack>

using namespace std;

typedef struct pos {
	int x;
	int y;

	pos(int x, int y) : x(x), y(y){}
	pos() : x(-1), y(-1) {}
	pos(int v[2]) : x(v[0]), y(v[1]) {}

	pos operator+(pos p) {
		pos tmp = pos(this->x + p.x, this->y + p.y);
		return tmp;
	}

	pos operator+(int v[2]) {
		pos tmp = pos(this->x + v[0], this->y + v[1]);
		return tmp;
	}

	pos operator-(pos p) {
		pos tmp = pos(this->x - p.x, this->y - p.y);
		return tmp;
	}

	pos operator-(int v[2]) {
		pos tmp = pos(this->x - v[0], this->y - v[1]);
		return tmp;
	}

	bool operator==(pos p) {
		return this->x == p.x && this->y == p.y;
	}

	bool operator==(int v[2]) {
		return this->x == v[0] && this->y == v[1];
	}
}pos;

void trim(string& s)
{
	int index = 0;
	if (!s.empty()) {
		while ((index = s.find(' ', index)) != string::npos) {
			s.erase(index, 1);
		}
	}
}

vector<string> initMaze(string fileName = "maze.txt") {
	ifstream fin;
	fin.open(fileName);
	if (!fin) {
		cout << "open '" << fileName << "' failed!" << endl;
		return {};
	}
	vector<string> res;

	string line;
	while (getline(fin, line)) {
		trim(line);
		res.push_back(line);
	}

	return res;
}

bool judge(pos p, pos end, vector<vector<int>>& flag, vector<string>& maze) {
	return p.x >= 0 && p.x <= end.x && p.y >= 0 && p.y <= end.y &&
		maze[p.x][p.y] != '1' && flag[p.x][p.y] == 0;//沒有越界,沒有走過,有路
}

void copyStack2Vector(stack<pos> s, vector<vector<pos>>& path) {
	stack<pos> s2 = s;
	stack<pos> s3;
	vector<pos> vec;
	while (!s2.empty()) {
		pos p = s2.top();
		s2.pop();
		s3.push(p);
	}

	while (!s3.empty()) {
		pos p = s3.top();
		s3.pop();
		vec.push_back(p);
	}

	path.push_back(vec);
}

int dfs(stack<pos>& s, pos now, pos end, vector<vector<int>>& flag, vector<string>& maze, vector<vector<pos>>& path) {
	if (now == end) {
		copyStack2Vector(s, path);
		return 1;
	}
	else {
		flag[now.x][now.y] = 1;//標記當前位置走過

		int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };//上、下、左、右
		for (int i = 0; i < 4; i++) {
			pos next = now + dir[i];
			if (!judge(next, end, flag, maze)) 
				continue;//如果下一個節點不合法, 那麼直接繼續判斷下一個可能的位置
			
			s.push(next);

			if (dfs(s, next, end, flag, maze, path)) {
				s.pop();
			}
		}

		flag[now.x][now.y] = 0;
		s.pop();//說明這邊走不通了,把棧頂的位置彈出
		
		return 0;
	}
}

void printMaze(vector<string>& maze) {
	for (auto e : maze) cout << e << endl;
	cout << endl;
}

void printMaze(vector<vector<unsigned char>>& maze) {
	for (auto vec : maze) {
		for (auto e : vec)
			cout << e << " ";
		cout << endl;
	}
	cout << endl;
}

int checkDirection(pos a, pos b) {
	int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };//上、下、左、右
	for (int i = 0; i < 4; i++) {
		if (a + dir[i] == b) {
			return i;
		}
	}
}

unsigned char printPath(pos pre, pos now, pos next, pos end) {
	int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };//上、下、左、右
	if (now == pos(0, 0)) {
		return '@';
	}
	else if (now == end) {
		return '$';
	}
	else {
		int pre_now = checkDirection(pre, now), now_next = checkDirection(next, now);
		if ((pre_now == 2 && now_next == 1) || (pre_now == 1 && now_next == 2))
			return '+';//Left Down
		else if ((pre_now == 0 && now_next == 3) || (pre_now == 3 && now_next == 0))
			return '+';//Right Up
		else if ((pre_now == 2 && now_next == 0) || (pre_now == 0 && now_next == 2))
			return '+';//Left Up
		else if ((pre_now == 3 && now_next == 1) || (pre_now == 1 && now_next == 3))
			return '+';//Left Up
		else if ((pre_now == 0 && now_next == 1) || (pre_now == 1 && now_next == 0))
			return '|';//Down Up
		else if ((pre_now == 2 && now_next == 3) || (pre_now == 3 && now_next == 2))
			return '-';//Left Right
		else
			return '$';
	}

	return '$';
}

int main()
{
	//std::cout << "Hello World!\n";
	vector<string> maze = initMaze("maze.txt");
	if (maze.empty()) return 0;

	int rows = maze.size(), cols = maze[0].size();
	vector<vector<int>> flag(rows, vector<int>(cols, 0));
	printMaze(maze);

	pos start = pos(0, 0), end = pos(rows-1, cols-1);

	stack<pos> s;
	s.push(start);
	
	vector<vector<pos>> path;
	dfs(s, start, end, flag, maze, path);

	if (!path.empty()) {
		for (auto vec : path) {
			int path_len = vec.size();
			cout << "path length: " << path_len << endl;
			vector<vector<unsigned char>> maze_tmp(rows, vector<unsigned char>(cols, '0'));
			for (int i = 0; i < rows; i++)
				for (int j = 0; j < cols; j++)
					maze_tmp[i][j] = maze[i][j];
			
			for (int i = 0; i < path_len; i++) {
				pos now = vec[i];
				pos pre = (i == 0) ? now : vec[i - 1];
				pos next = (i == path_len - 1) ? now : vec[i + 1];
				maze_tmp[now.x][now.y] = printPath(pre, now, next, end);
			}

			printMaze(maze_tmp);
		}

		cout << "there exists " << path.size() << " paths for this maze" << endl;
	}
	else {
		cout << "there is no path for this maze" << endl;
	}
}