376. 摆动序列
解题思路:
-
1. 动态规划 , 定义 up[i] 表示下标 i 的元素为 结尾 的【 最长上升摆动序列 】的 长度 , down[i] 表示下标 i 的元素为 结尾 的【 最长下降摆动序列 】的 长度 。
-
初始时,up[0] = down[0] = 1,然后从下标 1 开始遍历数组,两两比较元素:
-
① 如果出现 上升 , up[i] 等于前一个 down[i - 1] + 1 , down[i] 保持跟前一个相同,
-
② 如果出现 下降 , down[i] 等于前一个 up[i - 1] + 1 , up[i] 保持跟前一个相同,
-
③ 如果出现 相等 , up[i] 和 down[i] 都保持跟前一个相同。
-
最后返回 max(up[N-1], down[N-1]) 。
上面的动态规划代码还可以进行空间状态压缩,只使用两个变量来代替两个数组:
解题思路:
72. 编辑距离
解题思路:
-
动态规划 ,定义 dp[i][j] 表示 word1 前 i 个字符转换成 word2 前 j 个字符花的 最少操作数 , 最终返回 dp[M][N] 的值。
-
首先初始化 第一行 和 第一列 的 dp 值为对应 下标值 ,表示由 空字符串 修改成 另一个字符串 ( 或者反过来 ) 需要的步数 , 然后从 [1,1] 开始遍历, i 遍历 [1, M] , j 遍历 [1, N] , 求剩下的 dp 值,
-
① 如果 word1[i - 1] 和 word2[j - 1] 相等,那么 dp[i][j] 可由 左上角dp[i - 1][j - 1] 的值导出,
-
② 如果 word1[i - 1] 和 word2[j - 1] 不等,那么 dp[i][j] 由 左边列 、 上边行 以及 左上角 三个方向的最小值导出,即 min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1 , (加 1 表示修改的这一步数)
-
特判:如果有一个字符串长度是 0 ,就直接返回 另一个字符串长度 即可。
其实这个题的递推公式总结一下就是两种情况:
- 字符相等时,dp 值可由左上角的 dp 值推导出
- 字符不等时,dp 值由左、上、左上三个方向的最小 dp 值 + 1 推导出
注意,
上面代码中,dp的下标含义是表示的
个数
,因此写
代码需要注意三点:
-
1)定义dp数组的长度时要在 M 和 N 的基础上多 加1个 ,
-
2)循环遍历的时候,遍历 下标 的区间 结束 值都是取到 M 和 N 的 闭区间 ,
-
3)在遍历dp表求值的时候,拿dp的下标到原始字符串中取字符值的时候,是用dp的下标 减1 才行。
本题属于典型的【
样本对应模型
】 或者说 【
行列对应模型
】
。
96. 不同的二叉搜索树
解题思路:
-
dp + 卡特兰数应用 : F(0) = 1, F(1) = 1,F(n) = F(0) * F(n - 1) + F(1) * F(n - 2) + ... + F(n - 2) * F(1) + F(n - 1) * F(0)
-
定义 dp[i] 代表 i 个节点组成的互不相同的BST 数量 , 最后返回 dp[n] 即可, 初始化 dp[0] = 1 ( 0 个节点只能形成一颗 空树 ), dp[1] = 1 ( 1 个节点只能形成一颗 BST),
-
然后遍历 [2, n] , 计算 dp[i] 的值,当选定 i 做根之后, 左子树 可选节点数的范围就是 [0, i-1] , 右子树 可选 节点数的 范围就是 [i-1, 0] ,因此 dp[i] 的值就是 每次从左右子树的可选节点数范围中各取一种情况,将二者 相乘 并累加到 dp[i] 中, 即 dp[i]=∑ dp[j] * dp[i - j - 1] ,其中 j 的取值范围是 [0, i-1] 。
例如,左子树选择 0 个节点时,右子树可以选择 i - 1 个节点,此时的答案就是【左子树选择 0 个节点组成的不同BST数量】x【右子树选择 i - 1 个节点组成的不同BST数量】,把每一种选择情况的答案累加起来就是 dp[i] 的答案,因此也就是卡特兰数公式的应用。
152. 乘积最大子数组
解题思路:
-
动态规划 + 最大/最小前缀乘积规律总结 ,定义 maxP[i] 表示以下标 i 的元素为 结尾 的【 连续子数组的最大乘积 】, minP[i] 表示以下标 i 的元素为 结尾 的【 连续子数组的最小乘积 】。
-
之所以还要定义一个 minP 数组,是因为数组值 有正有负 ,所以连续子数组的最大乘积,不只和之前的最大乘积有关,还可能和之前的最小乘积有关,因为一个最小的负数再乘以一个负数就可能变成最大的正数,
-
所以 maxP[i] 跟三个数有关系: max ( 上一次的最大乘积*当前数 , 当前数 , 上一次的最小乘积*当前数 ) maxP[i] 会在这三者之中产生,
-
同理 minP[i] 为 min ( 上一次的最小乘积*当前数 , 当前数 , 上一次的最大乘积*当前数 ),因为一个最大的正数再乘以一个负数也有可能变成最小的负数。
-
初始 maxP[0]=minP[0]=nums[0] , 然后遍历数组,从第 2 个元素开始每次按照上面规则更新 maxP[i] 和 minP[i] , 并记录每次 maxP[i] 的 最大值 作为最终返回的答案。
这个题的解法很自然,但是也很精妙,一般也很难想的到。
312. 戳气球
解题思路:
-
1. DFS + 记忆优化搜索 ,首先创建一个长度 N+2 的数组,左右各补一个 1 ,方便处理边界问题,然后把原数组拷贝到新数组的 [1, N] 区间内。
-
递归函数返回 (L, R) 开区间内得到的最大硬币数,初始调用为 (0, N + 1) ,也就是求 [1, N] 范围的。
-
假设往 (L, R) 空区间 内 添加一个气球 ,如果戳爆这个气球,会得到一个临时积分: A[L] * A[i] * A[R] ,因此遍历区间 [L + 1, R - 1] 内的每一个 i , 将区间分成两部分 (L, i) 和 (i, R) ,对这两部分分别进行DFS,即在剩余的两部分区间内继续选择 添加 气球,将两部分递归调用返回的结果再加上前面的临时积分就是 (L, R) 整个区间内的总积分。
-
每次遍历计算得到积分取最大,最后递归函数返回这个最大积分即可。
-
递归终止: L + 1 >= R 时,返回 0 ,因为此时已经超出了 [L + 1, R - 1] 区间范围。
解题思路:
-
2. 动态规划 ,状态定义: dp[i][j] 表示戳破开区间 (i, j) 之间的气球之后能得到的最大值(不包括 i 与 j ),
-
状态转移方程: dp[i][j] = max{ dp[i][j], dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j] } ,其中 i < k < j ,
-
上面方程中的 nums[i]*nums[k]*nums[j] 表示戳破气球 k 时能得到的金币数,这里 k 表示区间 (i, j) 中 最后一个 戳破的气球,由于 k 是最后一个戳破的,那么在之前,已经戳破了 (i, k) 和 (k, j) 这两个开区间的气球,分别获得 dp[i][k] 和 dp[k][j] 金币,最后只剩下 k ,戳爆它就得到了 nums[i]*nums[k]*nums[j] ,三者相加就是 dp[i][j] 。
-
具体代码,先创建一个长度 N+2 的数组,左右各补一个 1 ,方便处理边界问题,然后把原数组拷贝到新数组的 [1, N] 区间内。
-
同样 dp 数组的长度也是 N+2 ,因为最后要返回的是 dp[0][N+1] , 戳爆开区间 (0, N+1) 也就是闭区间 [1, N] 上的气球得到的答案。
-
在计算 dp[i][j] 的值时,dp计算过程中会使用到下标比 i 大的 行 的dp值,因此要 从下往上 遍历,先计算下面的行,再计算上面的行,为了枚举所有满足 i < k < j 关系的 i 、 k 、 j ,使用 3层for 循环,最 外层的 i 控制左边界,枚举区间 [N - 1, 0] ,中间的 k 枚举区间 [i + 1, N] , 最内层的 j 控制右边界, 枚举区间 [k + 1, N + 1] , dp[i][j] 每次取当前得分和之前的最大值。
198. 打家劫舍
解题思路:
-
1. DFS + 记忆化搜索 ,递归函数中传递当前的偷窃的下标 i , 初始 i 从 0 开始,按照【 偷当前的 i 】和【 不偷当前的 i 】两种情况进行递归调用,
-
如果选择 偷当前的 i , 则获得当前 i 上的金额,递归调用时,只能偷 i + 2 号房屋,
-
如果选择 不偷当前的 i , 则递归调用时,可以选择偷 i + 1 号房屋,
-
将以上两种选择获得的金额 最大值 作为当前递归函数的返回值。
-
递归终止: i >= N , 越界返回 0 。
或者也可以让 i 从最后一个位置开始, 往前遍历,如果选择偷最后一个位置,则上一次只能偷 i - 2 位置, 否则如果选择不偷最后一个位置,则上一次可以是 i - 1, 递归终止条件变成:i==0 时,返回 nums[0], i < 0 时,返回 0。
解题思路:
-
2. 动态规划 ,定义 dp[i] :表示偷盗 [0, i] 区间房子得到的最大金额,则初始化 dp[0] = nums[0] , dp[1] = max(nums[0], nums[1]) ,然后 i 从 2 开始遍历数组,
-
对于 dp[i] 的值,可能有两种情况,要么上次偷了 i - 1 位置,那么这次就不能再偷当前的 i 位置,要么 上次 偷了 i - 2 位置,那么 这次 可以偷当前 i 位置,
-
如果是前者,则 dp[i] 的值保持和 dp[i - 1] 一致,如果是后者,则 dp[i] 的值为 dp[i - 2] + nums[i] , 所以 dp[i] 可以取两种情况的最大值 max(dp[i - 1], dp[i - 2] + nums[i]) 。
-
最终返回 dp[N - 1] 。
以上代码,可以使用两个滚动变量进行空间状态压缩,如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7eba92d19f8d3490ec75d605cd77312b.png)
213. 打家劫舍 II
解题思路:
-
动态规划 , 可以考虑【 不偷第一个房间,偷剩余房间 】的最大值 和 【 不偷最后一个房间,偷剩余房间 】的最大值;二者取 最大 。
-
其中, 偷剩余房间的最大值 求法跟198.题完全一样,只不过遍历的区间范围修改下就行, max(rob(nums, 1, N-1), rob(nums, 0, N-2))
因此这个题其实就是在198题基础上指定区间调用两次而已。
![](https://i-blog.csdnimg.cn/blog_migrate/deefbff8b5f9a173f023f10a3ad52ace.png)