最近在尝试着帮助我的朋友理解动态规划,我在网上找了好久,相关的资料有很多,但是大多时候直接引用了维基百科对动态规划的定义,然后直接对着问题撸代码,我觉得光注重代码实现,是不能很好地将思想传授给其他学习者的。
为了让大家能够更轻松地认识动态规划,同时我也想把我自己学习动态规划的一些过程跟理解记录下来,这篇文章是动态规划系列的第一篇,我将尽力在本文中把动态规划的本质跟它所解决的问题讲清楚。
斐波那契数列
能应用动态规划的问题有很多,但我觉得最经典的,能让人快速对它有一个朦胧的认识的问题就是求解斐波那契数列。很多人可能对斐波那契数列还不是很了解,简单来说,它就是以0,1开头的一个数列,之后的每一位都是前两位之和。举个例子,0,1,1,2,3,5,8,13,21,...
我们用公式把它列出来:
Fib(n) = Fib(n-1) + Fib(n-2), for n > 1
Fib(0) = 0, Fib(1) = 1
我们可以很轻易地把这个公式转化成代码:
public int fib(int n) {
if (n < 2)
return n;
return fib(n - 1) + fib(n - 2);
}
通过递归能够得到我们想要的答案,为了计算当前结果我们会转而先去计算前两个位置的结果Fn-1,Fn-2,最后结合起来就能得到Fn。但是有一个问题,此时的时间复杂度是O(2^n),空间复杂度是O(n),随着输入数字的变大,我们得到结果要等待的时间会增加,这个时间很大一部分浪费在重复计算上。假设我们输入是n=5,我们看一看下面一张图:

思路一:
工程上的经验告诉我们,当一个先前获取的结果后面还有可能用到并且每个相同的输入返回的结果都相同时,我们可以把这个结果缓存起来。这样,当缓存中存在对于某个输入的结果时,我们可以跳开计算直接从缓存返回那个结果,不然我们得先计算,然后把结果放到缓存中,以备下次需要的时候使用。代码如下:
public int fib(int n) {
int dp[] = new int[n + 1];//使用数组缓存结果
return fibRecursive(dp, n);
}
public int fibRecursive(int[] dp, int n) {
if (n < 2)
return n;
if (dp[n] == 0)
dp[n] = fibRecursive(dp, n - 1) + fibRecursive(dp, n - 2);
return dp[n];
}
这个时候我们已经能把代码优化到时间空间复杂度都是O(n),就结果而言,这对我们来说是个不小的突破。
那时间空间复杂度还能不能更低了?我们来试试看!
思路二:
我们再回过来仔细观察一下这个数列:

那我们实现就很清晰了:
public int fib(int n) {
int dp[] = new int[n + 1];
dp[0] = 0;
dp[1] = 1; for (int i = 2; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
此时时间空间复杂度还都是O(n),细心的同学可能已经发现了,对于这个问题,其实我们需要且只需要前两个数,更早之前的结果其实用完就不必缓存了,可以丢弃掉,(也就是说,当已经计算出F4的时候,还缓存着F0~F2其实没有意义,只会浪费空间,我们不会再用到它们了)那我们就不必再维护一个缓存数组,使用两个变量来存储前两个数就足够了,这样就把空间复杂度下降到一个常量级O(1)。
public int fib(int n) {
if (n < 2)
return n;
int n1 = 0, n2 = 1, temp;
for (int i = 2; i <= n; i++) {
temp = n1 + n2;
n1 = n2;
n2 = temp;
}
return n2;
}
到这里我们对斐波那契数列问题的探讨就结束了。很多人可能要纳闷了,???怎么完全没提到动态规划呀,哈哈哈,其实我们上面思考过程就是在用动态规划解决问题啦,就是这么简单,稍稍总结下,所谓动态规划本质上就是对递归进行优化的一种方法,而动态规划问题有两个显著的特征,
- 它有很多重复的子问题(递归中遇到重复计算);
- 基于子问题的结果可以得出原问题的结果。
然后根据这两个特征我们也衍生出了两种优化思路:
- 解决当前问题的过程中解决包含的子问题,并把已解决的问题结果缓存起来。(思路一)
- 先解决子问题,然后直接合并涉及到的子问题的结果产生当前问题的结果。(思路二)
好了,相信大家对动态规划都已经有了个大致的感知了,之前提到动态规划好多人都很怕啊,觉得面试遇到这种问题就肯定凉了。其实不必慌张,拿到动态规划问题后把它分解成更小更简单的子问题,然后应用我们上面的两种思路解决问题即可。我知道一个问题到手最难的其实是判别它是不是动态规划问题,或者说有些动态规划问题的子问题可能不像斐波那契数列这么好识别,在后续的文章里,我也会列举出常见的几种动态规划题目类型,帮助大家增强认识。
