1. 问题背景与核心挑战
LeetCode 1289题"下降路径最小和 II"是一个经典的动态规划问题变种。给定一个n x n的整数矩阵grid,我们需要找到一条从第一行到最后一行的下降路径,使得路径上的数字总和最小。这里的"下降路径"定义为:从第一行的某个元素开始,每次可以选择下一行与当前行不同列的任意元素。
这个问题与经典的"下降路径最小和"问题(LeetCode 120)的关键区别在于:原问题只允许移动到下一行相邻列的元素,而本题允许移动到下一行任意非同一列的元素。这个看似微小的变化实际上大大增加了问题的复杂度。
提示:理解题目约束条件中的"不同列"要求是解决本题的关键。这意味着我们不能简单地逐行选择最小值,因为相邻行的最小值可能在同一列。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法与复杂度分析
最直观的解法是使用深度优先搜索(DFS)尝试所有可能的路径。对于第一行的每个元素,我们有n种选择;对于第二行,由于不能与第一行同列,有n-1种选择;以此类推。这样总的时间复杂度是O(n×n!),显然无法处理较大的n值(比如n=100)。
java复制// 暴力DFS解法(仅用于理解问题,实际会超时)
public int minFallingPathSum(int[][] grid) {
int minSum = Integer.MAX_VALUE;
for (int j = 0; j < grid.length; j++) {
minSum = Math.min(minSum, dfs(grid, 0, j));
}
return minSum;
}
private int dfs(int[][] grid, int i, int j) {
if (i == grid.length - 1) return grid[i][j];
int min = Integer.MAX_VALUE;
for (int k = 0; k < grid.length; k++) {
if (k != j) {
min = Math.min(min, dfs(grid, i + 1, k));
}
}
return grid[i][j] + min;
}
3. 动态规划解法优化
3.1 基础DP思路
我们可以使用动态规划来优化解法。定义dp[i][j]表示从第i行第j列元素开始的下降路径的最小和。状态转移方程为:
dp[i][j] = grid[i][j] + min(dp[i+1][k]),其中k ≠ j
初始条件是dp[n-1][j] = grid[n-1][j](最后一行)。
这种解法的时间复杂度是O(n^3),因为对于每个dp[i][j],我们需要遍历下一行的所有非j列元素。
java复制// O(n^3) DP解法
public int minFallingPathSum(int[][] grid) {
int n = grid.length;
int[][] dp = new int[n][n];
// 初始化最后一行
for (int j = 0; j < n; j++) {
dp[n-1][j] = grid[n-1][j];
}
// 从倒数第二行开始向上计算
for (int i = n-2; i >= 0; i--) {
for (int j = 0; j < n; j++) {
int min = Integer.MAX_VALUE;
for (int k = 0; k < n; k++) {
if (k != j) {
min = Math.min(min, dp[i+1][k]);
}
}
dp[i][j] = grid[i][j] + min;
}
}
// 找出第一行的最小值
int result = Integer.MAX_VALUE;
for (int j = 0; j < n; j++) {
result = Math.min(result, dp[0][j]);
}
return result;
}
3.2 优化到O(n^2)
我们可以进一步优化到O(n^2)。关键观察是:对于每一行,我们只需要知道该行的最小值和次小值及其对应的列索引。这样,在计算dp[i][j]时:
- 如果当前列j不等于上一行最小值的列,则使用最小值
- 否则,使用次小值
这样,我们只需要在每行预处理最小值和次小值,就可以将内层循环的O(n)降到O(1)。
java复制// O(n^2)优化解法
public int minFallingPathSum(int[][] grid) {
int n = grid.length;
// 只需要保存前一行的dp值
int[] prevRow = new int[n];
System.arraycopy(grid[n-1], 0, prevRow, 0, n);
for (int i = n-2; i >= 0; i--) {
// 找出前一行的最小值和次小值及其列索引
int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
int min1Col = -1;
for (int j = 0; j < n; j++) {
if (prevRow[j] < min1) {
min2 = min1;
min1 = prevRow[j];
min1Col = j;
} else if (prevRow[j] < min2) {
min2 = prevRow[j];
}
}
// 计算当前行的dp值
int[] currRow = new int[n];
for (int j = 0; j < n; j++) {
if (j != min1Col) {
currRow[j] = grid[i][j] + min1;
} else {
currRow[j] = grid[i][j] + min2;
}
}
prevRow = currRow;
}
// 找出第一行的最小值
int result = Integer.MAX_VALUE;
for (int num : prevRow) {
result = Math.min(result, num);
}
return result;
}
4. 空间复杂度优化
上面的解法已经将空间复杂度优化到O(n),因为我们只保存前一行的dp值。如果允许修改原数组,我们还可以进一步优化到O(1)空间:
java复制// O(1)空间优化(修改原数组)
public int minFallingPathSum(int[][] grid) {
int n = grid.length;
for (int i = n-2; i >= 0; i--) {
// 找出下一行的最小值和次小值
int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
int min1Col = -1;
for (int j = 0; j < n; j++) {
if (grid[i+1][j] < min1) {
min2 = min1;
min1 = grid[i+1][j];
min1Col = j;
} else if (grid[i+1][j] < min2) {
min2 = grid[i+1][j];
}
}
// 更新当前行
for (int j = 0; j < n; j++) {
if (j != min1Col) {
grid[i][j] += min1;
} else {
grid[i][j] += min2;
}
}
}
// 找出第一行的最小值
int result = Integer.MAX_VALUE;
for (int num : grid[0]) {
result = Math.min(result, num);
}
return result;
}
5. 边界条件与测试用例
在实现过程中,需要考虑以下边界条件:
- 单行矩阵(n=1):直接返回该元素
- 两行矩阵(n=2):每行只能选择另一列
- 所有元素相同的情况
- 有多个相同最小值的情况
测试用例示例:
java复制// 测试用例1:常规情况
int[][] grid1 = {
{1,2,3},
{4,5,6},
{7,8,9}
};
// 预期输出:13 (1->5->7)
// 测试用例2:n=1
int[][] grid2 = {{5}};
// 预期输出:5
// 测试用例3:有多个相同最小值
int[][] grid3 = {
{1,1,1},
{5,3,1},
{4,1,1}
};
// 预期输出:3 (1->1->1)
6. 常见错误与调试技巧
-
列索引混淆:在优化解法中,容易混淆当前行和下一行的列索引。建议在变量命名时明确区分,如使用
currCol和nextCol。 -
初始化错误:忘记初始化最后一行,或者错误地从第一行开始计算。记住DP通常是从底向上计算的。
-
最小值更新逻辑错误:在寻找最小值和次小值时,更新顺序很重要。应该先更新次小值,再更新最小值。
-
空间优化时的覆盖问题:当进行O(1)空间优化时,确保在修改当前行之前已经完成了所有需要的计算。
调试技巧:
- 打印中间DP表格,验证每步计算是否正确
- 对于小规模测试用例(如2x2矩阵),手动计算验证
- 特别注意边界条件(如n=1)的处理
7. 算法扩展与变种
这个问题有几个有趣的变种:
-
允许k次同列下降:修改题目条件,允许最多k次移动到下一行的同一列。这需要增加一个状态维度来记录已经使用的同列下降次数。
-
最大下降路径和:类似问题,但求最大值而非最小值。解法思路相同,只需将min改为max。
-
带障碍物的下降路径:某些格子可能被标记为障碍物,不能经过。需要在状态转移时跳过这些格子。
-
多起点多终点:起点可以是第一行的任意元素,终点可以是最后一行的任意元素。解法类似,最终结果是所有起点结果的最小值。
8. 实际应用场景
虽然这是一个算法题,但其核心思想在实际中有广泛应用:
-
资源分配优化:如将任务分配给不同的工人,避免连续将重任务分配给同一工人。
-
路径规划:如机器人导航中,寻找能量消耗最小的路径,同时避免重复使用同一类型的移动方式。
-
投资组合优化:在不同时间段选择不同的投资产品,避免连续选择同一类高风险产品。
-
游戏AI:在棋盘类游戏中,寻找最优移动序列,避免被对手预测到模式。
理解这类动态规划问题的解法,有助于我们在面对类似的实际优化问题时快速找到解决方案。
