LeetCode算法日记 - Day 62: 黄金矿工、不同路径III
目录
1. 黄金矿工
1.1 题目解析
1.2 解法
1.3 代码实现
2. 不同路径III
2.1 题目解析
2.2 解法
2.3 代码实现
1. 黄金矿工
https://leetcode.cn/problems/path-with-maximum-gold/
你要开发一座金矿,地质勘测学家已经探明了这座金矿中的资源分布,并用大小为 m * n
的网格 grid
进行了标注。每个单元格中的整数就表示这一单元格中的黄金数量;如果该单元格是空的,那么就是 0
。
为了使收益最大化,矿工需要按以下规则来开采黄金:
- 每当矿工进入一个单元,就会收集该单元格中的所有黄金。
- 矿工每次可以从当前位置向上下左右四个方向走。
- 每个单元格只能被开采(进入)一次。
- 不得开采(进入)黄金数目为
0
的单元格。 - 矿工可以从网格中 任意一个 有黄金的单元格出发或者是停止。
示例 1:
输入:grid = [[0,6,0],[5,8,7],[0,9,0]] 输出:24 解释: [[0,6,0],[5,8,7],[0,9,0]] 一种收集最多黄金的路线是:9 -> 8 -> 7。
示例 2:
输入:grid = [[1,0,7],[2,0,6],[3,4,5],[0,3,0],[9,0,20]] 输出:28 解释: [[1,0,7],[2,0,6],[3,4,5],[0,3,0],[9,0,20]] 一种收集最多黄金的路线是:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7。
提示:
1 <= grid.length, grid[i].length <= 15
0 <= grid[i][j] <= 100
- 最多 25 个单元格中有黄金。
1.1 题目解析
题目本质
在一个最多 15×15 的网格里找一条“简单路径”(不重复格子),路径上格子权值为黄金数,目标是让路径和最大。属于典型的位置驱动 DFS + 回溯 + 局部最优扩展问题。
常规解法
从任意有金子的格子出发,四向扩展,每走进一个格子就把该格子的金子加入路径和,并标记已访问;探索完四个方向后回退(回溯),恢复标记,继续尝试其它方向;主过程对所有非零格子作为起点取最大值。
问题分析
若不回溯或不做访问标记,会反复走回头路甚至产生环;若只从边界起步会错过中部最优路径。最坏情况下分支因子≤4、深度≤25(最多 25 个含金格),搜索上界可接受,但剪枝(遇 0/越界/已访问立即跳过)非常关键。
思路转折
要想稳且高效:
-
起点必须遍历所有非零格子(题意允许从任意含金格起/止)。
-
递归返回值定义为“从该格出发能获得的最大金子”,则状态转移是:当前格子金子 + 四邻递归收益的最大值。
-
成对回溯:进入邻格前置 vis=true,子分支失败后恢复 false,确保路径不复用格子。
1.2 解法
算法思想:
-
设 dfs(i,j) 为从 (i,j) 出发能获得的最大金子,则 dfs(i,j) = grid[i][j] + max( dfs(x,y) ),其中 (x,y) 是四邻中未访问且 grid[x][y]>0 的合法邻居;若无合法邻居则为 grid[i][j]。
-
遍历每个非零格子作为起点:标记→调用 dfs→更新答案→恢复标记。
-
回溯保证每条路径不重复格子,穷举所有可能路径。
i)取 m,n,初始化 vis[m][n]=false。
ii)双层循环遍历每个格子 (i,j):
-
grid[i][j]==0 跳过;
-
将 vis[i][j]=true,计算 cur=dfs(i,j);
-
更新 ret=max(ret,cur);将 vis[i][j]=false。
iii)dfs(si,sj):
-
设 bestNext=0;
-
枚举四方向 (x,y),若在界内、未访问、且 grid[x][y]>0:
-
回溯保证每条路径不重复格子,穷举所有可能路径
-
置 vis[x][y]=true;
-
计算 gain=dfs(x,y) 并更新 bestNext=max(bestNext,gain);
-
回溯 vis[x][y]=false;
-
-
返回 grid[si][sj] + bestNext。
iv)返回全局最大值 ret。
易错点:
-
只从边界起点搜索会漏解;必须遍历所有非零起点。
-
忘记接住子递归的返回值、或没有对四邻取最大值,会导致只加当前格金子。
-
回溯不对称(标记后未恢复)会影响后续路径。
-
访问标记要在进入邻格前置位、在该分支结束后恢复。
-
不要走进 grid==0 的格子(题意禁止)。
1.3 代码实现
class Solution {boolean[][] vis;int m,n;int[] dx = {1,-1,0,0};int[] dy = {0,0,1,-1};public int getMaximumGold(int[][] grid) {m = grid.length;n = grid[0].length;vis = new boolean[m][n];int ret = 0;for(int i = 0; i < m; i++){for(int j = 0; j < n; j++){if(grid[i][j] != 0){ // 任意非零格都可作为起点vis[i][j] = true;int cur = dfs(grid, i, j); // 从该点出发的最大收益vis[i][j] = false;ret = Math.max(ret, cur);}}}return ret;}public int dfs(int[][] grid, int si, int sj){int bestNext = 0; // 记录四邻的最佳后续收益for(int k = 0; k < 4; k++){int x = si + dx[k], y = sj + dy[k];if(x>=0 && x<m && y>=0 && y<n && !vis[x][y] && grid[x][y] != 0){vis[x][y] = true;int gain = dfs(grid, x, y); // 接住子递归返回值vis[x][y] = false;if(gain > bestNext) bestNext = gain;}}return grid[si][sj] + bestNext; // 当前格金子 + 最佳后续}
}
复杂度分析
-
时间复杂度:起点最多 25 个,每条路径深度≤25、分支因子≤4,最坏可视作 O(25 * 4^25) 上界;但网格小、含金格有限且有强剪枝(越界/0/已访问跳过),实际远小于上界。
-
空间复杂度:O(25) 递归栈深度 + O(mn) 访问标记布尔表。
2. 不同路径III
https://leetcode.cn/problems/unique-paths-iii/
在二维网格 grid
上,有 4 种类型的方格:
1
表示起始方格。且只有一个起始方格。2
表示结束方格,且只有一个结束方格。0
表示我们可以走过的空方格。-1
表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。
每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
示例 1:
输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]] 输出:2 解释:我们有以下两条路径: 1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2) 2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)
示例 2:
输入:[[1,0,0,0],[0,0,0,0],[0,0,0,2]] 输出:4 解释:我们有以下四条路径: 1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2),(2,3) 2. (0,0),(0,1),(1,1),(1,0),(2,0),(2,1),(2,2),(1,2),(0,2),(0,3),(1,3),(2,3) 3. (0,0),(1,0),(2,0),(2,1),(2,2),(1,2),(1,1),(0,1),(0,2),(0,3),(1,3),(2,3) 4. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2),(2,3)
示例 3:
输入:[[0,1],[2,0]] 输出:0 解释: 没有一条路能完全穿过每一个空的方格一次。 请注意,起始和结束方格可以位于网格中的任意位置。
提示:
1 <= grid.length * grid[0].length <= 20
2.1 题目解析
题目本质
在网格上寻找一条从起点 1 到终点 2 的“哈密顿式”路径(仅覆盖所有非障碍格一次),统计这样的路径条数。典型位置驱动 DFS + 回溯 + 计数问题。
常规解法
从起点出发,四向扩展;每走到一个格子就标记为已访问,遇到终点时判断是否恰好走遍了所有可走格;失败则回退继续试其它方向。
问题分析
若不到终点就计数、或到终点后继续扩展,都会导致错误计数;若不做访问标记,会重复走格。状态空间最坏可看作分支因子 ≤ 4,路径长度 ≤ 非障碍格数(≤ 20),枚举仍可接受。
思路转折
-
要想正确:
-
终点处必须校验“是否已覆盖全部非障碍格”,否则会把未覆盖全的路径误算进去。
-
到达终点后立即返回,不能继续向外扩展。
-
起点进入前先标记已访问,否则可能回到起点重复走格。
-
-
要想稳:预先统计 0 的数量为 step,到达 2 时用“实际步数 count 是否等于 step+1”判断是否覆盖完(起点到终点共 step+1 步)。
2.2 解法
算法思想:
-
预处理:统计空格数量 step = #zeros。
-
回溯搜索:dfs(x,y,count) 表示当前站在 (x,y),已走了 count 步;
-
到达终点 2 时,若 count == step+1(覆盖了所有非障碍格),答案 ret++;随后立即返回。
-
否则在 4 个方向上对合法未访问且非障碍格递归,进栈标记、出栈恢复。
-
-
启动:从起点 1 开始,先标记已访问,再调用 dfs(1 的坐标, 0)。
i)统计网格尺寸 m,n,分配 vis[m][n]。
ii)遍历网格计数 step = 空格(0) 的数量。
iii)找到起点 (sx,sy),将 vis[sx][sy]=true,调用 dfs(sx,sy,0),回来后恢复标记。
iv)dfs 中若 grid[x][y]==2:计算 need = step+1,若 count==need 则答案加一,并返回
v)否则枚举 4 邻 (nx,ny):越界/障碍/已访问跳过;合法则标记→递归 count+1→恢复。
vi)返回最终计数 ret。
易错点:
-
终点判断必须在递归入口先做,并立刻返回。
-
起点必须在调用 dfs 之前标记为已访问。
-
count 是“步数(边数)”,覆盖所有非障碍格需要的步数是 #zeros + 1。
-
回溯要对称:进入前标记、返回后恢复。
2.3 代码实现
class Solution {boolean[][] vis;int m,n;int[] dx = {1,-1,0,0};int[] dy = {0,0,1,-1};int step; // 空格(0) 的数量int ret; // 路径计数public int uniquePathsIII(int[][] grid) {// 防御性初始化step = 0; ret = 0;m = grid.length;n = grid[0].length;vis = new boolean[m][n];// 统计所有 0 的数量for(int i = 0; i < m; i++){for(int j = 0; j < n; j++){if(grid[i][j] == 0) step++;}}// 从起点 1 出发for(int i = 0; i < m; i++){for(int j = 0; j < n; j++){if(grid[i][j] == 1) {vis[i][j] = true; // 起点先标记dfs(grid, i, j, 0); // 已走 0 步vis[i][j] = false; // 回溯恢复}}}return ret;}public void dfs(int[][] g, int si, int sj, int count){// 到达终点:判断是否覆盖所有非障碍格(0 与 1 与 2)if(g[si][sj] == 2){int need = step + 1; // 从 1 到 2 共需走的步数if(count == need) ret++; // 覆盖完才计数return; // 终点必须停止}for(int k = 0; k < 4; k++){int x = si + dx[k], y = sj + dy[k];if(x>=0 && x<m && y>=0 && y<n && !vis[x][y] && g[x][y] != -1){vis[x][y] = true;dfs(g, x, y, count + 1);vis[x][y] = false;}}}
}
复杂度分析:
-
时间复杂度:最坏在非障碍格数 K ≤ 20 的状态空间上做回溯,枚举所有不重复路径,复杂度上界接近 O(4^K)(实际远小于上界,因越界/障碍/已访问强剪枝)。
-
空间复杂度:O(K) 递归栈 + O(mn) 访问标记。