目录

题目描述

LCR 099. 最小路径和

题意分析

输入一个 $m \times n$ 的非负整数网格,从左上角出发走到右下角,每一步只能向右或向下移动,要求经过的数字总和最小。

「只能向右向下」是最关键的约束:任何一条路径都恰好经过 $m + n - 1$ 个格子,走过的格子不会被再次经过;到达任意格子 (i, j) 的上一步只可能来自上方 (i - 1, j) 或左方 (i, j - 1)

数据规模是明确信号:$m, n \le 200$,而从左上到右下的不同路径条数是组合数级别的天文数字,逐条枚举必然超时,必须复用中间结果。

边界上:网格至少 $1 \times 1$;单行只能一路向右、单列只能一路向下,路径唯一;起点和终点的值都要计入总和。

解法:原地动态规划

核心思路

暴力做法是从起点出发递归枚举每一条路径,取最小和。路径条数为 $\binom{m+n-2}{m-1}$,$200 \times 200$ 的网格下完全不可行。

瓶颈在于重复计算:不同路径会反复经过同一个格子,「从起点到这个格子的最优代价」被算了一遍又一遍。观察:到达 (i, j) 的最小代价只取决于到上方、到左方两个格子的最小代价中的较小者——前缀具体怎么绕并不影响后续决策。这就是无后效性:只能向右向下,走到 (i, j) 之后不可能再回头改变之前的格子。

于是定义状态 dp[i][j]从左上角走到 (i, j) 的最小路径和,转移方程为 dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])

首行首列必须单独初始化:第一行只能从左边一路走来,dp[0][j] 就是第一行的前缀和;第一列同理是第一列的前缀和。它们只有一个来源,不能套用取 min 的通用转移。

由于每个状态只依赖上方和左方两个已算好的值,可以把结果直接写回 grid 本身省掉额外数组——按从左上到右下的顺序遍历时,被覆盖的旧值不会再被用到,原地更新不影响正确性。

解题步骤

  • 把第一列累加成前缀和 grid[i][0] += grid[i-1][0]——为什么:第一列的格子只能从上方到达,路径唯一,最小和就是一路累加。
  • 把第一行累加成前缀和 grid[0][j] += grid[0][j-1]——为什么:同理,第一行的格子只能从左方到达。
  • (1, 1) 开始按行遍历,执行 grid[i][j] += min(grid[i-1][j], grid[i][j-1])——为什么:遍历到 (i, j) 时上方和左方都已经是算好的最小路径和,转移才成立。
  • 返回 grid[m-1][n-1]——为什么:按状态定义,右下角存的就是全程的最小路径和。
  • grid = [[1,3,1],[1,5,1],[4,2,1]] 走一遍:第一列累加后为 1, 2, 6;第一行累加后为 1, 4, 5。逐格转移:(1,1) = 5 + min(4, 2) = 7(1,2) = 1 + min(5, 7) = 6(2,1) = 2 + min(7, 6) = 8(2,2) = 1 + min(6, 8) = 7。右下角为 7,对应最优路径 1 → 3 → 1 → 1 → 1

代码实现

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;

        for (int row = 1; row < m; row++) {
            grid[row][0] += grid[row - 1][0];
        }
        for (int col = 1; col < n; col++) {
            grid[0][col] += grid[0][col - 1];
        }

        for (int row = 1; row < m; row++) {
            for (int col = 1; col < n; col++) {
                // 当前格子只能从上方或左方到达,取较小路径继续。
                grid[row][col] += Math.min(grid[row - 1][col], grid[row][col - 1]);
            }
        }

        return grid[m - 1][n - 1];
    }
}
func minPathSum(grid [][]int) int {
    m := len(grid)
    n := len(grid[0])

    for row := 1; row < m; row++ {
        grid[row][0] += grid[row-1][0]
    }
    for col := 1; col < n; col++ {
        grid[0][col] += grid[0][col-1]
    }

    for row := 1; row < m; row++ {
        for col := 1; col < n; col++ {
            // 当前格子只能从上方或左方到达,取较小路径继续。
            if grid[row-1][col] < grid[row][col-1] {
                grid[row][col] += grid[row-1][col]
            } else {
                grid[row][col] += grid[row][col-1]
            }
        }
    }

    return grid[m-1][n-1]
}

