文章目录
1. 题目
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 + 1 +1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
1.1 示例
-
示例 1 1 1:
- 输入:
triangle = [[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]]
- 输出: 11 11 11
- 解释: 如下面简图所示:
2 3 4 6 5 7 4 1 8 3
自顶向下的最小路径和为 11 11 11(即, 2 + 3 + 5 + 1 = 11 2 + 3 + 5 + 1 = 11 2+3+5+1=11)。
- 输入:
-
示例 2 2 2:
- 输入:
triangle = [[-10]]
- 输出: − 10 -10 −10
- 输入:
1.2 说明
- 来源: 力扣(LeetCode)
- 链接: https://leetcode-cn.com/problems/triangle
1.3 提示
1 <= triangle.length <= 200
triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
- − 1 0 4 < = triangle [ i ] [ j ] < = 1 0 4 -10^4 <= \text{triangle}[i][j] <= 10^4 −104<=triangle[i][j]<=104
1.4 进阶
- 你可以只使用 O ( n ) O(n) O(n) 的额外空间( n n n 为三角形的总行数)来解决这个问题吗?
- 你可以进一步给出得到最小和时的路径么?
2. 解法一(常规动态规划)
2.1 分析
2.1.1 定义状态
实际上,这题本身不难,只是首先要根据三角形用二维数组形式给出联想到 dp
数组的定义也应该是二维的,因此这里将dp
数组的元素(即状态) dp[i][j]
定义为到元素 triangle[i][j]
时最小的路径和。
2.1.2 状态转移
由此,再结合题目要求:每一步只能移动到下一行中相邻的结点上,可以得到状态转移方程为 dp[i][j] = min(dp[i - 1][j - 1] , dp[i - 1][j]) + triangle[i][j]
,然而还有一些特殊情况需要考虑:
- 当
j = 0
时,此时状态dp[i][j]
只可能由状态dp[i - 1][j]
转移而来; - 当
j = len(triangle[i] - 1)
时,此时状态dp[i][j]
只可能由状态dp[i - 1][j - 1]
转移而来。
2.2 解答
from typing import List
class Solution:
def minimum_sum(self, triangle: List[List[int]]) -> int:
if len(triangle) == 1:
return triangle[0][0]
dp = [[0 for _ in range(len(triangle[i]))] for i in range(len(triangle))]
dp[0][0] = triangle[0][0]
dp[1][0] = triangle[0][0] + triangle[1][0]
dp[1][1] = triangle[0][0] + triangle[1][1]
for i in range(1, len(triangle)):
for j in range(0, len(triangle[i])):
if j == 0:
dp[i][j] = dp[i - 1][j] + triangle[i][j]
elif j == len(triangle[i]) - 1:
dp[i][j] = dp[i - 1][j - 1] + triangle[i][j]
else:
dp[i][j] = min(dp[i - 1][j - 1] + triangle[i][j], dp[i - 1][j] + triangle[i][j])
return min(dp[-1])
def main():
triangle = [[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]]
sln = Solution()
print(sln.minimum_sum(triangle))
if __name__ == '__main__':
main()
- 执行用时: 44 ms , 在所有 Python3 提交中击败了 25.77% 的用户;
- 内存消耗: 15.9 MB , 在所有 Python3 提交中击败了 18.05% 的用户。
2.3 复杂度
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),即嵌套的两层
for
循环的循环次数和三角形的元素个数一样,其中 n n n 是三角形的行数。 - 空间复杂度: O ( n 2 ) O(n^2) O(n2)。我们需要一个和三角形元素数量相同的二维数组存放所有的状态。
实际上,如果实际中允许直接修改原数组的话,可以不用额外声明 dp
数组而直接复用 triangle
数组,此时空间复杂度可以直接降低至 O ( 1 ) O(1) O(1) :
from typing import List
class Solution:
def inplace_minimum_sum(self, triangle: List[List[int]]) -> int:
if len(triangle) == 1:
return triangle[0][0]
triangle[1][0] = triangle[0][0] + triangle[1][0]
triangle[1][1] = triangle[0][0] + triangle[1][1]
for i in range(2, len(triangle)):
for j in range(0, len(triangle[i])):
if j == 0:
triangle[i][j] = triangle[i - 1][j] + triangle[i][j]
elif j == len(triangle[i]) - 1:
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i][j]
else:
triangle[i][j] = min(triangle[i - 1][j - 1] + triangle[i][j], triangle[i - 1][j] + triangle[i][j])
return min(triangle[-1])
def main():
triangle = [[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]]
sln = Solution()
print(sln.inplace_minimum_sum(triangle))
if __name__ == '__main__':
main()
3. 解法二(由底到顶动态规划)
- 作者: stellari
3.1 分析
实际上,由于题目给出的三角形像一个树形结构,这会让很多人乍一看就想通过深度优先遍历的方式来解答。然而,仔细分析就可以发现相邻的节点总是拥有一个共同的子树,换句话说这就是所谓的重叠子问题;同时,假设 x
和 y
是 k
的子节点(或子树的根节点),这意味着如果可以确定从 x
和 y
到树状三角形的底部已知那么从 k
开始的最小路径就可以立马知道了,这实际就是最优子结构。从这个意义上来说使用动态规划是解决本体最好的方法。
除了像上述解法一样从三角形顶点开始求解之外,还可以从三角形最底层节点开始求解。对此,显而易见的是,从最底层节点开始的最小路径和就是这些节点本身的值。在更一般的情况下,从第 i
行的第 j
个节点 triangle[i][j]
开始的最小路径和是从其两个子节点开始的最小路径和中较小的那个加上该节点本身的值,即: dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]
。
实际上,仔细观察后可以发现:一旦整行 dp[i]
计算出来之后就用不到 dp[i + 1]
这一行了,因此上述转台转移方程可以从空间上简化为 dp[j] = min(dp[j], dp[j + 1] + triangle[i][j]
。
3.2 解答
根据上述分析,可以给出下列解答:
from typing import List
class Solution:
def bottom_up_minimum_sum(self, triangle: List[List[int]]) -> int:
dp = triangle[-1]
for i in range(len(triangle) - 2, -1, -1):
for j in range(len(triangle[i])):
dp[j] = triangle[i][j] + min(dp[j], dp[j + 1])
return dp[0]
def main():
triangle = [[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]]
sln = Solution()
print(sln.bottom_up_minimum_sum(triangle)) # 11
if __name__ == '__main__':
main()
3.3 复杂度
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),即嵌套的两层
for
循环的循环次数和三角形的元素个数一样,其中 n n n 是三角形的行数。 - 空间复杂度: O ( n ) O(n) O(n)。