深度优先搜索(DFS)
算法概述
【深度优先搜索】(Depth-First Search, DFS)是一种用于遍历或搜索树/图数据结构的算法。其特点是沿着一条路径尽可能深入地搜索,直到不能再深入为止,然后回溯到前一个节点,继续搜索其他路径。
算法设计思路
DFS的核心思想是:
- 从起点开始,选择一个方向深入探索
- 不断深入,直到无法继续(达到目标或碰到边界)
- 回溯到上一个状态,选择另一个方向继续探索
- 重复上述过程直到所有可能的路径都被探索完毕
DFS通常使用函数递归或显式栈来实现回溯机制。
代码实现与解析
递归实现(以图的遍历为例)
// 邻接表表示的图
vector<vector<int>> graph;
// 访问标记数组
vector<bool> visited;
// DFS函数定义
void dfs(int node) {
// 标记当前节点为已访问
visited[node] = true;
cout << "访问节点: " << node << endl;
// 遍历所有相邻节点
for (int neighbor : graph[node]) {
// 如果相邻节点未被访问,则递归访问它
if (!visited[neighbor]) {
dfs(neighbor);
}
}
}
// 图的DFS遍历,处理非连通图的情况
void dfsTraversal(int n) {
visited.resize(n, false);
for (int i = 0; i < n; i++) {
if (!visited[i]) {
dfs(i);
}
}
}
非递归实现(使用栈)
void dfsIterative(int start) {
stack<int> s;
s.push(start);
visited[start] = true;
while (!s.empty()) {
int node = s.top();
s.pop();
cout << "访问节点: " << node << endl;
// 注意:与递归DFS相比,迭代版本的访问顺序可能不同
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
s.push(neighbor);
}
}
}
}
流程图解析
DFS过程示意(以二叉树为例):
1 第1步: 访问节点1
/ \
2 3 第2步: 访问节点2
/ \ 第3步: 访问节点4
4 5 第4步: 访问节点5 (节点4没有子节点)
第5步: 返回节点2
第6步: 访问节点3 (节点2的所有路径都已探索)
复杂度分析
- 时间复杂度:O(V + E),其中V是节点数,E是边数
- 在邻接表表示下,每个节点和每条边都会被访问一次
- 在邻接矩阵表示下,时间复杂度为O(V²)
- 空间复杂度:O(V),递归实现时,递归栈的最大深度为图的深度,最坏情况为O(V)
典型应用场景
DFS在算法竞赛中有广泛应用,常见的应用场景包括:
- 【图的遍历】与节点的发现/访问
- 【连通分量】的识别
- 【拓扑排序】(通过结束时间的逆序)
- 【路径查找】问题,特别是所有路径的枚举
- 【迷宫问题】和网格搜索
- 【回溯算法】的框架,如排列组合、数独解决等
- 【图的环检测】
典型例题分析
例题1: 岛屿的数量(LeetCode 200)
问题描述:给你一个由 '1'
(陆地)和 '0'
(水)组成的二维网格,请你计算网格中岛屿的数量。岛屿由相邻的陆地连接而成,上、下、左、右四个方向视为相邻。
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int rows = grid.size();
int cols = grid[0].size();
int islands = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
islands++; // 发现一个新岛屿
dfs(grid, i, j); // 用DFS将整个岛屿标记为已访问
}
}
}
return islands;
}
private:
void dfs(vector<vector<char>>& grid, int row, int col) {
int rows = grid.size();
int cols = grid[0].size();
// 边界检查和已访问检查
if (row < 0 || col < 0 || row >= rows || col >= cols || grid[row][col] != '1')
return;
grid[row][col] = '2'; // 标记为已访问(可以设为任何非'1'值)
// 探索上下左右四个方向
dfs(grid, row + 1, col);
dfs(grid, row - 1, col);
dfs(grid, row, col + 1);
dfs(grid, row, col - 1);
}
};
分析:
- 时间复杂度:O(M×N),其中M是行数,N是列数
- 空间复杂度:O(M×N),最坏情况下,整个网格都是陆地,递归调用深度为M×N
- 核心思想:当发现一个陆地格子时,使用DFS将整个相连的陆地区域标记为已访问,避免重复计数
例题2: 全排列(LeetCode 46)
问题描述:给定一个不含重复数字的数组,返回其所有可能的全排列。
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
vector<int> current;
vector<bool> used(nums.size(), false);
dfs(nums, used, current, result);
return result;
}
private:
void dfs(vector<int>& nums, vector<bool>& used, vector<int>& current, vector<vector<int>>& result) {
// 达到排列长度,记录结果
if (current.size() == nums.size()) {
result.push_back(current);
return;
}
// 尝试每个数字
for (int i = 0; i < nums.size(); i++) {
// 跳过已使用的数字
if (used[i]) continue;
// 选择当前数字
used[i] = true;
current.push_back(nums[i]);
// 递归生成后续排列
dfs(nums, used, current, result);
// 回溯:撤销选择
current.pop_back();
used[i] = false;
}
}
};
分析:
- 时间复杂度:O(n!),其中n是数组长度,全排列的数量为n!
- 空间复杂度:O(n),递归调用栈的深度为n,额外使用了used数组和current数组
- 核心思想:使用DFS和回溯技术生成所有可能的排列
易错点与调试技巧
- 【访问标记】忘记标记节点为已访问,导致无限递归
```cpp
// 错误写法
void dfs_wrong(int node) {
// 忘记标记为已访问
for (int neighbor : graph[node]) {
} }if (!visited[neighbor]) { dfs_wrong(neighbor); }
// 正确写法 void dfs_correct(int node) { visited[node] = true; // 立即标记为已访问 for (int neighbor : graph[node]) { if (!visited[neighbor]) { dfs_correct(neighbor); } } }
2. 【边界条件】检查不充分,特别是在网格问题中
```cpp
// 错误写法
void dfs_grid_wrong(vector<vector<int>>& grid, int row, int col) {
// 缺少边界检查
if (grid[row][col] != 1) return; // 可能导致数组越界
grid[row][col] = 2;
dfs_grid_wrong(grid, row+1, col);
// ...其他方向
}
// 正确写法
void dfs_grid_correct(vector<vector<int>>& grid, int row, int col) {
int rows = grid.size();
int cols = grid[0].size();
// 完整的边界检查
if (row < 0 || row >= rows || col < 0 || col >= cols || grid[row][col] != 1)
return;
grid[row][col] = 2;
dfs_grid_correct(grid, row+1, col);
// ...其他方向
}
【递归深度】过深导致栈溢出
// 处理递归深度过大的情况 void dfs_with_depth_check(int node, int depth) { if (depth > MAX_DEPTH) return; // 限制递归深度 visited[node] = true; for (int neighbor : graph[node]) { if (!visited[neighbor]) { dfs_with_depth_check(neighbor, depth + 1); } } }
优化策略
- 【记忆化搜索】避免重复计算
```cpp
// 使用记忆化数组避免重复状态计算
vector
memo;
int dfs_with_memo(int state) { if (memo[state] != -1) return memo[state]; // 已计算过的状态直接返回
// 计算结果
int result = 0;
// ...计算逻辑
memo[state] = result; // 存储结果
return result;
}
2. 【剪枝】提前终止无效搜索路径
```cpp
void dfs_with_pruning(int node, int target, int current_sum) {
if (current_sum > target) {
return; // 剪枝:当前和已超过目标,无需继续
}
if (current_sum == target) {
// 找到解
return;
}
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
dfs_with_pruning(neighbor, target, current_sum + value[neighbor]);
visited[neighbor] = false;
}
}
}
练习题推荐
- LeetCode 733: 图像渲染(简单DFS网格问题)
- LeetCode 200: 岛屿数量(中等难度网格DFS)
- LeetCode 695: 岛屿的最大面积(中等难度网格DFS)
- LeetCode 46: 全排列(中等难度回溯)
- LeetCode 79: 单词搜索(中等难度网格DFS)
- LeetCode 417: 太平洋大西洋水流问题(中等难度多源DFS)
- POJ 1321: 棋盘问题(经典DFS练习)
- CodeForces 510B: Fox And Two Dots(环检测DFS)
总结
深度优先搜索是一种强大的图遍历算法,广泛应用于各类问题求解中。其核心思想是尽可能深入探索一条路径,再回溯尝试其他可能的选择。掌握DFS的关键在于:
- 理解递归与回溯的本质
- 正确处理访问标记和边界条件
- 灵活运用剪枝和记忆化等优化技巧
在ACM比赛中,DFS通常与回溯、记忆化、剪枝等技术结合使用,能够高效解决图遍历、排列组合、路径查找等各类问题。