今天记录一下用DFS和回溯法实现走迷宫问题,输出一个迷宫中从起点到终点的所有可能的路径。
迷宫我们用一个二维的字符数组表示,其中的0表示路,1表示墙。 为了方便起见,我们从txt文件中读入这个数组,txt文件中的内容如下所示:
接下来我们写一下从文件中读入这个数组的代码:
1 vector initMaze(string fileName = "maze.txt") 2 { 3 ifstream fin; 4 fin.open(fileName); 5 if (!fin) 6 { 7 cout << "open '" << fileName << "' failed!" << endl; 8 return {}; 9 } 10 11 vector res; 12 string line; 13 while (getline(fin, line)) 14 { 15 trim(line); 16 res.push_back(line); 17 } 18 return res; 19 }
由于文件中的字符之间可能存在空格,于是我们写一个函数过滤掉这些空格,有一说一,C++在很多细节处理方面是远没有python方便的
1 void trim(string &s) 2 { 3 int index = 0; 4 if (!s.empty()) 5 { 6 while ((index = s.find(' ', index)) != string::npos) 7 { 8 s.erase(index, 1); 9 } 10 } 11 }
然后我们准备好要用到的数据结构,由于其中很多地方都要用到坐标,所以我们写一个坐标的结构体
1 typedef struct pos 2 { 3 int x; 4 int y; 5 pos(int x, int y) : x(x), y(y) {} 6 pos() : x(-1), y(-1) {} 7 pos(int v[2]) : x(v[0]), y(v[1]) {} 8 pos operator+(pos p) 9 { 10 pos tmp = pos(this->x + p.x, this->y + p.y); 11 return tmp; 12 } 13 pos operator+(int v[2]) 14 { 15 pos tmp = pos(this->x + v[0], this->y + v[1]); 16 return tmp; 17 } 18 pos operator-(pos p) 19 { 20 pos tmp = pos(this->x - p.x, this->y - p.y); 21 return tmp; 22 } 23 pos operator-(int v[2]) 24 { 25 pos tmp = pos(this->x - v[0], this->y - v[1]); 26 return tmp; 27 } 28 bool operator==(pos p) { return this->x == p.x && this->y == p.y; } 29 bool operator==(int v[2]) { return this->x == v[0] && this->y == v[1]; } 30 } pos;
其中我们主要是定义了一些构造函数和重载了一些运算符。其实C++中的结构体和类基本用法是一样的,唯一的区别就是class默认是private,struct默认是public。
接下来我们写一下走迷宫过程的dfs函数
1 int dfs(stack &s, pos now, pos end, vector<vector> &flag, vector &maze, vector<vector> &path) 2 { 3 if (now == end) 4 { 5 copyStack2Vector(s, path); 6 return 1; 7 } 8 else 9 { 10 flag[now.x][now.y] = 1; //标记当前位置走过 11 int dir[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; //上、下、左、右 12 for (int i = 0; i < 4; i++) 13 { 14 pos next = now + dir[i]; 15 if (!judge(next, end, flag, maze)) 16 continue; //如果下一个节点不合法, 那么直接继续判断下一个可能的位置 17 s.push(next); 18 if (dfs(s, next, end, flag, maze, path)) 19 { 20 s.pop(); 21 } 22 } 23 flag[now.x][now.y] = 0; 24 s.pop(); //说明这边走不通了,把栈顶的位置弹出 25 return 0; 26 } 27 }
这里需要详细解释一下,首先我们看一下递归函数的参数
stack& s, pos now, pos end, vector<vector>& flag, vector& maze, vector<vector>& path
由于走迷宫过程中可能走到某一步的时候发现无法继续前进了,那么我们只能往回退一步(回溯)。那么我们就需要用一种数据结构去记录我们走过的路径,很明显,栈是符合我们要求的(后进先出)。所以这里的 stack& s 就是记录目前我们走过的路径。
pos now 是我们当前的位置,我们每次往前走一步就要更新这个变量。
pos end 是最后的终点位置,始终保持不变。
vector<vector>& flag是用来记录哪些位置是我们走过的,这个主要是为了避免我们又走到当前栈里面存在的位置上。所以一旦一个位置走过,我们就在这个数组里面标记一下。
vector& maze是我们的迷宫矩阵,里面的字符只能是0或者1,flag的维度和maze的维度相同。
vector<vector>& path 是用来记录我们找到的路径,每当我们走到终点的时候,我们就把栈里面的所有位置组成一条路径,再添加到path中。
然后我们再解释一下其中的代码。 既然是递归形式的代码,那么第一步自然是先写好边界条件,就好比n皇后问题一样,第一步是确定递归到什么时候结束,这里明显是当我们到了终点就结束。然后把此时栈里面的路径添加到结果path中。 每次进来,先把当前位置标记为已经在当前的路径当中,然后接下来就是试探性往上下左右某一个方向走一步,所以我们需要判断下一个位置是否可以走,这里是用judge函数判断的,其定义如下:
1 bool judge(pos p, pos end, vector<vector> &flag, vector &maze) 2 { 3 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; //没有越界,没有走过,有路 4 }
如果当前位置没有在迷宫外边,并且当前位置的迷宫中标记为0,同时flag数组没有标记过该位置,也就是还没在当前已经走过的路径中,所以是可以走的 回到dfs函数,如果当前该位置不可以走,那么就去判断下一个位置,一旦可以走,就把下一个位置添加到栈中,然后从下一个位置开始递归。 如果递归返回的是1,说明成功过一次,那么就弹出当前栈顶位置,去判断下一个位置。 如果当前位置的周围的4个位置都走不通,那么就说明当前位置是不能走的,于是我们把当前位置从路径中去掉,也就是最后面的
flag[now.x][now.y] = 0; s.pop();//说明这边走不通了,把栈顶的位置弹出 return 0;
针对我们上面那个迷宫,程序输出了740种走法,如下所示:
其中@表示起始位置,$表示终点位置,中间的+ - |字符表示路径。
最后,完整程序如下:
1 // Maze.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 2 // 3 4 #include <iostream> 5 #include <fstream> 6 #include <vector> 7 #include <string> 8 #include <stack> 9 10 using namespace std; 11 12 typedef struct pos { 13 int x; 14 int y; 15 16 pos(int x, int y) : x(x), y(y){} 17 pos() : x(-1), y(-1) {} 18 pos(int v[2]) : x(v[0]), y(v[1]) {} 19 20 pos operator+(pos p) { 21 pos tmp = pos(this->x + p.x, this->y + p.y); 22 return tmp; 23 } 24 25 pos operator+(int v[2]) { 26 pos tmp = pos(this->x + v[0], this->y + v[1]); 27 return tmp; 28 } 29 30 pos operator-(pos p) { 31 pos tmp = pos(this->x - p.x, this->y - p.y); 32 return tmp; 33 } 34 35 pos operator-(int v[2]) { 36 pos tmp = pos(this->x - v[0], this->y - v[1]); 37 return tmp; 38 } 39 40 bool operator==(pos p) { 41 return this->x == p.x && this->y == p.y; 42 } 43 44 bool operator==(int v[2]) { 45 return this->x == v[0] && this->y == v[1]; 46 } 47 }pos; 48 49 void trim(string& s) 50 { 51 int index = 0; 52 if (!s.empty()) { 53 while ((index = s.find(' ', index)) != string::npos) { 54 s.erase(index, 1); 55 } 56 } 57 } 58 59 vector<string> initMaze(string fileName = "maze.txt") { 60 ifstream fin; 61 fin.open(fileName); 62 if (!fin) { 63 cout << "open '" << fileName << "' failed!" << endl; 64 return {}; 65 } 66 vector<string> res; 67 68 string line; 69 while (getline(fin, line)) { 70 trim(line); 71 res.push_back(line); 72 } 73 74 return res; 75 } 76 77 bool judge(pos p, pos end, vector<vector<int>>& flag, vector<string>& maze) { 78 return p.x >= 0 && p.x <= end.x && p.y >= 0 && p.y <= end.y && 79 maze[p.x][p.y] != '1' && flag[p.x][p.y] == 0;//没有越界,没有走过,有路 80 } 81 82 void copyStack2Vector(stack<pos> s, vector<vector<pos>>& path) { 83 stack<pos> s2 = s; 84 stack<pos> s3; 85 vector<pos> vec; 86 while (!s2.empty()) { 87 pos p = s2.top(); 88 s2.pop(); 89 s3.push(p); 90 } 91 92 while (!s3.empty()) { 93 pos p = s3.top(); 94 s3.pop(); 95 vec.push_back(p); 96 } 97 98 path.push_back(vec); 99 } 100 101 int dfs(stack<pos>& s, pos now, pos end, vector<vector<int>>& flag, vector<string>& maze, vector<vector<pos>>& path) { 102 if (now == end) { 103 copyStack2Vector(s, path); 104 return 1; 105 } 106 else { 107 flag[now.x][now.y] = 1;//标记当前位置走过 108 109 int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };//上、下、左、右 110 for (int i = 0; i < 4; i++) { 111 pos next = now + dir[i]; 112 if (!judge(next, end, flag, maze)) 113 continue;//如果下一个节点不合法, 那么直接继续判断下一个可能的位置 114 115 s.push(next); 116 117 if (dfs(s, next, end, flag, maze, path)) { 118 s.pop(); 119 } 120 } 121 122 flag[now.x][now.y] = 0; 123 s.pop();//说明这边走不通了,把栈顶的位置弹出 124 125 return 0; 126 } 127 } 128 129 void printMaze(vector<string>& maze) { 130 for (auto e : maze) cout << e << endl; 131 cout << endl; 132 } 133 134 void printMaze(vector<vector<unsigned char>>& maze) { 135 for (auto vec : maze) { 136 for (auto e : vec) 137 cout << e << " "; 138 cout << endl; 139 } 140 cout << endl; 141 } 142 143 int checkDirection(pos a, pos b) { 144 int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };//上、下、左、右 145 for (int i = 0; i < 4; i++) { 146 if (a + dir[i] == b) { 147 return i; 148 } 149 } 150 } 151 152 unsigned char printPath(pos pre, pos now, pos next, pos end) { 153 int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };//上、下、左、右 154 if (now == pos(0, 0)) { 155 return '@'; 156 } 157 else if (now == end) { 158 return '$'; 159 } 160 else { 161 int pre_now = checkDirection(pre, now), now_next = checkDirection(next, now); 162 if ((pre_now == 2 && now_next == 1) || (pre_now == 1 && now_next == 2)) 163 return '+';//Left Down 164 else if ((pre_now == 0 && now_next == 3) || (pre_now == 3 && now_next == 0)) 165 return '+';//Right Up 166 else if ((pre_now == 2 && now_next == 0) || (pre_now == 0 && now_next == 2)) 167 return '+';//Left Up 168 else if ((pre_now == 3 && now_next == 1) || (pre_now == 1 && now_next == 3)) 169 return '+';//Left Up 170 else if ((pre_now == 0 && now_next == 1) || (pre_now == 1 && now_next == 0)) 171 return '|';//Down Up 172 else if ((pre_now == 2 && now_next == 3) || (pre_now == 3 && now_next == 2)) 173 return '-';//Left Right 174 else 175 return '$'; 176 } 177 178 return '$'; 179 } 180 181 int main() 182 { 183 //std::cout << "Hello World! "; 184 vector<string> maze = initMaze("maze.txt"); 185 if (maze.empty()) return 0; 186 187 int rows = maze.size(), cols = maze[0].size(); 188 vector<vector<int>> flag(rows, vector<int>(cols, 0)); 189 printMaze(maze); 190 191 pos start = pos(0, 0), end = pos(rows-1, cols-1); 192 193 stack<pos> s; 194 s.push(start); 195 196 vector<vector<pos>> path; 197 dfs(s, start, end, flag, maze, path); 198 199 if (!path.empty()) { 200 for (auto vec : path) { 201 int path_len = vec.size(); 202 cout << "path length: " << path_len << endl; 203 vector<vector<unsigned char>> maze_tmp(rows, vector<unsigned char>(cols, '0')); 204 for (int i = 0; i < rows; i++) 205 for (int j = 0; j < cols; j++) 206 maze_tmp[i][j] = maze[i][j]; 207 208 for (int i = 0; i < path_len; i++) { 209 pos now = vec[i]; 210 pos pre = (i == 0) ? now : vec[i - 1]; 211 pos next = (i == path_len - 1) ? now : vec[i + 1]; 212 maze_tmp[now.x][now.y] = printPath(pre, now, next, end); 213 } 214 215 printMaze(maze_tmp); 216 } 217 218 cout << "there exists " << path.size() << " paths for this maze" << endl; 219 } 220 else { 221 cout << "there is no path for this maze" << endl; 222 } 223 }