动态规划(dynamic programming),简称 dp。是刷leetcode、刷ob的主要算法之一。(逃
动态规划其实有挺多问题有他使用的场景的,比如是数据库的JOIN 。如果你深入了解过数据库原理的话,数据库的多表 JOIN 是真的复杂。
动态规划,和递归是类似的,都是一种分而治之的思想。递归的思想,就一些情况下其实是存在着重复计算的。而递归可优化成动态规划的原因,或者说动态规划的性质吧,就是递归中的每一步都是最优解,每一步的最优解都可以根据上一步的结果得出。并且每一步中都存在着重复计算的问题。
问题描述
这样说有点抽象,具个例子吧,就是几乎每本教材将递归的时候都会讲到的斐波那契数列(兔子队列)。原本的问题描述是这样的,假设第1个月有1对刚诞生的兔子,第2个月进入成熟期,第3个月开始生育兔子,而1对成熟的兔子每月会生1对兔子,兔子永不死去……那么,由1对初生兔子开始,12个月后会有多少对兔子呢?
这个问题。答案肯定是 当前月的兔子书 = 上个月的兔子数 + 新生的兔子数。
而新生的兔子数又刚好又等于 上上个月的兔子数。 所以可以得出结论是 当前月的兔子书 = 上个月的兔子数 + 上上个月的兔子数。
抽象出来就是
用 c++ 描述就是
long fib(int n) {
return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
}
数学描述和程序实现是简洁的,但是这程序就存在这很多的重复计算
比如:计算 F(4)
F(4) = F(3) + F(2);
F(3) = F(2) + F(1); //重复计算了 F(2)
F(2) = F(1) + F(0); //重复计算了 F(1)
F(1) = 1;
//...
就意味着,程会序算得很慢,你可以试试 fib(100)看看。速度感人了。这里的时间复杂度可是O(2^n)
但这也存在这非常明显的优化空间。
首先,F(10) 这样的函数是不会因为外部状态发生改变的,也就是说跟什么时间、网络io是一点关系都没有的。F(10) 的答案是恒定的,F(9) 、F(8)的答案也是是恒定的。符合每一步都是最优解,当然也符合每一步都可以通过上一步的结果中得出答案。
其次,这是每一步中都会重复计算的问题。
那么怎样优化。
哈希?
如果没有学过动态规划,正常人最快想到的方法应该是哈希表吧。既然是重复计算,我将重复的结果保存一下就好了。也很容易写出这样的代码
long fib(int n, map<int, long> &map) {
if (map.count(n) > 0)
return map[n];
else {
long val = n < 2 ? 1 : fib(n - 1, map) + fib(n - 2, map);
map.insert({n, val});
return val;
}
}
虽说这种方式是可行了。比如计算 Fib(100),比上面的一段代码快多了。 但这种方式不够优雅。本质上也只是在递归的层面上做了一些优化。而哈希表 这种数据结构存储值的话,内存会有一些浪费,而且要处理key冲突的问题。
数组?
想想计算 fib(100),也就是100次运算,用个数组存储值就可以了。所以可以写成这样。(这其实已经是dp的思路了)
long fib(int n, vector<long> &array) {
if (array[n] != 0)
return array[n];
else {
long val = n < 2 ? 1 : fib(n - 1, array) + fib(n - 2, array);
array[n] = val;
return val;
}
}
递归的思维是自顶向下,和我们在解数学题的时候的思维是一致的。 而如果用自底向上的思路呢?就会写成这样。
long fib(int n) {
if (n < 2)
return 1;
vector<long> array(n + 1, 0);
array[0] = 1;
array[1] = 1;
for (int i = 2; i <= n; i++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[n];
}
应该来讲就更合符,过程式编程语言或者说是机器语言的思维了。我会觉得这种方式比较难想一点。
更好的方式
如果用自底向上的思维。从上面的代码可以看出,其实真的不需要一个数组的空间。只需知道上一步的值和上上一步的值就可以了。
就可以写成这样
long fib(int n) {
if (n < 2)
return 1;
long p1 = 1;
long p2 = 1;
for (int i = 2; i <= n; i++) {
long val = p1 + p2;
p1 = p2;
p2 = val;
}
return p2;
}
再精简一下,去掉 if 就变成这样了
long fib(int n) {
long p1 = 0;
long p2 = 1;
for (int i = 1; i <= n; i++) {
long val = p1 + p2;
p1 = p2;
p2 = val;
}
return p2;
}
总结
总结一下就是,如果纯粹用递归的思维去解决问题,其实是简单的,是符合我们以前解决数学问题的思维的。这种思维在编程语言的实现中,就有重复计算的问题。动态规划会用表格化的思想去解决问题。
可能是我思维定型了吧,就算大学不怎么学习数学,也学了十年数学。(也没怎么acm训练)以前的思维很难转向计算机那种思维,我解决问题的方式往往是先写递归的实现,再慢慢地转成自底向上的动态规划。。。