目录
[1. 黄金矿工](#1. 黄金矿工)
[1.1 题目解析](#1.1 题目解析)
[1.2 解法](#1.2 解法)
[1.3 代码实现](#1.3 代码实现)
[2. 不同路径III](#2. 不同路径III)
[2.1 题目解析](#2.1 题目解析)
[2.2 解法](#2.2 解法)
[2.3 代码实现](#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 代码实现
java
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 代码实现
java
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) 访问标记。