目录

题目描述

1289. 下降路径最小和 II

题意分析

给一个 $n \times n$ 的整数矩阵,从第一行任选一格出发,每次向下走一行,要求相邻两行选的列号不能相同(可以跳到任意其他列,不限于相邻列),一直走到最后一行,求路径上元素之和的最小值。

「列号不同即可,不限相邻」是本题与经典下降路径题的唯一区别,却完全改变了转移的形态:经典版每格只能来自上一行的三个位置,是常数个来源;本题每格可以来自上一行的 $n-1$ 个位置,来源数量随 n 增长。

数据规模:$n$ 最多 200,矩阵元素在 $[-99, 99]$。$n^2 = 4 \times 10^4$,而朴素转移是 $O(n^3) = 8 \times 10^6$,其实也能过——但这道题被标为困难,考的正是能否把它降到 $O(n^2)$。

元素可以为负,这一点很重要:它排除了任何「路径和单调递增」的假设,也意味着不能用「只要挑最小的往下走」这种局部贪心,必须真正做完整的状态转移。

边界:$n = 1$(只有一格,答案就是它本身,且不存在「列号不同」的约束)、上一行存在多个相同的最小值、全负矩阵。

解法:DP + 维护两小值

核心思路

dp[j] 表示走到上一行第 j 列时的最小路径和。当前行第 j 列不能接在上一行同列之后,因此朴素转移是:

\[next[j] = grid[i][j] + \min_{k \ne j} dp[k]\]

若每个 j 都扫描上一行,时间复杂度是 $O(n^3)$。真正需要的只有上一行的最小值 min1、其列号 minCol,以及排除该位置后的最小值 min2:当前列不是 minCol 时接 min1,否则接 min2

这里的 min2 是按位置排第二,而不是严格不同的第二小数值。若上一行是 [6,6,7],排除第一个 6 后仍可使用另一个 6,所以应有 min1 = min2 = 6

不变量:处理第 i 行前,dp[j] 是到达第 i-1 行第 j 列的最优值;扫描 dp 后,min1/minCol/min2 能在排除任意一列时给出合法最小值。由此计算出的 next[j] 枚举了所有合法前驱并取到最优。

正确性:对第一行结论显然成立。假设 dp 对上一行正确,任意到达当前格的路径都必须来自某个不同列,其最优前缀恰由上述最小值或次小值给出;反之该前驱对应一条合法路径。因此 next 对当前行正确,归纳到最后一行后取最小值即为答案。

解题步骤

  1. 用第一行初始化 dp
  2. 对每个后续行,先完整扫描旧 dp,求 min1minColmin2
  3. 对当前行每一列 j,若 j == minCol 就加 min2,否则加 min1,写入新数组 next
  4. next 替换 dp;所有行完成后返回 dp 的最小值。

样例 [[1,2,3],[4,5,6],[7,8,9]]:第一轮得到 [6,6,7];此时两个最小值都为 6,下一轮得到 [13,14,15],答案是 13。

反例也来自这一步:若把次小值定义成“严格大于最小值”,面对 [6,6,7] 会错误地取 7。转移到下一行第 0 列时本可使用第 1 列的 6,却会被迫使用 7。

代码实现

class Solution {
    public int minFallingPathSum(int[][] grid) {
        int n = grid.length;
        int[] dp = new int[n];
        for (int j = 0; j < n; j++) {
            dp[j] = grid[0][j];
        }

        for (int i = 1; i < n; i++) {
            int min1 = Integer.MAX_VALUE;
            int min2 = Integer.MAX_VALUE;
            int minCol = -1;
            for (int j = 0; j < n; j++) {
                int v = dp[j];
                if (v < min1) {
                    min2 = min1;
                    min1 = v;
                    minCol = j;
                } else if (v < min2) {
                    min2 = v;
                }
            }

            int[] ndp = new int[n];
            for (int j = 0; j < n; j++) {
                int add = j == minCol ? min2 : min1;
                ndp[j] = grid[i][j] + add;
            }
            dp = ndp;
        }

        int answer = dp[0];
        for (int v : dp) {
            answer = Math.min(answer, v);
        }
        return answer;
    }
}
func minFallingPathSum(grid [][]int) int {
	n := len(grid)
	dp := make([]int, n)
	copy(dp, grid[0])

	for i := 1; i < n; i++ {
		min1, min2 := int(^uint(0)>>1), int(^uint(0)>>1)
		minCol := -1
		for j := 0; j < n; j++ {
			v := dp[j]
			if v < min1 {
				min2 = min1
				min1 = v
				minCol = j
			} else if v < min2 {
				min2 = v
			}
		}

		ndp := make([]int, n)
		for j := 0; j < n; j++ {
			add := min1
			if j == minCol {
				add = min2
			}
			ndp[j] = grid[i][j] + add
		}
		dp = ndp
	}

	answer := dp[0]
	for _, v := range dp {
		if v < answer {
			answer = v
		}
	}
	return answer
}

复杂度分析

  • 时间复杂度:$O(n^2)$。每行各用一次线性扫描求两小值、一次线性扫描完成转移。
  • 空间复杂度:$O(n)$。只保留上一行和当前行的状态。

关键点总结

  • “排除一个位置后取最小值”只需最小值、最小值位置和按位置计的次小值。
  • 重复最小值必须保留:次小值允许等于最小值。
  • 先统计完整旧行,再写新行,避免滚动数组的新旧状态混用。
  • 最后一行的落点不固定,必须再取一次全行最小值。

易错点总结

  • 次小值要求严格大于最小值:样例第二轮会把 [6,6,7] 的次小值错算为 7,答案从 13 变成 14。
  • 所有列都接 min1:样例会得到非法同列路径 1 + 4 + 7 = 12
  • 在同一个 dp 上原地转移:后面的列会读到当前行的新值,破坏“只依赖上一行”的状态定义。
  • 两小值没有按行重置,或初始化为 0:负数矩阵会读到不存在的前驱值。
  • 直接返回固定列:最优路径可以结束在最后一行任意列。
  • n = 1 时不应执行跨行转移,答案就是唯一格子。

相似题目

题目 难度 考察点
931. 下降路径最小和 中等 只能落到相邻三列,来源是常数个,不需要次小值优化
64. 最小路径和 中等 只能向右或向下,依赖两个方向,边界行列要单独初始化
120. 三角形最小路径和 中等 每行长度递增,自底向上推导可以省掉边界判断
174. 地下城游戏 困难 状态必须倒着定义为「所需初始值」,正向 DP 会因负值失效