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

递归专题4 - 网格DFS与回溯

递归专题4 - 网格DFS与回溯

本文是递归算法系列的第4篇,完整体系包括:

  1. 递归基础与思维方法
  2. 二叉树DFS专题
  3. 回溯算法十大类型
  4. 网格DFS与回溯 (本文)
  5. FloodFill算法专题

在这里插入图片描述

📋 目录

  • 前言
  • 一、网格DFS的特点
    • 1.1 什么是网格DFS
    • 1.2 网格DFS vs 全排列
  • 二、网格DFS的两种形态
    • 形态1: 找一条路径就停 (bool返回值)
    • 形态2: 找所有路径/最优解 (void返回值)
    • 两种形态对比
  • 三、形态1详解: 单词搜索 (bool返回值)
    • 3.1 题目: LeetCode 79 单词搜索
    • 3.2 代码实现
    • 3.3 关键点
  • 四、形态2详解: 黄金矿工 (void返回值)
    • 4.1 题目: LeetCode 1219 黄金矿工
    • 4.2 代码实现
    • 4.3 关键点
  • 五、bool vs void的本质区别
  • 六、两种实现方式对比
  • 七、常见错误
  • 八、总结

前言

网格DFS是回溯算法里比较特殊的一类。做单词搜索那道题的时候,一开始一直超时,后来发现是返回值用错了。网格DFS有两种形态:找一条路径用bool,找所有路径/最优解用void。搞清楚这两种的区别后,网格题就容易多了。

这篇文章主要讲网格DFS的两种形态、bool vs void的本质区别,以及常见的坑。


一、网格DFS的特点

1.1 什么是网格DFS

网格DFS就是在二维网格中搜索路径,每步可以往上下左右四个方向走。

// 四个方向
int dx[4] = {-1, 1, 0, 0};   // 行变化:上 下 左 右
int dy[4] = {0, 0, -1, 1};   // 列变化:上 下 左 右void dfs(int i, int j) {// 递归出口if (越界 || 不满足条件) return;// 标记vis[i][j] = true;// 四个方向探索for (int k = 0; k < 4; k++) {int x = i + dx[k];int y = j + dy[k];dfs(x, y);}// 恢复现场 (回溯)vis[i][j] = false;
}

1.2 网格DFS vs 全排列

网格DFS和全排列很相似:

特性全排列网格DFS
空间一维数组二维网格
选择选哪个数往哪个方向走
标记check[i]vis[i][j]
恢复check[i]=falsevis[i][j]=false
关心顺序

可以把网格DFS理解成"在二维空间中的全排列"。


二、网格DFS的两种形态

做了几道网格DFS后发现,有两种不同的形态,返回值不一样。

形态1:找一条路径就停 (bool返回值)

特征:

  • 题目问"是否存在"、“能否找到”
  • 找到一条满足条件的路径就返回
  • 不需要遍历所有路径

模板:

bool vis[m][n];bool dfs(grid, int i, int j, 条件) {// 出口1:越界/已访问/不符合if (越界 || vis[i][j] || 不符合条件) return false;// 出口2:找到目标if (找到目标) return true;// 标记vis[i][j] = true;// 四个方向for (int k = 0; k < 4; k++) {int x = i + dx[k];int y = j + dy[k];if (dfs(grid, x, y, 条件)) return true;  // 找到就返回}// 恢复现场vis[i][j] = false;return false;
}

关键:

  • 返回值bool
  • if (dfs(...)) return true; 实现剪枝
  • 找到一个就停,不继续搜索

典型题目:LeetCode 79 单词搜索


形态2:找所有路径/最优解 (void返回值)

特征:

  • 题目问"最大值"、“所有可能”
  • 需要遍历所有路径
  • 用全局变量记录答案

模板:

bool vis[m][n];
int ret = 0;   // 全局记录答案
int tmp = 0;   // 当前路径的值void dfs(grid, int i, int j) {// 出口:越界/已访问/不符合if (越界 || vis[i][j] || 不符合条件) return;// 累加当前格子tmp += grid[i][j];ret = max(ret, tmp);  // 更新最大值// 标记vis[i][j] = true;// 四个方向for (int k = 0; k < 4; k++) {int x = i + dx[k];int y = j + dy[k];dfs(grid, x, y);}// 恢复现场vis[i][j] = false;tmp -= grid[i][j];  // 恢复tmp
}

