leetcode 1289. 下降路径最小和 II
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了leetcode 1289. 下降路径最小和 II相关的知识,希望对你有一定的参考价值。
下降路径最小和 II 题解汇总
动态规划朴素解法
- 定含义: dp[i][j]表示当前(i,j)位置的最小下降路径和
- 找状态转移方程:
- 很显然,有上面的图可以得到状态转移方程:
for (int k = 0; k < r; k++)
{
if(j!=k)
dp[i][j] = min(dp[i - 1][k] + arr[i][j], dp[i][j]);
}
- 这里注意:需要把dp数组初始元素都设置为最大值
代码展示:
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& arr)
{
if (arr.empty()) return 0;
int r = arr.size();
vector<vector<int>> dp(r,vector<int>(r,INT_MAX));
for (int i = 0; i < r; i++)//第一行初始化
dp[0][i] = arr[0][i];
for (int i = 1; i < r; i++)
{
for (int j = 0; j < r; j++)
{
for (int k = 0; k < r; k++)
{
if(j!=k)
dp[i][j] = min(dp[i - 1][k] + arr[i][j], dp[i][j]);
}
}
}
//在最后一行中找出最小值
int Min = INT_MAX;
for (int i = 0; i < r; i++)
{
Min = min(dp[r - 1][i], Min);
}
return Min;
}
};
动态规划进阶解法
我们来分析一下上述解法有哪些可优化的点:
-
DP 状态转移部分,共有 n * n 个状态需要转移
-
每次转移的时候,枚举上一行的所有列
我们要确保所有的方案都枚举到,得到的才是全局最优解。
因此 DP 部分,我们是无法优化的。
那就只剩下枚举上一行的所有列这个部分可以优化了。
其实细想就可以发现,当我们在计算某行的状态值的时候,只会用到「上一行」的两个值:最小值和次小值。
- 举个🌰,当我们已经处理完第 i-1i−1 行的状态值。
- 假设第 i-1行状态中的最小值对应的列下标是 i1,次小值对应的列下标是 i2。
- 那么当我们处理第 i 行时,显然有:
- 处理第i 行中列下标为 i1的状态值时,由于不能选择「正上方」的数字,用到的是次小值。转移方程为:
f[i][j]=f[i-1][i2]+arr[i][j]
- 处理第 i行其他列下标的状态值时,这时候用到的是最小值。转移方程为:
f[i][j]=f[i-1][i1]+arr[i][j]
因此我们可以使用 i1 保存上一行的最小值对应的列下标,用 i2 保存次小值对应的列下标。
而无需每次转移都枚举上一行的所有列。
代码如下:
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& arr)
{
if (arr.empty()) return 0;
int r = arr.size();
vector<vector<int>> dp(r,vector<int>(r,INT_MAX));
int i1=-1, i2=-1;//最小值和次小值
//给dp数组第一行进行初始化
for (int i = 0; i < r; i++)
{
dp[0][i] = arr[0][i];
//找出当前行的最小值和次小值
if (arr[0][i]<(i1==-1?INT_MAX:dp[0][i1]))
{
i2 = i1;//更新i2
i1 = i;//更新i1
}
else if (arr[0][i] <(i2==-1?INT_MAX:dp[0][i2]))
{
i2 = i;
}
}
for (int i = 1; i < r; i++)
{
// 当前转移第 i 行,使用临时变量保存转移过程中的「最小值列下标」&「次小值列下标」
int ti1 = -1, ti2 = -1;
for (int j = 0; j < r; j++)
{
dp[i][j] = INT_MAX;
int val = arr[i][j];
//如果最小值与当前列不冲突,选择上一行最小值,否则选择上一行次小值
if (j != i1)
dp[i][j] = dp[i - 1][i1] + val;
else
dp[i][j] = dp[i - 1][i2] + val;
//寻找当前行的最小值和次小值下标
if (dp[i][j] < (ti1 == -1? INT_MAX : dp[i][ti1]))
{
ti2 = ti1;
ti1 = j;
}
else if (dp[i][j] < (ti2 == -1 ? INT_MAX : dp[i][ti2]))
{
ti2 = j;
}
}
// 使用临时变量更新 i1 和 i2
i1 = ti1; i2 = ti2;
}
int Min = INT_MAX;
for (int i = 0; i < r; i++)
{
Min = min(dp[r - 1][i], Min);
}
return Min;
}
};
动态规划终极优化版本
- 此外,我们还可以对空间复杂度进行优化。由于 f[i][j] 只会从 f[i - 1][i1] 或 f[i -1][i2] 转移而来,那么我们并不用将第 i - 1 行的所有状态存储下来,而是可以浓缩成三个变量:
- first_sum 表示这一行的最小值;
- first_pos 表示这一行最小值对应的下标;
- second_sum 表示这一行的次小值。
状态转移方程修改为:
f[i][j] = first_sum + arr[i][j] 其中 j != first_pos
f[i][j] = second_sum + arr[i][j] 其中 j == first_pos
- 通过这三个变量计算出第 i 行的所有状态之后,我们也不用将它们存储下来,同样可以浓缩成三个变量,为第 i + 1行的动态规划提供转移基础。由于在计算第 i + 1 行的状态时,不需要第 i - 1 行的任何信息,因此第 i - 1行浓缩成的三个变量此时可以被丢弃。这样以来,我们就将空间复杂度从 O(N^2) 降低至了 O(1)。
代码演示:
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& arr)
{
if (arr.empty()) return 0;
int r = arr.size();
int first_sum = 0;//当前行最小值---这里最小值和次小值一开始都要初始化为0,因为计算第一行时累加起点从0开始
int first_pos = -1;//当前行最小值对应的下标---是为了每一次判断上一行最小值与当前列是否发生冲突
int second_sum = 0;//当前行次小值
//计算每一行的最小值和次小值
for (int i = 0; i < r; i++)
{
//下面三个临时变量用来计算对应的三个值,然后赋值外面对应的三个
int fs = INT_MAX, fp = -1, ss = INT_MIN;//当前行最小值,最小值下标,当前行次小值
for (int j = 0; j < r; j++)
{
//计算当前行从第一列开始每一个位置元素的最小结果
//如果上一行最小值与当前列发生冲突,那么选上一行次小值加上去
int cur_sum = (first_pos != j ? first_sum : second_sum) + arr[i][j];
//计算当前行最小值,最小值下标,当前行次小值
if (cur_sum < fs)
{
ss = fs;
fs = cur_sum;
fp = j;
}
else if (cur_sum < ss)
{
ss = cur_sum;
}
}
//临时变量赋值给外面对应的三个
first_pos = fp;
first_sum = fs;
second_sum = ss;
}
//返回最后一行保存的最小值
return first_sum;
}
};
复杂度分析
-
时间复杂度:O(N^2)O(N 2 ),其中 NN 是方阵 arr 的边长。
-
空间复杂度:O(1)O(1)。
总结
这道题其实还可以用记忆化递归的方法求解,这里不作出展示了,留给读者自己思考
以上是关于leetcode 1289. 下降路径最小和 II的主要内容,如果未能解决你的问题,请参考以下文章