目录
介绍
深度优先搜索(Depth-First Search,DFS)是一种用于图和树等数据结构中的遍历或搜索算法
它通过尽可能深地探索图的分支,直到不能再继续为止,然后回溯并继续探索其他分支
步骤
- 从起始节点开始,标记该节点为已访问
- 探索从当前节点出发的每个相邻节点
- 对于每个相邻节点,如果它还没有被访问,就递归地访问它,然后继续深度探索
- 如果没有未访问的相邻节点,或者达到终止条件(自己设置的,例如找到目标节点或遍历完整个图),则回溯到上一个节点,继续查找未访问的节点
- 重复以上步骤,直到所有节点都被访问或找到了解决问题的路径
应用
遍历图/树
- DFS 可以用来访问并处理图或树中的所有节点
- 通过深度优先的方式,它会首先访问一个节点,然后递归地访问其相邻节点,以此类推,直到所有节点都被访问
查找路径/解决问题
- DFS 可以用于查找从一个节点到另一个节点的路径
- 或者用于解决搜索问题,如迷宫问题、拓扑排序、生成树等
迷宫
下面用迷宫作为例子,展示一下如何用递归/非递归版本的dfs解决迷宫问题
首先,先定义迷宫
// 定义迷宫的大小 const int m = 5; const int n = 5; // 定义方向,上、右、下、左 int dir[4][2] = { {-1, 0}, {0, 1}, {1, 0}, {0, -1} }; // 定义三元组结构体 struct Triple { int x, y, dir; //横坐标,纵坐标,下一个位置的方向 Triple(int _x, int _y, int _dir) : x(_x), y(_y), dir(_dir) {} };
这里,还引入了一个新变量,dir -- 记录下一个位置的方向(别问为什么,问就是老师布置的作业中有这个)
非递归版本
思路
虽然是迷宫问题,但实际上其实就是找能走的位置,然后将它记录下来
- 而我们dfs是需要回溯的,也就是会将最后一个元素删掉,而且记录元素都是插在尾巴上
- 所以!栈就是我们最好的选择
由于我们要记录下一个位置的方向,但是我们又无法提前预知到
- 虽然下一个方向的不知道,但我们可以知道这次要选择的方向(也就是上一个位置的下一个方向)
- 所以,存储的时候,我们要插入两次
- 一次是上一个位置+它对应的下一个方向(这个是我们最终路径上有效的值)
- 一次是这次的位置+随便一个值(因为这个是为了让我们取位置用的,方向没用到)
- 注意第二次插入的临时值,在下次探索的时候,需要先pop掉,再进行操作
还有就是,我们这里存进去的下标都是从1开始的,但实际使用的时候需要-1,因为计算机里的下标是从0开始的,所以会有相互转换的处理
代码
// 非递归DFS算法
bool solveMaze(stack<Triple>& s, int maze[m][n],int sx, int sy, int tx, int ty) {
s.push(Triple(sx, sy, 0));
maze[sx-1][sy-1] = 2;
while (!s.empty()) {
Triple cur = s.top();
int x = cur.x - 1, y = cur.y - 1, d = 0;
if (x == tx - 1 && y == ty - 1) {
return true;
}
bool found = false;
while (d < 4) {
int nx = x + dir[d][0];
int ny = y + dir[d][1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && maze[nx][ny] == 0) {
s.pop(); //说明上一个位置有效,所以先把那个临时值pop掉
found = true;
s.push(Triple(x+1, y+1, d)); //有效值
s.push(Triple(nx + 1, ny + 1, d)); //临时值
x = nx;
y = ny;
maze[x][y] = 2; // 标记为已访问过的路径
break;
}
else {
d++; // 尝试下一个方向
}
}
if (found == false) {
s.pop(); //四个方向都不行,说明这个位置不对
}
}
return false; // 没有找到通路
}
递归版本
因为递归版本老师要求我们求出所有通路,所以,元素结构也要变一下,不需要记录那个方向了
struct Path {
vector<pair<int, int>> steps;
};
思路
还是一样的思路
每次取上一个位置的坐标进行四个方向的探索,如果可以,就把这个结点插入,并且以该结点为基础进行下一次的探索
代码
int a[m][n]; //用于计算类型(其实可以不用的(挠头))
void findPaths(Path& currentPath,decltype(a)maze, int sx, int sy, int tx, int ty) {
if (sx == tx && sy == ty ) {
// 找到一条通路,输出路径
cout << "通路:";
for (const auto& step : currentPath.steps) {
cout << "(" << step.first << "," << step.second << ") ";
}
cout << endl;
currentPath.steps.pop_back(); //把终点pop掉
return;
}
for (int d = 0; d < 4; d++) {
int size = currentPath.steps.size();
if (size == 0) {
break;
}
int x = currentPath.steps[size - 1].first, y = currentPath.steps[size - 1].second;
int nx = x + dir[d][0]-1;
int ny = y + dir[d][1]-1;
if (nx >= 0 && nx < m && ny >= 0 && ny < n && maze[nx][ny] == 0) {
// 标记当前位置已访问
maze[nx][ny] = 2;
// 添加当前步骤到路径
currentPath.steps.push_back(make_pair(nx+1, ny+1));
// 继续搜索下一步
findPaths( currentPath,maze, nx+1, ny+1, tx,ty);
// 恢复状态,回溯
if (currentPath.steps.size() != 0) {
currentPath.steps.pop_back();
}
maze[nx][ny] = 0;
}
}
}