复杂度分析

  • 时间复杂度:$O(mn)$,每个格子恰好被计算一次,每次转移只做一次比较和一次加法。
  • 空间复杂度:$O(1)$,不计输入本身,结果直接覆盖写回原网格,没有额外的状态数组。

关键点总结

  • 状态定义先行:dp[i][j] 即「从起点到 (i, j) 的最小路径和」,定义写清之后,转移和初始化都是顺水推舟。
  • 无后效性来自「只能向右向下」:前缀怎么走不影响后续决策,这是能用状态复用取代路径枚举的根本原因。
  • 只有单一来源的边界(首行首列)必须单独初始化,通用转移只对「两个来源都存在」的格子成立。
  • 原地更新的合法性要能说清:遍历顺序保证读到的上方、左方都已是新值,被覆盖的旧值不再被需要。
  • 面试视角:这题常被连环追问——「不许修改输入怎么办」(一维滚动数组,$O(n)$ 额外空间);「要输出路径本身怎么办」(从终点按转移来源倒着回溯);「格子允许四方向移动还成立吗」(不成立,会出现环,退化为最短路问题,需要 Dijkstra 一类算法)。提前把这三问想好,才算真正吃透。

易错点总结

  • 错误写法:首行首列没有累加初始化,保留原值就参与转移:grid = [[1,3,1],[1,5,1],[4,2,1]]grid[0][2] 仍是 1 而非前缀和 5,所有依赖它的格子跟着偏小,最终结果错误地小于 7
  • 错误写法:对第一行第一列也套用取 min 的通用转移:任意用例 → 访问 grid[-1][j]grid[i][-1],数组越界直接崩溃。
  • 错误写法:初始化第一列时写成 grid[i][0] = grid[i-1][0],忘了加自身:同一用例 → 第一列变成 1, 1, 1,丢掉格子自身代价,结果偏小。
  • 错误写法:转移取 max 而不是 min:官方样例 → 算出最大路径和 12 而不是 7
  • 错误写法:每步贪心选右、下邻居中较小者:[[1,3,1],[1,5,1],[4,2,1]] → 走出 1 → 1 → 4 → 2 → 1 = 9,错过全局最优 7,局部最优不等于全局最优。
  • 错误写法:从右下往左上遍历却仍依赖上方左方:任意用例 → 读到的是尚未更新的原始值,转移全错。
  • 错误写法:返回 grid[m][n]:任意用例 → 下标越界,正确终点是 grid[m-1][n-1]
  • 错误写法:改用一维滚动数组时换行不更新行首 dp[0] += grid[i][0]:多行用例 → 第一列没有累加,等价于首列初始化缺失,结果错误。

相似题目

题目 难度 考察点
62. 不同路径 中等 同网格模型改求路径条数,转移是求和
63. 不同路径 II 中等 路径计数加障碍物,转移需分类讨论
120. 三角形最小路径和 中等 三角形结构,自底向上可省去边界特判
174. 地下城游戏 困难 必须从终点倒推,正向递推不满足无后效性
688. 骑士在棋盘上的概率 中等 状态多一维步数,转移带概率权重
931. 下降路径最小和 中等 逐行下落,三个来源取最小
1289. 下降路径最小和 II 困难 禁止同列衔接,需维护每行最小与次小值
1301. 最大得分的路径数目 困难 同时维护最优值与方案数两个状态
1594. 矩阵的最大非负积 中等 乘积含负数,需同时维护最大值与最小值
LCR 098. 不同路径 中等 62 题镜像,路径计数模板练习
LCR 100. 三角形最小路径和 中等 120 题镜像,空间优化的对照练习
剑指 Offer 47. 礼物的最大价值 中等 同模型改求最大值,转移换成取 max