当前位置: 首页 > news >正文

下篇:回溯与剪枝的「智慧寻宝人」——DFS 进阶与网格 / 图论应用

掌握了基础回溯之后,DFS 的真正威力体现在二维网格、图结构、约束满足类问题中。本篇聚焦 DFS 在网格遍历、图连通性、复杂约束回溯中的高阶应用,带你学会「Flood Fill 泛洪算法」和「约束剪枝」两大杀器。

例题 1:岛屿数量

题目:给你一个由'1'(陆地)和'0'(水)组成的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和 / 或竖直方向上相邻的陆地连接形成。

思路讲解这是最经典的 Flood Fill(泛洪填充)问题。遍历网格每一个格子,只要遇到未访问的陆地,就以它为起点启动 DFS,把所有相连的陆地都标记为已访问(直接改成水即可),每启动一次 DFS 就代表发现一座岛屿。

完整代码(C++)

cpp

运行

#include <iostream> #include <vector> using namespace std; class Solution { private: int m, n; // 网格的行数和列数 // 从 (i,j) 出发,把所有相连的陆地淹没(标记为已访问) void dfs(vector<vector<char>>& grid, int i, int j) { // 越界或当前不是陆地,终止递归 if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != '1') { return; } grid[i][j] = '0'; // 标记为已访问(淹没) // 向上下左右四个方向深度优先遍历 dfs(grid, i - 1, j); // 上 dfs(grid, i + 1, j); // 下 dfs(grid, i, j - 1); // 左 dfs(grid, i, j + 1); // 右 } public: int numIslands(vector<vector<char>>& grid) { if (grid.empty()) return 0; m = grid.size(); n = grid[0].size(); int count = 0; // 遍历每个格子 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == '1') { count++; // 发现新岛屿 dfs(grid, i, j); // 淹没整座岛屿 } } } return count; } };

代码详解

  • 方向数组思想:这里显式写出四个方向,也可以用方向数组dirs = {{-1,0},{1,0},{0,-1},{0,1}}简化代码。
  • 原地修改技巧:直接把访问过的陆地改成'0',省去了额外的 visited 数组,空间复杂度降为 O (1)(不计递归栈)。
  • 计数时机:每次进入 DFS 前计数,因为一次 DFS 对应一整座岛屿。

例题 2:单词搜索

题目:给定一个m x n二维字符网格board和一个字符串单词word。如果word存在于网格中,返回true;否则,返回false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中相邻单元格是水平相邻或垂直相邻的单元格。

思路讲解这是二维网格上的回溯问题。遍历网格中每个字符作为起点,若与单词首字母匹配,就启动 DFS 向四周探索,逐位匹配单词字符;匹配失败则回溯,注意要标记已访问的位置避免重复使用。

完整代码(C++)

cpp

运行

#include <iostream> #include <vector> #include <string> using namespace std; class Solution { private: int m, n; vector<vector<bool>> visited; // 上下左右四个方向 int dirs[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}}; // index:当前匹配到单词的第几个字符 bool dfs(vector<vector<char>>& board, string& word, int i, int j, int index) { // 匹配到单词最后一个字符,成功 if (index == word.size() - 1) { return board[i][j] == word[index]; } // 当前字符匹配,才继续深入 if (board[i][j] == word[index]) { visited[i][j] = true; // 标记已访问 // 遍历四个方向 for (auto& dir : dirs) { int ni = i + dir[0]; int nj = j + dir[1]; // 坐标合法且未访问过 if (ni >= 0 && ni < m && nj >= 0 && nj < n && !visited[ni][nj]) { if (dfs(board, word, ni, nj, index + 1)) { return true; // 找到一条路径就直接返回 } } } visited[i][j] = false; // 回溯:撤销访问标记 } return false; } public: bool exist(vector<vector<char>>& board, string word) { m = board.size(); n = board[0].size(); visited.resize(m, vector<bool>(n, false)); // 枚举所有起点 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (dfs(board, word, i, j, 0)) { return true; } } } return false; } };

