动态规划
基本思想
若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用
明确三个事情:
- 目标问题
- 状态的定义:opt[n]
- 状态转移方程:opt[n]=best_of(opt[n-1],opt[n-2])
分治与动态规划
共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题.然后将子问题的解合并,形成原问题的解.
不同点:分治法将分解后的子问题看成相互独立的,通过用递归来做。
动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。
步骤
- 求一个问题的最优解
- 大问题可以分解为子问题,子问题还有重叠的更小的子问题
- 整体问题最优解取决于子问题的最优解(状态转移方程)
- 从上往下分析问题,从下往上解决问题
- 讨论底层的边界问题
动态规划最重要的有三个概念:1、最优子结构 2、边界 3、状态转移方程
引出:
由一个简单的问题来解释为什么要使用动态规划:
有十个台阶,从上往下走,一次只能走一个或两个台阶,请问总共有多少种走法?
- 最优子结构:我们来考虑要走到第十个台阶的最后一步,最后一步必须走到第八或者第九。不难得到 f(10) = f(9)+f(8)、f(9) = f(8)+f(7)·····
- 边界:f(1) = 1, f(2) = 2
- 状态转移:f(n) = f(n-1) + f(n-2)
简单粗暴的递归解法:
def get_count(n):
if n == 1:return 1
if n == 2:return 2
else:
return get_count(n-1)+get_count(n-2)
print(get_count(10))
很显然这是一个满二叉树,高度为N-1。所以总节点数为2N−12^{N-1}2N−1,时间复杂度为O(2N2^{N}2N) 。看着就恐怖。递归计算方法,我们不难看出,2N−12^{N-1}2N−1个节点产生了大量重复的运算。找到问题的根源,对应的解决方法就应运而生,那就是从下往上算,把以前计算过的数值,保存在一个哈希表中,然后后面计算时先查询一下,存在就无需计算。时间复杂度为O(n) ,空间复杂度为O(n)。但是在仔细一想其实,无需保存所有的fff,每个fff