今天是我们讲解「动态规划专题」中的 路径问题 的第六天。
我在文章结尾处列举了我所整理的关于 路径问题 的相关题目。
路径问题 我会按照编排好的顺序进行讲解(一天一道)。
你也先可以尝试做做,也欢迎你向我留言补充,你觉得与路径相关的 DP 类型题目 ~
这是 LeetCode 上的「1289. 下降路径最小和 II」,难度为 Hard。
给你一个整数方阵 arr ,定义「非零偏移下降路径」为:从 arr 数组中的每一行选择一个数字,且按顺序选出来的数字中,相邻数字不在原数组的同一列。
请你返回非零偏移下降路径数字和的最小值。
示例 1:
输入:arr = [[1,2,3],[4,5,6],[7,8,9]]
输出:13
解释:
所有非零偏移下降路径包括:
[1,5,9], [1,5,7], [1,6,7], [1,6,8],
[2,4,8], [2,4,9], [2,6,7], [2,6,8],
[3,4,8], [3,4,9], [3,5,7], [3,5,9]
下降路径中数字和最小的是 [1,5,7] ,所以答案是 13 。
提示:
我们已经做过好几道「路径问题」了。
凭借我们的经验,一个直观的做法是定义 为到达位置 的最小路径和。
那么答案必然是所有的 中的最小值,i 的取值范围为 [0, n)。
代表最优路径的最后一个数可能取自最后一行的任意下标。
由于题目要求每一行的取数,不能与上一行的取数列下标相同。
也就是规定了我们为每行进行取数时不能取「正上方」的值。
因此我们在进行状态转移的时候,需要枚举上一行的所有列下标。
这样的做法复杂度是 ,题目范围为 ,因此计算量为 ,可以过。
代码:
class Solution {
public int minFallingPathSum(int[][] arr) {
int n = arr.length;
int[][] f = new int[n][n];
// 初始化首行的路径和
for (int i = 0; i < n; i++) f[0][i] = arr[0][i];
// 从第一行进行转移
for (int i = 1; i < n; i++) {
for (int j = 0; j < n; j++) {
f[i][j] = Integer.MAX_VALUE;
int val = arr[i][j];
// 枚举上一行的每个列下标
for (int p = 0; p < n; p++) {
// 只有列下标不同时,才能更新状态
if (j != p) {
f[i][j] = Math.min(f[i][j], f[i-1][p] + val);
}
}
}
}
// 在所有的 f[n - 1][i] 中取最小值
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
ans = Math.min(ans, f[n-1][i]);
}
return ans;
}
}
那么是否有比 更好的做法呢?
要知道我们上述的解法,当数据范围出到 就会超时了。
我们来分析一下上述解法有哪些可优化的点:
1. DP 状态转移部分,共有 个状态需要转移
2. 每次转移的时候,枚举上一行的所有列
我们要确保所有的方案都枚举到,得到的才是全局最优解。
因此 DP 部分,我们是无法优化的。
那就只剩下「枚举上一行的所有列」这个部分可以优化了。
其实细想就可以发现,当我们在计算某行的状态值的时候,只会用到「上一行」的两个值:「最小值」和「次小值」。
举个?,当我们已经处理完第 行的状态值。
假设第 行状态中的最小值对应的列下标是 ,次小值对应的列下标是 。
那么当我们处理第 行时,显然有:
因此我们可以使用 i1
保存上一行的最小值对应的列下标,用 i2
保存次小值对应的列下标。
而无需每次转移都枚举上一行的所有列。
代码:
class Solution {
int MAX = Integer.MAX_VALUE;
public int minFallingPathSum(int[][] arr) {
int n = arr.length;
int[][] f = new int[n][n];
// i1 代表最小值列下标,i2 代表次小值列下标
int i1 = -1, i2 = -1;
// 先转移第一行
for (int i = 0; i < n; i++) {
// 更新动规值
int val = arr[0][i];
f[0][i] = val;
// 更新 i1 和 i2
if (val < (i1 == -1 ? MAX : f[0][i1])) {
i2 = i1;
i1 = i;
} else if (val < (i2 == -1 ? MAX : f[0][i2])) {
i2 = i;
}
}
// 再转移剩余行
for (int i = 1; i < n; i++) {
// 当前转移第 i 行,使用临时变量保存转移过程中的「最小值列下标」&「次小值列下标」
int ti1 = -1, ti2 = -1;
for (int j = 0; j < n; j++) {
f[i][j] = MAX;
int val = arr[i][j];
// 更新动规值
// 可以选择「最小值」的列选择「最小值」
if (j != i1) {
f[i][j] = f[i - 1][i1] + val;
// 不能选择「最小值」的列选择「次小值」
} else {
f[i][j] = f[i - 1][i2] + val;
}
// 更新 ti1 和 ti2
if (f[i][j] < (ti1 == -1 ? MAX : f[i][ti1])) {
ti2 = ti1;
ti1 = j;
} else if (f[i][j] < (ti2 == -1 ? MAX : f[i][ti2])) {
ti2 = j;
}
}
// 使用临时变量更新 i1 和 i2
i1 = ti1; i2 = ti2;
}
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
ans = Math.min(ans, f[n-1][i]);
}
return ans;
}
}
62.不同路径(中等):路径问题第一讲
63.不同路径 II(中等):路径问题第二讲
64.最小路径和(中等):路径问题第三讲
120.三角形最小路径和(中等):路径问题第四讲
931.下降路径最小和(中等):路径问题第五讲
1289.下降路径最小和 II(困难):本篇
1575.统计所有可行路径(困难)
576.出界的路径数(中等)
1301.最大得分的路径数目(困难)
欢迎补充 ~
这是我们「刷穿 LeetCode」系列文章的第 No.1289
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。