ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度优先搜索(DFS)算法详解与C++实现

深度优先搜索(DFS)算法详解与C++实现 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(vectorvectorint 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); } } vectorvectorint floodFill(vectorvectorint 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(vectorvectorint 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(vectorvectorint 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: vectorvectorint combinationSum(vectorint candidates, int target) { vectorvectorint res; vectorint path; sort(candidates.begin(), candidates.end()); dfs(candidates, target, 0, path, res); return res; } void dfs(vectorint nums, int remain, int start, vectorint path, vectorvectorint 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 -s5.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]方案三使用pairconst pairint,int dir[4] {{-1,0}, {1,0}, {0,-1}, {0,1}}; // 使用时nx x dir[i].first, ny y dir[i].second个人推荐方案二在可读性和使用便捷性上取得平衡。方案三在C17后可以使用结构化绑定更优雅地处理。6. 性能优化实战6.1 记忆化搜索将已计算的结果缓存避免重复计算。以斐波那契数列为例unordered_mapint, 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(vectorint nums, vectorbool used, vectorint path, vectorvectorint 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(vectorvectorint graph) { vectorint visited(graph.size(), 0); vectorint 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(vectorvectorint graph, int node, vectorint visited, vectorint 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获取逆后序 vectorint order; vectorbool 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]) { vectorint component; dfs2(u, visited, component); sccs.push_back(component); } } }在实际项目中DFS的应用远不止于此。我在开发游戏AI时曾用DFS实现过迷宫生成算法通过控制递归深度和方向选择概率可以生成不同复杂度的迷宫结构。一个实用的技巧是在递归时引入随机性避免生成过于规则的迷宫void generateMaze(int x, int y) { grid[x][y] PATH; // 标记为通路 // 随机打乱方向顺序 vectorDirection 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[(xnx)/2][(yny)/2] PATH; generateMaze(nx, ny); } } }这种基于DFS的迷宫生成算法虽然简单但效果非常好配合不同的随机种子可以生成无限多样的迷宫布局。这也体现了DFS在解决空间探索类问题时的天然优势——系统性地覆盖所有可能性同时通过剪枝和随机化控制探索方向。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进