代码详解

  • 提前返回:只要找到一条合法路径就立刻返回 true,无需遍历所有可能,大幅提速。
  • 回溯本质:四个方向都探索完后,必须取消当前格子的访问标记,因为它可能属于其他路径。
  • 边界处理:先判断 index 是否到末尾,再判断字符是否匹配,逻辑清晰且避免越界。

例题 3:被围绕的区域

题目:给你一个m x n的矩阵board,找到所有被'X'围绕的区域,并将这些区域里所有的'O''X'填充。被围绕的区间不会存在于边界上。

思路讲解正向找「被包围的 O」比较复杂,逆向思维更简单:边界上的 O 以及和边界相连的 O 都不会被包围。我们从四条边界的 O 出发做 DFS,把所有不被包围的 O 标记成特殊字符(如'#');最后遍历整个矩阵,把剩余的 O 改成 X,把#还原成 O 即可。

完整代码(C++)

cpp

运行

#include <iostream> #include <vector> using namespace std; class Solution { private: int m, n; int dirs[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}}; void dfs(vector<vector<char>>& board, int i, int j) { if (i < 0 || i >= m || j < 0 || j >= n) return; if (board[i][j] != 'O') return; // 不是 O 或者已经标记过,终止 board[i][j] = '#'; // 标记为「与边界连通,不被包围」 for (auto& dir : dirs) { dfs(board, i + dir[0], j + dir[1]); } } public: void solve(vector<vector<char>>& board) { if (board.empty()) return; m = board.size(); n = board[0].size(); // 1. 遍历左右边界 for (int i = 0; i < m; i++) { if (board[i][0] == 'O') dfs(board, i, 0); if (board[i][n-1] == 'O') dfs(board, i, n-1); } // 2. 遍历上下边界 for (int j = 0; j < n; j++) { if (board[0][j] == 'O') dfs(board, 0, j); if (board[m-1][j] == 'O') dfs(board, m-1, j); } // 3. 二次遍历:O 变 X,# 变回 O for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (board[i][j] == 'O') { board[i][j] = 'X'; } else if (board[i][j] == '#') { board[i][j] = 'O'; } } } } };

代码详解

  • 逆向思维:从边界入手,标记所有「安全的 O」,剩下的 O 自然就是被包围的。
  • 两次遍历:第一次 DFS 标记,第二次修改结果,逻辑清晰且时间复杂度仍为 O (mn)。
  • 边界 DFS 起点:只从四条边上的 O 出发,避免了遍历整个矩阵启动 DFS。

例题 4:解数独

题目:编写一个程序,通过填充空格来解决数独问题。数独的解法需遵循:数字 1-9 在每一行、每一列、每个 3x3 宫格内都只能出现一次。

思路讲解数独是典型的「约束满足型回溯」问题。我们按格子顺序逐个填空,每个位置尝试 1-9 所有合法数字;填入后递归填下一个格子,若后续无解则回溯换数字。通过行、列、宫格三个数组快速判断数字是否合法,实现强力剪枝。

完整代码(C++)

cpp

运行