关键:

  • 返回值void
  • 用全局变量ret记录答案
  • 用全局变量tmp记录当前路径的值
  • 必须恢复tmp (因为是引用,不是值传递)

典型题目:LeetCode 1219 黄金矿工


两种形态对比

特性找一条路径 (bool)找最优解 (void)
返回值boolvoid
目标找到就停遍历所有
剪枝if (dfs()) return true不能提前停
全局变量不需要需要(ret, tmp)
典型题目单词搜索黄金矿工
关键字“是否”、“能否”“最大”、“所有”

判断技巧:

题目问"是否存在" -> bool
题目问"最大值" -> void + 全局变量

三、形态1详解:单词搜索 (bool返回值)

3.1 题目:LeetCode 79 单词搜索

给定一个网格和一个单词,判断网格中是否存在该单词的路径。

示例:

board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']]
word = "ABCCED"返回:true (路径:A->B->C->C->E->D)

3.2 代码实现

class Solution {
public:bool vis[7][7];int m, n;int dx[4] = {-1, 1, 0, 0};int dy[4] = {0, 0, -1, 1};bool exist(vector<vector<char>>& board, string word) {m = board.size();n = board[0].size();memset(vis, false, sizeof(vis));// 遍历所有格子作为起点for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (dfs(board, i, j, word, 0)) {return true;}}}return false;}bool dfs(vector<vector<char>>& board, int i, int j, string& word, int pos) {// 出口1:找到完整单词if (pos == word.size()) return true;// 出口2:越界if (i < 0 || i >= m || j < 0 || j >= n) return false;// 出口3:已访问或字符不匹配if (vis[i][j] || board[i][j] != word[pos]) return false;// 标记当前格子vis[i][j] = true;// 四个方向搜索for (int k = 0; k < 4; k++) {int x = i + dx[k];int y = j + dy[k];// 只要有一个方向找到就返回trueif (dfs(board, x, y, word, pos + 1)) {vis[i][j] = false;  // 找到了也要恢复return true;}}// 恢复现场vis[i][j] = false;return false;}
};

3.3 关键点

1. bool返回值的剪枝:

if (dfs(board, x, y, word, pos + 1)) {vis[i][j] = false;  // 找到了也要恢复!return true;
}

注意:找到答案后也要恢复vis,因为可能有其他起点会用到这个格子。

2. 递归出口的顺序:

// 先判断pos (必须最先判断)
if (pos == word.size()) return true;// 再判断越界
if (i < 0 || i >= m ...) return false;// 最后判断vis和字符匹配
if (vis[i][j] || board[i][j] != word[pos]) return false;

为什么pos == word.size()要最先判断?

因为如果最后一个字符匹配了,pos会变成word.size(),这时候已经找到完整单词,应该立即返回true,而不是继续判断越界(这时候i,j可能还合法)。

3. dx/dy数组的使用:

之前可能这样写:

// 传统写法:4个if
if (i-1 >= 0) if (dfs(board, i-1, j, ...)) return true;
if (i+1 < m) if (dfs(board, i+1, j, ...)) return true;
if (j-1 >= 0) if (dfs(board, i, j-1, ...)) return true;
if (j+1 < n) if (dfs(board, i, j+1, ...)) return true;

用dx/dy更简洁:

// dx/dy写法:1个for
for (int k = 0; k < 4; k++) {int x = i + dx[k];int y = j + dy[k];if (dfs(board, x, y, ...)) return true;
}

四、形态2详解:黄金矿工 (void返回值)

4.1 题目:LeetCode 1219 黄金矿工

给定一个网格,每个格子有金子,从某个格子出发,每步可以走上下左右,不能重复走,求能收集的最大金子数。

示例:

grid = [[0,6,0],[5,8,7],[0,9,0]]最大值:24 (路径:9->8->7 或 5->8->7->6)

4.2 代码实现

class Solution {
public:bool vis[16][16];int m, n;int dx[4] = {-1, 1, 0, 0};int dy[4] = {0, 0, -1, 1};int ret = 0;   // 全局最大值int tmp = 0;   // 当前路径的金子数int getMaximumGold(vector<vector<int>>& grid) {m = grid.size();n = grid[0].size();memset(vis, false, sizeof(vis));// 遍历所有格子作为起点for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] != 0) {dfs(grid, i, j);}}}return ret;}void dfs(vector<vector<int>>& grid, int i, int j) {// 出口:越界if (i < 0 || i >= m || j < 0 || j >= n) return;// 出口:已访问或是0if (vis[i][j] || grid[i][j] == 0) return;// 累加当前格子的金子tmp += grid[i][j];ret = max(ret, tmp);  // 更新最大值// 标记vis[i][j] = true;// 四个方向搜索for (int k = 0; k < 4; k++) {int x = i + dx[k];int y = j + dy[k];dfs(grid, x, y);}// 恢复现场vis[i][j] = false;tmp -= grid[i][j];  // 必须恢复tmp!}
};

4.3 关键点

1. 为什么用void不用bool?

因为要找最大值,必须遍历所有可能的路径。如果用bool,找到一条路径就停了,可能不是最优的。

2. 为什么每个节点都更新ret?

刚开始我以为只有"叶子节点"(四个方向都走不了了)才更新ret,但这是错的。

反例:

grid = [[1,2],[3,4]]

从1出发:1->2->4->3,路径上每个节点都可能是某条路径的终点:

