在LeetCode的动态规划题目中,「三角形最小路径和」是一道经典的入门级题目,它既考察了对动态规划核心思想的理解,也需要我们结合题目特性设计合理的状态转移方程。今天就来一步步拆解这道题,从题目分析到代码实现,再到思路优化,帮你彻底搞懂这道题的解题逻辑。
一、题目回顾:明确需求与约束
题目给出一个三角形 triangle,要求找出自顶向下的最小路径和,核心约束如下:
-
每一步只能移动到下一行中「相邻的结点」;
-
相邻结点定义:当前行下标为
i,下一步可移动到下一行的i或i+1下标; -
三角形的行数不固定,每一行的元素个数等于当前行的行号(从0开始计数的话,第0行1个元素,第1行2个元素,以此类推)。
举个简单示例:若三角形为 [[2],[3,4],[6,5,7],[4,1,8,3]],自顶向下的最小路径和为 2→3→5→1 = 11。
二、解题思路:为什么用动态规划?
这道题的核心是「求最小路径和」,而路径的选择具有「重叠子问题」和「最优子结构」,这正是动态规划的适用场景:
-
重叠子问题:到达第
i行第j个元素的最小路径和,依赖于到达第i-1行第j个和第j-1个元素的最小路径和(除了边界情况); -
最优子结构:到达第
i行第j个元素的最小路径和,是其上方两个可能路径的最小路径和加上当前元素的值,即「局部最优推导全局最优」。
因此,我们可以用动态规划的思路,通过构建一个DP数组,逐步计算每一步的最小路径和,最终得到整个三角形的最小路径和。
三、代码逐行解析:读懂每一步的意义
先给出完整的解题代码(题目要求的TypeScript版本),再逐行拆解其逻辑:
function minimumTotal(triangle: number[][]): number {
const Len = triangle.length;
const dp: number[][] = new Array(Len);
for (let i = 0; i < Len; i++) {
dp[i] = new Array(i + 1).fill(0);
}
dp[0][0] = triangle[0][0];
for (let i = 1; i < Len; i++) {
for (let j = 0; j <= i; j++) {
if (j === 0) {
dp[i][j] = dp[i - 1][j] + triangle[i][j];
} else if (j === i) {
dp[i][j] = dp[i - 1][j - 1] + triangle[i][j];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle[i][j];
}
}
}
return Math.min(...dp[Len - 1]);
};
1. 初始化DP数组
首先获取三角形的行数 Len,然后创建一个二维DP数组 dp,其中 dp[i][j] 表示「到达第 i 行第 j 个元素的最小路径和」。
由于第 i 行有 i+1 个元素(行号从0开始),所以我们通过循环给每一行的DP数组初始化,长度为 i+1,初始值填充为0。
2. 边界条件:三角形顶部
三角形的顶部只有一个元素(第0行第0列),到达这个元素的路径只有一条,所以 dp[0][0] = triangle[0][0],这是整个DP数组的起始值。
3. 状态转移:填充DP数组
从第1行开始(因为第0行已经初始化),逐行逐列填充DP数组,分三种情况讨论:
-
当
j === 0(当前列是当前行的第一列):只能从上层的第一列(j=0)移动过来,所以dp[i][j] = dp[i-1][j] + triangle[i][j]; -
当
j === i(当前列是当前行的最后一列):只能从上层的最后一列(j-1)移动过来,所以dp[i][j] = dp[i-1][j-1] + triangle[i][j]; -
其他情况(中间列):可以从上层的
j列或j-1列移动过来,取两者的最小值,再加上当前元素的值,即dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-1]) + triangle[i][j]。
4. 最终结果:取最后一行的最小值
DP数组填充完成后,最后一行的每个元素 dp[Len-1][j],分别表示到达三角形底部每一个元素的最小路径和。我们只需要取这一行的最小值,就是整个三角形的最小路径和,用 Math.min(...dp[Len-1]) 即可实现。
四、代码测试:验证逻辑正确性
用前面提到的示例 triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] 测试代码:
-
初始化DP数组为
[[0], [0,0], [0,0,0], [0,0,0,0]]; -
设置
dp[0][0] = 2; -
第1行(i=1):j=0 → dp[1][0] = 2+3=5;j=1 → dp[1][1] = 2+4=6;
-
第2行(i=2):j=0 → 5+6=11;j=1 → min(5,6)+5=10;j=2 → 6+7=13;
-
第3行(i=3):j=0 → 11+4=15;j=1 → min(11,10)+1=11;j=2 → min(10,13)+8=18;j=3 →13+3=16;
-
最后一行最小值为 11,与预期结果一致,代码正确。
五、优化方向:空间复杂度优化
上面的代码空间复杂度是 O(n²)(n为三角形行数),因为我们创建了一个和三角形大小一致的DP数组。但实际上,我们可以优化到 O(n),核心思路是「滚动数组」:
观察状态转移方程,我们发现第 i行的DP值,只依赖于第 i-1 行的DP值,因此不需要保存整个二维数组,只需要一个一维数组,不断覆盖更新即可。
优化后的代码(TypeScript):
function minimumTotal(triangle: number[][]): number {
const Len = triangle.length;
let dp: number[] = new Array(Len).fill(0);
dp[0] = triangle[0][0];
for (let i = 1; i < Len; i++) {
// 从后往前更新,避免覆盖上一行未使用的值
for (let j = i; j >= 0; j--) {
if (j === 0) {
dp[j] = dp[j] + triangle[i][j];
} else if (j === i) {
dp[j] = dp[j - 1] + triangle[i][j];
} else {
dp[j] = Math.min(dp[j], dp[j - 1]) + triangle[i][j];
}
}
}
return Math.min(...dp);
};
注意:优化后需要「从后往前」更新一维数组,因为如果从前往后更新,会覆盖上一行的 dp[j-1],导致后续计算出错。
六、总结:解题关键与易错点
解题关键
-
明确DP数组的定义:
dp[i][j]表示到达第i行第j个元素的最小路径和; -
区分边界情况(第一列和最后一列)和中间情况,正确写出状态转移方程;
-
最终结果是最后一行的最小值。
易错点
-
忽略边界条件:忘记第一列只能从上层第一列过来,最后一列只能从上层最后一列过来;
-
DP数组初始化错误:第
i行的长度应该是i+1,而非固定长度; -
空间优化时,从前往后更新一维数组,导致数据覆盖出错。
这道题虽然简单,但却是动态规划思想的典型应用,掌握它的解题逻辑,能为后续解决更复杂的DP问题打下基础。