LeetCode算法日记 - Day 62: 黄金矿工、不同路径III

目录

[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) 访问标记。

相关推荐
ACEEE12222 小时前
解读DeepSeek-V3.2-Exp:基于MLA架构的Lightning Index如何重塑长上下文效率
人工智能·深度学习·算法·架构·deep
qq_437896433 小时前
unsigned 是等于 unsigned int
开发语言·c++·算法·c
952363 小时前
数据结构—单链表
c语言·数据结构·学习
Learn Beyond Limits3 小时前
Using per-item Features|使用每项特征
人工智能·python·神经网络·算法·机器学习·ai·吴恩达
greentea_20133 小时前
Codeforces Round 863 A. Insert Digit (1811)
数据结构·算法
小南家的青蛙4 小时前
LeetCode第51题 - N 皇后
算法·leetcode·职场和发展
文火冰糖的硅基工坊4 小时前
[创业之路-682]:实即虚,虚即实。真正的技术壁垒,藏在光路之外、电路之下、代码之中。
人工智能·算法·系统架构·制造·创业·产业链
2401_841495644 小时前
【计算机视觉】霍夫变换检测
图像处理·人工智能·python·opencv·算法·计算机视觉·霍夫变换
半桶水专家5 小时前
C语言中的setitimer函数详解
c语言·开发语言·算法