Code Ganker: LeetCode总结 -- 一维动态规划篇

2014年8月9日星期六

LeetCode总结 -- 一维动态规划篇

这篇文章的主题是动态规划, 主要介绍LeetCode中一维动态规划的题目, 列表如下:
Climbing Stairs
Decode Ways
Unique Binary Search Trees
Maximum Subarray
Maximum Product Subarray
Best Time to Buy and Sell Stock

在介绍上述具体题目之前, 我们先说说动态规划的通常思路。 动态规划是一种算法思路(注意这里不要和递归混淆, 事实上递归和迭代只是两种不同的实现方法, 并不是算法), 用一句话来总结就是, 动态规划是利用存储历史信息使得未来需要历史信息时不需要重新计 算, 从而达到降低时间复杂度, 用空间复杂度换取时间复杂度目的的方法。 我个人喜欢把动态规划分为以下几步:
1) 确定递推量。 这一步需要确定递推过程中要保留的历史信息数量和具体含义, 同时也会定下动态规划的维度;
2) 推导递推式。 根据确定的递推量, 得到如何利用存储的历史信息在有效时间(通常是常量或者线性时间)内得到当前的信息结果;
3) 计算初始条件。 有了递推式之后, 我们只需要计算初始条件, 就可以根据递推式得到我们想要的结果了。 通常初始条件都是比较简单的情况, 一般来说直接赋值即可;
4) (可选)考虑存储历史信息的空间维度。 这一步是基于对算法优化的考虑, 一般来说几维动态规划我们就用几维的存储空间是肯定可以实现的。 但是有时我们对于历史信息的要求不高, 比如这一步只需要用到上一步的历史信息, 而不需要更早的了, 那么我们可以只存储每一步的历史信息, 每步覆盖上一步的信息, 这样便可以少一维的存储空间, 从而优化算法的空间复杂度。
动态规划的时间复杂度是O((维度)×(每步获取当前值所用的时间复杂度))。 基本上按照上面的思路, 动态规划的题目都可以解决, 不过最难的一般是在确定递推量, 一个好的递推量可以使得动态规划的时间复杂度尽量低。

接下来我们来看看具体题目, 一维动态规划的题目主要分成两类:

(1) 第一种是比较简单的, 直接地按照上面步骤就可以解出来的, 确定递归量, 然后按递归式迭代就可以得到。 这种类型的题目是: Climbing StairsDecode WaysUnique 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)。

(2) 接下来我们介绍第二种类型, 虽然也是一维动态规划, 但是区别在于这类题目需要维护两个递推量, 所以思路上有一点技巧。 不过还是比较有通法的, 我通常把这种方法称为”局部最优和全局最优解法“。 这种方法中我们通常维护两个量, 一个是到目前为止最好的结果信息(全局最优), 另一个必须包含新加进来的元素的最好的结果信息(局部最优), 然后还是推导递推式, 计算初始条件, 跟动态规划的通常思路一样了。 Maximum SubarrayBest 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 StockMaximum Subarray是完全一样的, 也是维护两个量, 一个是到目前为止最好的交易(全局最优), 另一个是在当前一天卖出的最佳交易(局部最优), 其他步骤也是一样的, 这里就不列出来了。

可以看出, 上面五道一维动态规划的题目都是按照我前面列出的四个步骤进行求解的, 事实上所有动态规划题目都是按照这个基本思路来的。 掌握了套路之后就是看对具体问题要维护的递推量的选择了,这个个人感觉还是比较靠经验的, 熟能生巧。

没有评论:

发表评论