#include <iostream> #include <vector> using namespace std; class Solution { private: // 三个标记数组:行、列、3x3 宫格中数字是否已使用 vector<vector<bool>> row; vector<vector<bool>> col; vector<vector<bool>> box; // 找到一个解就返回 true,停止继续搜索 bool dfs(vector<vector<char>>& board, int pos) { // 所有 81 个格子都填完了,找到解 if (pos == 81) return true; int i = pos / 9; // 当前行号 int j = pos % 9; // 当前列号 int boxIdx = (i / 3) * 3 + j / 3; // 所在宫格编号 // 如果当前格子已经有数字,直接跳下一个 if (board[i][j] != '.') { return dfs(board, pos + 1); } // 尝试填入 1-9 for (int num = 1; num <= 9; num++) { // 剪枝:行、列、宫格中只要有一个出现过,就不能填 if (row[i][num] || col[j][num] || box[boxIdx][num]) { continue; } // 填入数字 board[i][j] = num + '0'; row[i][num] = true; col[j][num] = true; box[boxIdx][num] = true; // 递归填下一个格子,如果成功直接返回 if (dfs(board, pos + 1)) { return true; } // 回溯:撤销填入 board[i][j] = '.'; row[i][num] = false; col[j][num] = false; box[boxIdx][num] = false; } return false; // 1-9 都试完都不行,返回失败 } public: void solveSudoku(vector<vector<char>>& board) { // 初始化三个标记数组(下标 0 不用,1-9 对应数字) row.assign(9, vector<bool>(10, false)); col.assign(9, vector<bool>(10, false)); box.assign(9, vector<bool>(10, false)); // 先统计已有数字 for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { if (board[i][j] != '.') { int num = board[i][j] - '0'; int boxIdx = (i / 3) * 3 + j / 3; row[i][num] = true; col[j][num] = true; box[boxIdx][num] = true; } } } dfs(board, 0); // 从第 0 个格子开始填 } };

代码详解

  • 位置编码:用pos从 0 到 80 代表 81 个格子,通过除法和取模换算出行列,简化递归参数。
  • 三维约束剪枝:行、列、宫格三重校验,不合法的数字直接跳过,大幅减少搜索分支。
  • 提前终止:找到第一个解就立刻返回,因为题目保证只有唯一解,无需继续搜索。
谢谢
http://www.cnnetsun.cn/news/3582242.html

相关文章:

  • 小程序毕业设计-基于 SpringBoot+Android 的个人健身计划管理系统的设计与实现 移动端智能健身训练计划定制 APP 设计(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • 电商企业选型指南:多平台分账财税服务商
  • 计算机小程序毕设实战-基于SpringBoot的居家用药管理与健康提醒医务助手 基于前后端分离的家庭医务服务小程序【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 2026答辩翻车重灾区!别再瞎做毕业PPT|Okbiye AI学术PPT才是正确打开方式[特殊字符]
  • 缺口100万+,这行严重缺人!有计算机底子的直接躺赢...
  • 2026论文双检必过攻略!Okbiye AI论文自查|查重+AI痕迹一键预检✅
  • GitHub今日热榜 | 2026-07-22:阿波罗 11 号制导计算机(AGC)的原始源代码上榜
  • 【存储中间件之 Ceph 进阶】文件存储/块存储/对象存储/项目实战部署
  • 《郑州考研机构如何用3个策略吸引职场考生》
  • 字节开源 DeerFlow 2.0:让 AI 不止于聊天
  • TM4C1294NCPDT I2C总线协议深度解析与驱动开发实战
  • 《心癌》动画技术解析:独立短片制作流程与渲染优化
  • Unity转盘抽奖开发指南:从数据驱动到流畅动画实现
  • 超8万家面包店关停,为啥大家不去面包店买面包了?
  • AI如何变革科研写作:文献管理与期刊匹配实战
  • Kafka+Zookeeper+MongoDB分布式数据管道部署指南
  • AI大模型算力瓶颈解析:从Kimi暂停会员看Token成本与优化策略
  • 无限流跑团平台技术实现:从规则引擎到实时通信系统
  • OpenClaw与飞书集成:企业自动化办公实战指南
  • TI ISS ISP中断与DMA机制解析:嵌入式视觉系统核心驱动开发指南
  • AI设计辅助插件:提升UI设计效率的智能工具
  • C++实现农历转换:从算法原理到工程实践
  • 创建64位远线程调用所需ASM函数
  • MibSPI多缓冲串行接口:解放CPU,实现高效嵌入式数据通信
  • C++ std::any性能瓶颈分析与五种优化方案深度对比
  • 问题现象与原因
  • RHCSA简单实用Linux
  • C++继承机制深度解析:从概念到实践,掌握面向对象设计核心
  • 木材烘干房用什么高温风机?看过这三点再决定
  • 新终端流量风口:努比亚豆包 AI 手机,GEO 优化新增流量阵地深度解读