【LeetCode刷题笔记】动态规划(一)

376. 摆动序列

解题思路:
  • 1. 动态规划 定义 up[i] 表示下标 i 的元素为 结尾 的【 最长上升摆动序列 】的 长度 down[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])

 

上面的动态规划代码还可以进行空间状态压缩,只使用两个变量来代替两个数组:

解题思路:
  • 2.  贪心 我们只需要统计该序列中「 」与「 」的 数量 即可 具体解法请参考【 贪心篇

72. 编辑距离

解题思路:
  • 动态规划 ,定义 dp[i][j] 表示  word1  前  个字符转换成  word2  j 个字符花的 最少操作数 最终返回 dp[M][N] 的值。
  • 首先初始化 第一行 第一列 dp 值为对应 下标值 ,表示由 空字符串 修改成 另一个字符串 或者反过来 需要的步数 然后从 [1,1] 开始遍历, 遍历 [1, M] 遍历 [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] 表示以下标  的元素为 结尾 的【 连续子数组的最小乘积 】。
  • 之所以还要定义一个 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) 之间的气球之后能得到的最大值(不包括  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 时能得到的金币数,这里  表示区间 (i, j) 最后一个 戳破的气球,由于  是最后一个戳破的,那么在之前,已经戳破了 (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计算过程中会使用到下标比  大的 的dp值,因此要 从下往上 遍历,先计算下面的行,再计算上面的行,为了枚举所有满足  i < k < j  关系的  i k j ,使用 3层for 循环,最 外层的 i 控制左边界,枚举区间 [N - 1, 0] ,中间的  枚举区间  [i + 1, N] 最内层的 j 控制右边界, 枚举区间 [k + 1, N + 1] dp[i][j] 每次取当前得分和之前的最大值。

198. 打家劫舍

解题思路:
  • 1.   DFS + 记忆化搜索 ,递归函数中传递当前的偷窃的下标 初始 i 0 开始,按照【 偷当前的  i 】和【 不偷当前的  i 】两种情况进行递归调用,
  • 如果选择 偷当前的  i , 则获得当前  上的金额,递归调用时,只能偷 i + 2 号房屋,
  • 如果选择 不偷当前的  i , 则递归调用时,可以选择偷 i + 1 号房屋,
  • 将以上两种选择获得的金额 最大值 作为当前递归函数的返回值。
  • 递归终止: i >= N , 越界返回 0

或者也可以让 最后一个位置开始,  往前遍历,如果选择偷最后一个位置,则上一次只能偷 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 位置,那么 这次 可以偷当前  位置,
  • 如果是前者,则 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]

 

以上代码,可以使用两个滚动变量进行空间状态压缩,如下:

213. 打家劫舍 II

解题思路:
  • 动态规划 , 可以考虑【 不偷第一个房间,偷剩余房间 】的最大值 和 【 不偷最后一个房间,偷剩余房间 】的最大值;二者取 最大
  • 其中, 偷剩余房间的最大值 求法跟198.题完全一样,只不过遍历的区间范围修改下就行, max(rob(nums, 1, N-1), rob(nums, 0, N-2))

因此这个题其实就是在198题基础上指定区间调用两次而已。

337. 打家劫舍 III

  • 33
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值