题目
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
例如,给定三角形:
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/triangle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
代码及分析
这道题目很容易想成用贪婪法进行求解。题目中所给的测试案例确实可以采用贪婪法得到,但再看看下面这种情况。
上述矩阵,如果采用贪婪法,则其最小路径为2 -> 3 -> 5 -> 4 ,最小路径和为14,
实际上,其最小路径应为 2 -> 3 -> 6 -> 1, 最小路径和为12。
由这个例子可以看出,贪婪法是行不通的,这是因为如果采用贪婪法进行求解,容易陷入局部最小值当中,而非全局最小值,可等对于整个矩阵的前部分层而言,得到了最小值,但继续向下走时,已经不再考虑的那些数值可能要远远小于当前所考虑局部区域的所有值。
方法一:动态规划法(空间复杂度O(1))
这道题可以用动态规划的思想去解决。我们不能保证那条路径上其和为最小值,那么我们就采用动态规划的方法计算所有可能路径的较小值。
在这里我们可以另外建立动态转移矩阵,而是直接用保存矩阵值的triangle来作为动态转移矩阵。当其作为动态转移矩阵存在时,其中的没一个元素triangle[i][j]表示的是从矩阵顶端到达该位置的最短路径之和,那么对于该矩阵的最后一行而言,我们将计算出从顶端到达底端所有位置的最小值,其中这些最小值中的最小值,便是我们所需求解的最小路径和。
再来看一下动态转移矩阵triangle的具体更新过程:
对于triangle[i][j],其职能从上方与它相邻的位置移动过来,即triangle[i-1][j]和triangle[i-1][j-1],那么到达给点的最短路径必定是从上一行中相邻两节点的较小值处移动过来,则有:
triangle[i][j] += min(triangle[i-1][j-1],triangle[i-1][j]);
需要注意的是,有两个边界问题需要考虑到,
(1)对于每一行的第一个节点,上一行与之相邻的节点只有triangle[i-1][0];
(2)对于每一行的最后一个节点,上一行与之相邻的节点只有triangle[i-1][j-1]。
int minimumTotal(vector<vector<int>>& triangle) {
if(triangle.empty())
return 0;
int rows = triangle.size();
for(int i = 1; i < rows; i++)
{
for(int j = 0; j < triangle[i].size(); j++)
{
if(j == 0)
triangle[i][0] += triangle[i-1][0];
else if(j == triangle[i].size()-1)
triangle[i][j] += triangle[i-1][j-1];
else
triangle[i][j] += min(triangle[i-1][j-1],triangle[i-1][j]);
}
}
sort(triangle[rows-1].begin(),triangle[rows-1].end());
return triangle[rows-1][0];
}
复杂度:
时间复杂度: O(n^2) ( ∑ i = 2 n ( ( n + 2 ) ( n − 1 ) 2 ) \sum\limits_{i=2}^n(\frac{(n+2)(n-1)}{2}) i=2∑n(2(n+2)(n−1)) )。
空间复杂度: O(1) 。
方法二:动态规划法(空间复杂度O(n^2))
上述方法一中,我们直接修改了给定的矩阵triangle,这样实际上在某些情况之下是不好的,我们也可以建立一个同等大小的矩阵dp用于动态转移。
int minimumTotal(vector<vector<int>>& triangle) {
if(triangle.empty())
return 0;
int rows = triangle.size();
vector<vector<int>> dp(rows,vector<int>(rows,0));
dp[0][0] = triangle[0][0];
for(int i = 1; i < rows; i++)
{
for(int j = 0; j <= i; j++)
{
if(j == 0)
dp[i][0] = triangle[i][0] + dp[i-1][0];
else if(j == triangle[i].size()-1)
dp[i][j] = triangle[i][j] + dp[i-1][j-1];
else
dp[i][j] += min(dp[i-1][j-1],dp[i-1][j])+triangle[i][j];
}
}
sort(dp[rows-1].begin(),dp[rows-1].end());
return dp[rows-1][0];
}
复杂度:
时间复杂度: O(n^2) ;
空间复杂度: O(n^2) 。
方法三:动态规划法(空间复杂度O(n))
方法二中,我们的动态转移矩阵dp只与上一层中的dp值有关,故我们可以只建立一维的dp矩阵进行动态转移。
若我们依旧采用自顶向下的顺序,则有:
int minimumTotal(vector<vector<int>>& triangle) {
if(triangle.empty())
return 0;
int rows = triangle.size();
vector<int> dp(rows,0);
dp[0] = triangle[0][0];
for(int i = 1; i < rows; i++)
{
for(int j = 0; j <= i; j++)
{
if(j == 0)
dp[0] = triangle[i][0] + dp[0];
else if(j == triangle[i].size()-1)
dp[j] = triangle[i][j] + dp[j-1];
else
dp[j] = min(dp[j-1],dp[j])+triangle[i][j];
cout << dp[j] <<" ";
}
cout << endl;
}
sort(dp.begin(),dp.end());
return dp[0];
}
对于题目中的测试案例,
其计算过程中dp矩阵的更新过程为:
5 9
11 14 21
15 15 23 26
可以看到第一次更新dp时,其第二个元素便是错误的,原因是因为,我们在更新dp[0]时,已经修改了dp[0]的值,那么在计算dp[1]时,将采用新的dp[0]去更新。
故为了只采用O(n)的空间,我们应该用自底向上的方法去求解。
int minimumTotal(vector<vector<int>>& triangle) {
if(triangle.empty())
return 0;
int rows = triangle.size();
vector<int> dp(triangle[rows-1]);
for(int i = rows-2; i >= 0; i--)
{
for(int j = 0; j <= i; j++)
{
dp[j] = min(dp[j+1],dp[j])+triangle[i][j];
}
}
return dp[0];
}
复杂度:
时间复杂度: O(n^2) ;
空间复杂度: O(n) 。