  • 路径1: 1 (金子=1)
  • 路径1->2: 1->2 (金子=3)
  • 路径1->2->4: 1->2->4 (金子=7)
  • 路径1->2->4->3: 1->2->4->3 (金子=10)

所以每个节点都要更新ret = max(ret, tmp)

3. 为什么要恢复tmp?

因为tmp是全局变量(引用),如果不恢复,会影响其他路径的计算。

// 进入节点:tmp += grid[i][j]
tmp += grid[i][j];// 离开节点:tmp -= grid[i][j]
tmp -= grid[i][j];

这和恢复vis[i][j]是一样的道理。

4. void没有返回值,怎么回溯?

很多人疑惑:void函数没有返回值,怎么回溯?

答案:函数结束自动返回上一层,这就是回溯。

void dfs(i, j) {vis[i][j] = true;tmp += grid[i][j];dfs(i+1, j);  // 往下走// 这里自动回溯到(i, j)dfs(i, j+1);  // 往右走// 这里自动回溯到(i, j)vis[i][j] = false;tmp -= grid[i][j];
}  // 函数结束,回到上一层

五、bool vs void的本质区别

做了这两道题后,终于理解了bool和void的本质区别。

5.1 有没有全局变量记录状态

bool返回值:

  • 不需要全局变量
  • 通过返回值传递信息
  • 找到答案就返回true

void返回值:

  • 需要全局变量记录状态
  • 全局变量本身就是"状态传递"的机制
  • 不需要通过返回值传递信息

关键理解:

全局变量自动记录状态,所以不需要返回值传递

示例:

// bool:通过返回值传递"是否找到"
bool dfs(...) {if (找到) return true;  // 返回值传递信息if (dfs(...)) return true;return false;
}// void:通过全局变量记录"找到了什么"
int ret = 0;
void dfs(...) {ret = max(ret, 当前值);  // 全局变量记录信息dfs(...);  // 不需要返回值
}

5.2 是否需要提前终止

bool返回值:

  • 需要提前终止
  • if (dfs(...)) return true; 实现剪枝
  • 找到一个答案就停

void返回值:

