跳过正文
  1. 算法/

动态规划、分治、贪心算法对比

·93 字·1 分钟
目录

动态规划(Dynamic Programming)与分治方法相似,都是通过组合子问题的解来求解原问题。分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。在这种情况下,分治算法会做许多不必要的工作,它会反复地求解那些公共子子问题。而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了不必要的重复工作。
——《算法导论》

从动态规划的名字(Dynamic Programming,此处 Programming 指的是表格记录,而非编程)就可看出,表格记录是动态规划的一个关键特征,这也是其与分治法的一个区别。

在分治法中,当父问题被分解为几个子问题后,尽管子问题又被分解为子子问题,但父问题的解仅由子问题的解计算出来,不直接依赖与子子问题。因而我们在得到下一层(子问题)的解后,就不保存下下一层(子子问题)的解了,即用不到表格记录。而在动态规划问题中,比如背包问题,当我们计算容量为 j 的背包时,可能会用到容量为 j - weight[i] 的背包的解,相比于分治的层层分离,这便是动态规划问题的重叠所在。

而且,动态规划是一种通过递归思想自上向下分析问题,而通过循环自下而上解决问题的方法。因为问题的依赖性是上向下依赖,只有自下而上,才有可能实现有效的记忆化。

不同算法应对的场景:

  • 子问题最优则原始问题最优——贪心算法或者动态规划算法。
  • 子问题最优则原始问题最优,且子问题互相独立——分治算法。
  • 子问题最优不能推导出原始问题最优——暴力搜索等。

狭义动态规划 与 广义动态规划
#

  • 狭义的动态规划是指使用 dp table,以递推的方式来求解。
  • 广义的动态规划可以是备忘录递归,也可以是 dp table 递推。

记忆化搜索(递归) vs 狭义动态规划(递推)
#

记忆化搜索是通过记录已经遍历过的状态的信息,从而避免对同一状态重复遍历的搜索实现方式。

因为记忆化搜索确保了每个状态只访问一次,所以它也属于(广义)动态规划。

记忆化搜索与递推的代码,在形式上是高度类似的。这是由于他们使用了相同的状态表示方式和状态转移方程。也正因如此,二者的时间复杂度一般是差不多的(由于需要维护递归栈,记忆化搜索的时间开销会大一些),但是由于使用了递归,其空间复杂度要高得多。

记忆化搜索和递推都确保了同一状态至多只被求解一次,而他们实现这一点的方式略有不同。递推通过设置明确的访问顺序来避免重复访问。而记忆化搜索虽然没有明确的访问顺序,但是通过给已经访问过的子状态打标记的形式,也达到了相同的目的。

  • 相同点
    • 都有重叠子问题。相对于暴力求解,使用这两种方法的目的,都是为了避免重复计算这些重叠子问题。
    • 都有状态(case)、base case 和状态转移方程。
  • 区别点
    • 记忆化搜索的编程模式是递归,而狭义动态规划(递推 DP) 的编程模式是递推。一般来说,递归的时间和空间开销比递推大。并且递推不一定需要保存所有状态,如 dp[i] = dp[i-1] + dp[i-2] 只需要保存两个状态即可。
    • 记忆化搜索是自顶向下求解,从目标状态到边界条件。递推 DP 是自底向上,从边界条件到目标状态。
    • 记忆化搜索不需要严格设计好计算顺序,备忘录没有记录则进行计算即可。而递推 DP 则通过严格计算 dp table 的递推计算顺序,来避免重复计算。
    • 多个状态下,递推 DP 有可能会产生大量无效状态,而记忆化搜索则不会。这是记忆化搜索在速度上有可能战胜递推 DP 的地方。

回溯 → 记忆化搜索 → 递推 DP 三部曲
#

  • step1,回溯:遍历所有状态(可能剪枝)
  • step2,记忆化搜索:存下已访问过的状态以避免重复计算
  • step3,递推 DP:设置明确的访问顺序来避免重复

形象比喻
#

  • 分治

    分而治之。先解决子问题,再将子问题的解合并求出原问题。

  • 贪心

    一条路走到黑。选择当下局部最优路线,没有后悔药。

  • 回溯

    一条路走到黑,手握后悔药,可以不断折返重来。

  • 动规

    上帝视角,手握无数个平行宇宙的历史存档,同时发展出无数个未来。

参考资料
#