1. 题目描述
LeetCode 第 120 题是“三角形最小路径和”。题目描述为:给定一个三角形 triangle
,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
2. 模式识别
本题属于动态规划(Dynamic Programming)的典型问题。动态规划通常用于解决具有最优子结构和重叠子问题的问题。在本题中,从顶部到底部的最小路径和可以由子三角形的最小路径和推导出来,满足最优子结构;并且在计算过程中,会多次重复计算相同的子问题,满足重叠子问题的特性。
3. 考点分析
- 动态规划思想:理解如何将大问题分解为小问题,并通过保存子问题的解来避免重复计算。
- 状态转移方程:确定状态转移方程是解决动态规划问题的关键,本题需要找到从一个状态转移到下一个状态的规律。
- 空间优化:可以通过优化空间复杂度,减少不必要的内存开销。
4. 所有解法
- 递归法:从顶部开始,递归地计算所有可能的路径和,取最小值。时间复杂度为 O ( 2 n ) O(2^n) O(2n) ,空间复杂度为 O ( n ) O(n) O(n) ,其中 n n n 是三角形的行数。由于存在大量的重复计算,这种方法效率较低。
- 记忆化搜索:在递归的基础上,使用一个二维数组记录已经计算过的路径和,避免重复计算。时间复杂度和空间复杂度均为 O ( n 2 ) O(n^2) O(n2) 。
- 动态规划(二维数组):使用一个二维数组
dp
来保存中间结果,状态转移方程为dp[i][j] = triangle[i][j] + min(dp[i + 1][j], dp[i + 1][j + 1])
。时间复杂度和空间复杂度均为 O ( n 2 ) O(n^2) O(n2) 。 - 动态规划(一维数组):对二维数组进行优化,使用一个一维数组来保存中间结果,因为在计算
dp[i][j]
时,只需要用到dp[i + 1][j]
和dp[i + 1][j + 1]
。时间复杂度为 O ( n 2 ) O(n^2) O(n2) ,空间复杂度为 O ( n ) O(n) O(n) 。
5. 最优解法(动态规划,一维数组)的 C 语言代码
#include <stdio.h>
#include <stdlib.h>
// 函数用于计算三角形的最小路径和
// triangle: 指向二维数组的指针,表示三角形
// triangleSize: 三角形的行数
// triangleColSize: 指向一维数组的指针,记录每一行的列数
// 返回值: 三角形的最小路径和
int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {
// 创建一个一维数组 dp,用于保存中间结果
// 数组的大小为三角形的行数,因为我们只需要保存下一行的结果
int* dp = (int*)malloc(triangleSize * sizeof(int));
// 初始化 dp 数组,将最后一行的值赋给 dp 数组
for (int i = 0; i < triangleSize; i++) {
dp[i] = triangle[triangleSize - 1][i];
}
// 从倒数第二行开始向上遍历
for (int i = triangleSize - 2; i >= 0; i--) {
// 遍历当前行的每一个元素
for (int j = 0; j <= i; j++) {
// 状态转移方程:当前位置的最小路径和等于当前元素加上下一行相邻两个元素的较小值
dp[j] = triangle[i][j] + (dp[j] < dp[j + 1] ? dp[j] : dp[j + 1]);
}
}
// 最终结果保存在 dp[0] 中
int result = dp[0];
// 释放动态分配的内存
free(dp);
return result;
}
6. 复杂度分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n 是三角形的行数。需要遍历三角形的每一个元素。
- 空间复杂度: O ( n ) O(n) O(n) ,只使用了一个一维数组来保存中间结果。