1. 动态规划
动态规划(Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列,最小编辑距离等等。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于朴素解法。
动态规划有自底向上和自顶向下两种解决问题的方式。自顶向下即记忆化递归,自底向上就是递推。
使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性,求解问题的过程形成了一张有向无环图。动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量。
这是leetcode上面的概念,我们可以把他结构化一下,并理解一下具体的概念。
1、什么是最优子结构?
当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
可以反向理解,我们可以通过子问题的最优解,推导出问题的最优解,也就是子找父。如果我们把最优子结构,对应到我们前面定义的动态规划上,那我们也可以理解为,原问题可以通过子问题推导出来,如F(10) = F(9)+F(8)
,则F(9)
和F(8)
是F(10)
的最优子结构。
2、什么是重叠子问题?
递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次,这种性质称为子问题的重叠性质。
某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响。
2. 斐波拉契数列
斐波那契数列指的是这样一个数列:
这个数列数列从第 3 项开始,每一项都等于前两项之和。其递推公式为:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
2.1 暴力递归
我们通过递归的方式来实现一个斐波那契数列
public int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
可以看到在leetcode运行上并不是很优秀。
我们来分析一下它的执行过程,假如我们要求n=6
的值,则其递归树如下:
可以看到,上面每个节点都会执行一次,而且存在重复执行的节点,这也就是重叠子问题性质的表现,如fib(2)
被重复执行了 5 次。由于调用每一个函数的时候都要保留上下文,所以空间上开销也不小。这么多的子节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。
我们试着用动态规划的自底向上和自顶向下方式来解决斐波拉契数列问题。
2.2 自顶向下备忘录
在递归方法中如果要计算原问题 f(20)
的值,就得先计算出子问题 f(19)
和 f(18)
,然后要计算 f(19)
,我就要先算出子问题 f(18)
和 f(17)
,以此类推。最后遇到 f(1)
或者 f(2)
的时候,结果已知,就能直接返回结果,递归树不再向下生长了。
因此,我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。
一般使用一个数组充当这个「备忘录」,当然也可以使用哈希表。
private Map<Integer, Integer> memo = new HashMap<>();
//备忘录法
public int fib(int n) {
if (n == 0) return 0;
if (n == 1 || n == 2) return 1;
if (memo.containsKey(n)) {
return memo.get(n);
} else {
int value = fib(n - 1) + fib(n - 2);
memo.put(n, value);
return value;
}
}
再次查看时间消耗,很明显耗时减少。
「备忘录」到底做了什么?
当计算 f(20)
的值,先计算出 f(19)
和 f(18)
,而在计算f(19)
的值时,已经把f(18)
的值计算出来了。
实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数,即:
2.3 自底向上动态规划
可以看到,备忘录法还是利用了递归,计算fib(20)
的时候还是要计算出fib(19)
,fib(18)
,fib(17)
…,如果我们先计算出fib(1)
,fib(2)
,fib(3)
…呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。
因为我们已经知道fib(0)
和fib(1)
的值,实际上,fib(2)
的值也是知道的,即:
fib(2) = fib(0) + fib(1) = 1
fib(3) = fib(1) + fib(2) = 2
fib(4) = fib(2) + fib(3) = 3
fib(5) = fib(3) + fib(4) = 5
fib(6) = fib(4) + fib(5) = 8
......
我们根据备忘录的思想,用一张表来记录,即:
public int fib(int n