Skip to content

Instantly share code, notes, and snippets.

@jianminchen
Created February 14, 2018 18:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jianminchen/cfca7c18cfe6e0618214f33ee292c0d9 to your computer and use it in GitHub Desktop.
Save jianminchen/cfca7c18cfe6e0618214f33ee292c0d9 to your computer and use it in GitHub Desktop.
Leetcode dynamic programming - prepare study notes - Feb. 14, 2018
http://blog.csdn.net/linhuanmars/article/details/38468361
**动态规划的通常思路**
动态规划是一种算法思路(注意这里不要和递归混淆, 事实上递归和迭代只是两种不同的实现方法, 并不是算法).
用一句话来总结就是, 动态规划是利用存储历史信息使得未来需要历史信息时不需要重新计算,从而达到降低时间
复杂度, 用空间复杂度换取时间复杂度目的的方法。
动态规划分为以下几步:
1) 确定递推量。 这一步需要确定递推过程中要保留的历史信息数量和具体含义, 同时也会定下动态规划的维度;
2) 推导递推式。 根据确定的递推量,得到如何利用存储的历史信息在有效时间(通常是常量或者线性时间)内
得到当前的信息结果;
3) 计算初始条件。有了递推式之后,我们只需要计算初始条件,就可以根据递推式得到我们想要的结果了。通常初始
条件都是比较简单的情况,一般来说直接赋值即可;
4) 可选)考虑存储历史信息的空间维度。这一步是基于对算法优化的考虑,一般来说几维动态规划我们就用几维的
存储空间是肯定可以实现的。但是有时我们对于历史信息的要求不高,比如这一步只需要用到上一步的历史信息,而不
需要更早的了,那么我们可以只存储每一步的历史信息,每步覆盖上一步的信息,这样便可以少一维的存储空间,从而
优化算法的空间复杂度。
动态规划的时间复杂度是O((维度)×(每步获取当前值所用的时间复杂度))。基本上按照上面的思路,动态规划的
题目都可以解决,不过最难的一般是在确定递推量,一个好的递推量可以使得动态规划的时间复杂度尽量低。
接下来我们来看看具体题目,一维动态规划的题目主要分成两类:
**简单的**
第一种是比较简单的,直接地按照上面步骤就可以解出来的,确定递归量,然后按递归式迭代就可以得到。这种
类型的题目是: Climbing Stairs,Decode Ways 和 Unique Binary Search Trees。
Climbing Stairs中递推量很清晰,就是爬到 i 级楼梯有多少种可行爬法。而对于递推式我们可以看出,要到达i级楼梯,
必须通过 i-1 级或者 i-2 级(以为只能爬一级或者两级),如此可以得到到达 i 级楼梯的方式有 f(i)= f(i-1) + f(i-2)
种,这样递推式也就出来了。而初始条件则是一级楼梯是一种解法,两级楼梯是两种解法( 2 或者 11 )。有了这些接
下来递推到 n 级楼梯返回即可,空间复杂度是 O(n) (一维动态规划乘以每一步的常量操作)。空间上我们发现每一步,
只需要前两步的历史信息,所以我们不需要存储所有历史信息,只需要保存前两步,然后迭代替换就可以了,所以空间复杂
度是 O(2)= O(1), 这里对应于上面的第四步。
Decode Ways 中递推量也是类似的到达第 i 个字符可解析方式的数量,递推式比 Climbing Stairs 稍微复杂一些,要
分情况讨论,主要对于自己和前面一位组成数字的不同要分别处理一下,这里就不列出来的,大家可以看看Decode Ways --
LeetCode。虽然是分情况,不过每种情况也是可以常量时间更新信息的,初始条件依然是非常简单的case,空间上也是只需
要保存前两步的信息,所以时间和空间复杂度跟 Climbing Stairs 都是一样的。
Unique Binary Search Trees 思路还是类似的,递推式是稍有不同,按左右子树划分然后进行累加,最后归结为卡特兰数
的模型。这个问题仍然是一维动态规划,但是求解单步信息时是一个线性操作,所以时间复杂度是 O(n^2)。而空间上因为
每一步都需要前面每一步的所有信息,所以也无法优化,是O(n)。
**局部最优和全局最优解法**
局部最优是一定要包含当前元素, 全局最优就是当前的局部最优或者还是原来的全局最优.
接下来我们介绍第二种类型, 虽然也是一维动态规划,但是区别在于这类题目需要维护两个递推量,所以思路上有
一点技巧。不过还是比较有通法的,通常把这种方法称为 "局部最优和全局最优解法"。
这种方法中我们通常维护两个量,
一个是到目前为止最好的结果信息(全局最优),另一个必须包含新加进来的元素的最好的结果信息(局部最优),然后还是
推导递推式,计算初始条件,跟动态规划的通常思路一样了。Maximum Subarray 和 Best Time to Buy and Sell Stock
就是这种类型的题目。
Maximum Subarray 中对于递推量我们维护两个,一个是到目前为止最好的子数组,而另一个量则是加入当前元素之后,包含
当前元素的最好的子数组,最终我们是看全局最优的变量的最优值,而局部最优却是我们在递推过程中维护全局最优所需要的。
递推式还是有点技巧,第 i + 1 步表达式如下:
local[i + 1] = Math.max(A[i], local[i] + A[i]),就是局部最优是一定要包含当前元素,所以不然就是上 一步的
局部最优 local[i]+ 当前元素 A[i](因为 local[i] 一定包含第 i 个元素,所以不违反条件),但是如果 local[i] 是负的,
那么加上他就不如不需要的,所以不然就是直接用A[i];
global[i + 1] = Math(local[i + 1], global[i]),有了当前一步的局部最优,那么全局最优就是当前的局部最优或者
还是原来的全局最优.
所有情况都会被涵盖进来,因为最优的解如果不包含当前元素,那么前面会被维护在全局最优里面,如果包含当前元素,那么
就是这个局部最优。
初始条件都是 0 或者第一个元素既可以了,一遍扫过来,每次迭代更新两个量(都是常量时间),所以时间是O(n)。空间上可以
看出只需要上一步的信息,所以只需要保存上一步的全局最优和局部最优即可,复杂度是O(2) = O(1)。
Maximum Product Subarray 的题目模型跟 Maximum Subarray比较类似,只是把加法改成了乘法,思路还是用这个方法,只是
注意这里两个负数相乘可能得到更优的乘法结果,所以我们在维护局部最优时把局部的最小值也存下来,这样遇到负数时就可以
得到也许更大的乘积。其他就跟 Maximum Subarray 是一致的了。
Best Time to Buy and Sell Stock 跟 Maximum Subarray 是完全一样的,也是维护两个量,一个是到目前为止最好的交易
(全局最优),另一个是在当前一天卖出的最佳交易(局部最优),其他步骤也是一样的, 这里就不列出来了。
可以看出,上面五道一维动态规划的题目都是按照我前面列出的四个步骤进行求解的,事实上所有动态规划题目都是按照这个基本
思路来的。掌握了套路之后就是看对具体问题要维护的递推量的选择了,这个个人感觉还是比较靠经验的,熟能生巧。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment