week 3
一、先搞懂问题本身
就是给一个 n 行的数字三角形,从顶上开始走,每步只能往左下或右下走,要找到一条路,让路上的数字加起来最大,最后输出这个最大的和就行。输入很简单,先给 n,再给 n 行数字,数字都在 0 到 99 之间。
二、动态规划一步步拆问题
1.1 先明确 “状态” 和递推关系(核心!)
其实动态规划没那么玄乎,先搞清楚 “dp [i][j]” 到底代表啥 —— 它就是从三角形最顶上,走到第 i 行第 j 列那个位置时,能拿到的最大数字和(行和列都从 0 开始数,比如第 0 行就 1 个数,第 1 行 2 个数,第 i 行有 i+1 个数)。
然后找递推关系:走到第 i 行第 j 列,只能从它上面两个位置来 —— 要么是第 i-1 行第 j-1 列(左上方),要么是第 i-1 行第 j 列(正上方)。那肯定选这两个里面和更大的那个,再加上当前位置的数字,就是 dp [i][j] 的值,所以递推式就是:dp [i][j] = (dp [i-1][j-1] 和 dp [i-1][j] 里的最大值) + 三角形 [i][j]
边界条件也很简单:最顶上那个位置(第 0 行第 0 列),没有前一个位置,所以 dp [0][0] 就等于三角形最顶上的那个数字。
1.2 填表怎么填才不乱
表的维度:用一个二维数组 dp 就行,大小是 n×n(其实只有上三角能用,比如第 i 行只用到前 i+1 个位置,其他位置不用管)。
填表范围:从第 1 行开始填(第 0 行已经确定了),一直填到第 n-1 行(最后一行),每行从第 0 列填到第 i 列(当前行的最后一列)。
填表顺序:两种简单的方式
从上往下填:先填第 0 行,再填第 1 行、第 2 行…… 最后填到第 n-1 行,每行从左到右填。
从下往上填:更省事!先把最后一行的 dp 值都设成和三角形最后一行一样(因为走到最后一行就没下文了),然后从倒数第二行开始,往上一行一行填,每行还是从左到右。
最优值在哪:
从上往下填的话,最后一行所有 dp 值里最大的那个就是答案。
从下往上填的话,最后 dp [0][0] 就是答案(相当于把所有路径的最大和都汇总到顶上了)。
1.3 复杂度分析(不用死记,理解就行)
时间复杂度:O (n²)。因为总共要填 n (n+1)/2 个位置,每个位置就做一次比较加一次加法,花不了多少时间,整体就是 n² 级别的,比暴力搜快多了(暴力是 2ⁿ,n=100 的话根本跑不出来)。
空间复杂度:
普通版:O (n²),就是用个二维数组存所有 dp 值。
优化版:O (n)。其实填的时候只需要上一行的信息,比如从上往下填,填第 i 行只需要第 i-1 行的 dp 值,所以用一个一维数组反复覆盖就行,能省不少内存。
三、自己学动态规划的真实感受
刚开始觉得动态规划好难,光 “状态定义” 就绕不明白,后来发现核心就两点:一是搞清楚 dp [i][j] 到底代表啥(一定要和问题目标挂钩,比如这里就是 “到当前位置的最大和”),二是找到递推关系(怎么从之前的状态得到现在的状态)。
数字三角形这个题特别适合入门,因为它的最优子结构很明显 —— 当前位置的最优解,全靠前面两个位置的最优解。而且能直观感受到动态规划的优势:如果用暴力搜索,每个岔路都试一次,n=100 的话根本不可能算完,但动态规划把每个子问题的结果存下来,后面直接用,效率一下子就上去了。
还有个小技巧:从下往上填比从上往下填更方便,不用处理边界的特殊情况(比如第 i 行第 0 列,只能从上面一行第 0 列来,从上往下填要判断 j=0 的情况,从下往上就不用)。空间优化也很实用,虽然 n=100 时二维数组也够用,但学会一维滚动数组,后面遇到更大的问题也能应对。
浙公网安备 33010602011771号