  • 不能提前终止
  • 必须遍历所有可能
  • 全局变量记录最优解

5.3 决策流程

面对一道题,怎么选择返回值?

题目要求"是否存在"、"能否找到"↓
需要提前终止↓
用bool返回值题目要求"最大值"、"所有可能"↓
需要遍历所有↓
用void + 全局变量

六、两种实现方式对比

单词搜索和黄金矿工的对比:

特性单词搜索 (bool)黄金矿工 (void)
返回值boolvoid
目标找一条路径找最优路径
全局变量不需要ret, tmp
剪枝if (dfs()) return true不能剪枝
更新答案出口处return true每个节点更新ret
恢复状态visvis + tmp

代码对比:

// 单词搜索 (bool)
bool dfs(board, i, j, word, pos) {if (pos == word.size()) return true;  // 出口:找到if (...) return false;vis[i][j] = true;for (四个方向) {if (dfs(...)) return true;  // 找到就返回}vis[i][j] = false;return false;
}// 黄金矿工 (void)
int ret = 0, tmp = 0;
void dfs(grid, i, j) {if (...) return;tmp += grid[i][j];ret = max(ret, tmp);  // 每个节点更新vis[i][j] = true;for (四个方向) {dfs(...);  // 不返回,继续遍历}vis[i][j] = false;tmp -= grid[i][j];  // 恢复tmp
}

七、常见错误

7.1 bool题用了void

// 错误:单词搜索用void
void dfs(board, i, j, word, pos) {if (pos == word.size()) {找到了 = true;  // 用全局变量?return;}for (四个方向) {dfs(...);  // 找到了还继续搜?}
}

问题:找到一个答案后还在继续搜索,浪费时间。

7.2 void题忘记恢复tmp

// 错误:忘记恢复tmp
void dfs(grid, i, j) {tmp += grid[i][j];ret = max(ret, tmp);vis[i][j] = true;for (四个方向) dfs(...);vis[i][j] = false;// 忘记tmp -= grid[i][j]了!
}

结果:tmp一直累加,后面的路径都不对。

7.3 只在叶子节点更新答案

// 错误:只在叶子更新
void dfs(grid, i, j) {tmp += grid[i][j];vis[i][j] = true;// 判断是否是叶子bool isLeaf = true;for (四个方向) {if (可以走) isLeaf = false;}if (isLeaf) ret = max(ret, tmp);  // 只在叶子更新?
}

问题:中间节点也可能是某条路径的终点。

正确做法:每个节点都更新。

7.4 递归出口判断顺序错

// 错误:先判断越界
bool dfs(board, i, j, word, pos) {if (i < 0 || i >= m ...) return false;if (pos == word.size()) return true;  // 太晚了
}

问题:如果最后一个字符匹配了,pos变成word.size(),应该立即返回true,不应该先判断越界。

正确顺序:

// 正确:先判断pos
if (pos == word.size()) return true;
if (i < 0 || i >= m ...) return false;

八、总结

网格DFS的两种形态:

形态1:找一条路径 (bool)

  • 目标:找到就停
  • 返回值:bool
  • 剪枝:if (dfs()) return true
  • 典型题:单词搜索

形态2:找最优解 (void)

  • 目标:遍历所有
  • 返回值:void
  • 全局变量:ret, tmp
  • 每个节点都更新答案
  • 必须恢复tmp
  • 典型题:黄金矿工

判断技巧:

"是否存在"、"能否" -> bool
"最大值"、"所有" -> void + 全局变量

核心理解:

  • void不需要返回值,因为全局变量自动记录状态
  • 函数结束自动返回上一层,这就是回溯
  • 有全局变量记录状态 -> 不需要返回值传递

掌握这两种形态和它们的区别,网格DFS的题就能快速解决。


系列文章

  1. 递归基础与思维方法
  2. 二叉树DFS专题
  3. 回溯算法十大类型
  4. 网格DFS与回溯 (本文)
  5. FloodFill算法专题
http://www.dtcms.com/a/554699.html

相关文章:

  • 免费排版网站专业网站是什么意思
  • 精准且快速校准的语音神经假体研究与学习
  • 授权购买网站广州越秀网站制作
  • 马克·扎克伯格大学做的网站lnmp wordpress 500
  • Maven 入门指南
  • 网站建设电话销售技巧和话术合肥网络seo
  • 湖北人工智能建站系统软件360建筑兼职网官网
  • 怎么搭建自己的电影网站建设部网站社保联网
  • 学习笔记二:发展历程
  • 回森AI智能服务唯一服务已更新
  • 设计的素材网站有哪些软件平台开发流程
  • 山东网站建设优化技术网站建设的实践体会
  • 免费设计软件网站攻击Wordpress网站
  • Nginx简介与应用场景:从原理到实战案例
  • 网站的icp备案信息企业网站建设流程第一步是什么
  • 用easyui皮肤做漂亮的网站购物建设网站
  • 网站提供入口做网站存在的问题
  • 网站建设怎么骗人洛阳网站建设多少钱
  • 石家庄网站建设公司锦州网站建设品牌
  • Makerbase CANable V1.0 PCAN环境安装与测试
  • 唐山模板建站系统网站底版照片怎么做
  • 哪个网站开发好网站建设 方案书
  • 无备案网站 阿里联盟南宁网络企业网站
  • 商业广告的“智慧大脑”:OBOO鸥柏满天星发布屏系统赋能技术发布
  • 部署Kubernetes 1.32版
  • 从“合规”到“价值跃迁”,检测报告在信创产业中的角色升级
  • Unlock Music 多种音乐免费解锁使用教程
  • python进阶教程9:生成器和迭代器
  • 遵义网站优化达州seo排名
  • 网站建设图片按钮网站建设及维护涉及哪些内容