1. 深度搜索(DFS)基础概念解析
深度优先搜索(Depth-First Search)是图论和树结构中最基础的遍历算法之一。它的核心思想是"一条路走到黑"——从起始节点出发,沿着某条路径尽可能深入地探索,直到无法继续前进,然后回溯到上一个分叉点选择另一条路径继续探索。
在C++实现中,DFS通常通过递归或显式栈结构来完成。递归实现更直观,但需要注意递归深度可能导致的栈溢出问题;显式栈实现则更适合处理大规模数据。
关键特性:DFS不保证找到最短路径,但能完整遍历所有可能路径,适合解决连通性、排列组合等问题。
2. DFS核心实现框架
2.1 递归实现模板
void dfs(参数列表) { // 终止条件判断 if (满足结束条件) { 记录结果/处理方案; return; } // 遍历所有可能的选择 for (所有可选方向/选项) { if (该选择合法) { 做出选择; dfs(新参数); // 递归进入下一层 撤销选择; // 回溯 } } }2.2 显式栈实现模板
void dfs(起始节点) { stack<节点类型> stk; stk.push(起始节点); while (!stk.empty()) { 当前节点 = stk.top(); stk.pop(); if (当前节点未访问) { 标记为已访问; 处理当前节点; // 将相邻节点按特定顺序压栈 for (所有相邻节点) { if (节点合法) { stk.push(相邻节点); } } } } }3. 经典例题精解
3.1 图像渲染问题(LeetCode 733)
问题描述:给定一个二维矩阵表示的图像,从(sr,sc)位置开始,将所有与起始位置颜色相同且连通的位置填充为新颜色。
递归解法:
class Solution { const int dirX[4] = {0, 0, -1, 1}; const int dirY[4] = {-1, 1, 0, 0}; public: void dfs(vector<vector<int>>& image, int x, int y, int oldColor, int newColor) { if (x < 0 || x >= image.size() || y < 0 || y >= image[0].size() || image[x][y] != oldColor) { return; } image[x][y] = newColor; for (int i = 0; i < 4; ++i) { dfs(image, x + dirX[i], y + dirY[i], oldColor, newColor); } } vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) { if (image[sr][sc] == color) return image; dfs(image, sr, sc, image[sr][sc], color); return image; } };关键点分析:
- 使用dirX/dirY数组定义四个移动方向
- 递归前检查边界条件和颜色匹配
- 时间复杂度O(mn),空间复杂度O(mn)(递归栈深度)
3.2 岛屿最大面积问题(LeetCode 695)
问题描述:给定一个二进制矩阵,1表示陆地,0表示水域,找出最大的连通陆地面积。
优化解法:
class Solution { const int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}}; public: int dfs(vector<vector<int>>& grid, int x, int y) { if (x < 0 || x >= grid.size() || y < 0 || y >= grid[0].size() || grid[x][y] != 1) { return 0; } grid[x][y] = 0; // 标记为已访问 int area = 1; for (auto [dx, dy] : dir) { area += dfs(grid, x + dx, y + dy); } return area; } int maxAreaOfIsland(vector<vector<int>>& grid) { int maxArea = 0; for (int i = 0; i < grid.size(); ++i) { for (int j = 0; j < grid[0].size(); ++j) { if (grid[i][j] == 1) { maxArea = max(maxArea, dfs(grid, i, j)); } } } return maxArea; } };性能优化技巧:
- 原地修改矩阵值代替额外访问数组
- 方向数组使用二维初始化更直观
- 累计面积而非传递引用
4. DFS进阶应用
4.1 二叉树合并(LeetCode 617)
问题描述:合并两棵二叉树,对应位置节点值相加,空节点视为0。
优雅解法:
class Solution { public: TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { if (!t1) return t2; if (!t2) return t1; TreeNode* merged = new TreeNode(t1->val + t2->val); merged->left = mergeTrees(t1->left, t2->left); merged->right = mergeTrees(t1->right, t2->right); return merged; } };设计要点:
- 空节点处理优先
- 前序遍历顺序(根-左-右)
- 不破坏原树结构(创建新节点)
4.2 组合总和问题(LeetCode 39)
问题描述:给定无重复元素的数组和目标数,找出所有使数字和为目标数的组合(可重复使用)。
完整实现:
class Solution { public: vector<vector<int>> combinationSum(vector<int>& candidates, int target) { vector<vector<int>> res; vector<int> path; sort(candidates.begin(), candidates.end()); dfs(candidates, target, 0, path, res); return res; } void dfs(vector<int>& nums, int remain, int start, vector<int>& path, vector<vector<int>>& res) { if (remain == 0) { res.push_back(path); return; } for (int i = start; i < nums.size(); ++i) { if (remain - nums[i] < 0) break; path.push_back(nums[i]); dfs(nums, remain - nums[i], i, path, res); path.pop_back(); } } };剪枝策略:
- 先排序数组便于提前终止
- 传递start索引避免重复组合
- 剩余值检查减少无效递归
5. 常见问题与调试技巧
5.1 栈溢出问题处理
当递归深度过大时(如处理1e5级别的树),系统栈可能溢出。解决方案:
- 改用显式栈实现
- 使用尾递归优化(部分编译器支持)
- 调整系统栈大小(Linux可用ulimit -s)
5.2 重复访问问题
在网格类问题中,必须标记已访问节点,否则会导致:
- 无限递归
- 错误的结果计数
标记方法对比:
| 方法 | 优点 | 缺点 |
|---|---|---|
| 修改原数据 | 节省空间 | 破坏原始数据 |
| 额外访问数组 | 保留原数据 | 增加空间复杂度 |
| 哈希集合 | 通用性强 | 查询效率略低 |
5.3 方向数组的最佳实践
处理网格问题时,定义方向数组有几种方式:
方案一:分离坐标数组
const int dx[4] = {-1,1,0,0}; const int dy[4] = {0,0,-1,1}; // 使用时:nx = x + dx[i], ny = y + dy[i]方案二:二维方向数组
const int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}}; // 使用时:nx = x + dir[i][0], ny = y + dir[i][1]方案三:使用pair
const pair<int,int> dir[4] = {{-1,0}, {1,0}, {0,-1}, {0,1}}; // 使用时:nx = x + dir[i].first, ny = y + dir[i].second个人推荐方案二,在可读性和使用便捷性上取得平衡。方案三在C++17后可以使用结构化绑定更优雅地处理。
6. 性能优化实战
6.1 记忆化搜索
将已计算的结果缓存,避免重复计算。以斐波那契数列为例:
unordered_map<int, int> memo; int fib(int n) { if (n <= 1) return n; if (memo.count(n)) return memo[n]; return memo[n] = fib(n-1) + fib(n-2); }6.2 剪枝策略
在搜索过程中提前终止不可能得到解的路径。以排列问题为例:
void dfs(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& res) { if (path.size() == nums.size()) { res.push_back(path); return; } for (int i = 0; i < nums.size(); ++i) { if (used[i]) continue; // 剪枝:已使用的元素跳过 if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue; // 剪枝:去重 used[i] = true; path.push_back(nums[i]); dfs(nums, used, path, res); path.pop_back(); used[i] = false; } }6.3 迭代加深搜索
当解可能在较浅层时,限制递归深度逐步增加:
bool IDDFS(Node* root, int target, int max_depth) { for (int depth = 0; depth <= max_depth; ++depth) { if (DLS(root, target, depth)) return true; } return false; } bool DLS(Node* node, int target, int depth) { if (depth == 0 && node->val == target) return true; if (depth > 0) { for (auto child : node->children) { if (DLS(child, target, depth-1)) return true; } } return false; }7. 工程实践建议
7.1 参数传递优化
- 对于大型数据结构(如矩阵),使用引用传递避免拷贝
- 基本类型考虑值传递(int, char等)
- 使用const修饰不会修改的参数
7.2 调试技巧
- 打印递归树:在递归入口和出口打印缩进信息
void dfs(int level, ...) { cout << string(level*2, ' ') << "Enter level " << level << endl; // ...递归逻辑 cout << string(level*2, ' ') << "Exit level " << level << endl; }- 可视化工具:使用Graphviz生成递归调用图
- 条件断点:在特定递归深度设置断点
7.3 测试用例设计
- 极小案例:空输入、单元素
- 边界情况:最大允许尺寸
- 特殊模式:完全连通图、链状结构
- 随机测试:生成随机图验证正确性
8. 扩展应用场景
8.1 拓扑排序
DFS实现拓扑排序的典型应用:
bool topologicalSort(vector<vector<int>>& graph) { vector<int> visited(graph.size(), 0); vector<int> result; for (int i = 0; i < graph.size(); ++i) { if (!dfs(graph, i, visited, result)) { return false; // 存在环 } } reverse(result.begin(), result.end()); return true; } bool dfs(vector<vector<int>>& graph, int node, vector<int>& visited, vector<int>& result) { if (visited[node] == 1) return false; // 存在环 if (visited[node] == 2) return true; visited[node] = 1; for (int neighbor : graph[node]) { if (!dfs(graph, neighbor, visited, result)) { return false; } } visited[node] = 2; result.push_back(node); return true; }8.2 欧拉路径
使用DFS查找欧拉路径的框架:
void hierholzer(int node) { while (!adj[node].empty()) { int next = adj[node].back(); adj[node].pop_back(); hierholzer(next); } path.push_back(node); }8.3 强连通分量
Kosaraju算法实现:
void kosaraju() { // 第一次DFS获取逆后序 vector<int> order; vector<bool> visited(n, false); for (int i = 0; i < n; ++i) { if (!visited[i]) { dfs1(i, visited, order); } } // 第二次DFS在逆图上处理 reverse(order.begin(), order.end()); fill(visited.begin(), visited.end(), false); for (int u : order) { if (!visited[u]) { vector<int> component; dfs2(u, visited, component); sccs.push_back(component); } } }在实际项目中,DFS的应用远不止于此。我在开发游戏AI时曾用DFS实现过迷宫生成算法,通过控制递归深度和方向选择概率,可以生成不同复杂度的迷宫结构。一个实用的技巧是在递归时引入随机性,避免生成过于规则的迷宫:
void generateMaze(int x, int y) { grid[x][y] = PATH; // 标记为通路 // 随机打乱方向顺序 vector<Direction> dirs = {UP, DOWN, LEFT, RIGHT}; shuffle(dirs.begin(), dirs.end(), rng); for (auto dir : dirs) { int nx = x + dx[dir]; int ny = y + dy[dir]; if (isValid(nx, ny) && grid[nx][ny] == WALL) { // 打通墙壁 grid[(x+nx)/2][(y+ny)/2] = PATH; generateMaze(nx, ny); } } }这种基于DFS的迷宫生成算法虽然简单,但效果非常好,配合不同的随机种子可以生成无限多样的迷宫布局。这也体现了DFS在解决空间探索类问题时的天然优势——系统性地覆盖所有可能性,同时通过剪枝和随机化控制探索方向。