什么是动态规划?

最近在尝试着帮助个人朋友理解动态规划,我在网上找了很久,相关的资料有不少,可是大多时候直接引用了维基百科对动态规划的定义,而后直接对着问题撸代码,我以为光注重代码实现,是不能很好地将思想传授给其余学习者的。java

为了让你们可以更轻松地认识动态规划,同时我也想把我本身学习动态规划的一些过程跟理解记录下来,这篇文章是动态规划系列的第一篇,我将尽力在本文中把动态规划的本质跟它所解决的问题讲清楚。面试

斐波那契数列

能应用动态规划的问题有不少,但我以为最经典的,能让人快速对它有一个朦胧的认识的问题就是求解斐波那契数列。不少人可能对斐波那契数列还不是很了解,简单来讲,它就是以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
复制代码

咱们能够很轻易地把这个公式转化成代码:bash

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,咱们看一看下面一张图: 学习

求解过程分解
咱们能够看到,光是计算F5,咱们就进行了不少重复计算。那怎么解决呢?

思路一:

工程上的经验告诉咱们,当一个先前获取的结果后面还有可能用到而且每一个相同的输入返回的结果都相同时,咱们能够把这个结果缓存起来。这样,当缓存中存在对于某个输入的结果时,咱们能够跳开计算直接从缓存返回那个结果,否则咱们得先计算,而后把结果放到缓存中,以备下次须要的时候使用。代码以下:优化

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),就结果而言,这对咱们来讲是个不小的突破。spa

那时间空间复杂度还能不能更低了?咱们来试试看!3d

思路二:

咱们再回过来仔细观察一下这个数列:code

斐波那契数列
咱们能够发现后面的结果只依赖前面的两个数。也就是说咱们在求解5对应的结果的时候,只要知道3跟4对应的结果就行了。而F0跟F1是已知的,经过它俩能够计算出F2,而后又能够计算出F3,以此类推。(注意,这里跟上面的解决方案听着类似实则有个重要的区别,上面的方案是在求当前数对应结果的过程当中去算前两个数的结果,这里的思路是已经事先算出了前两个数的结果,能够直接拿来组合出当前须要的结果)

那咱们实现就很清晰了:

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; 
}
复制代码

到这里咱们对斐波那契数列问题的探讨就结束了。不少人可能要纳闷了,???怎么彻底没提到动态规划呀,哈哈哈,其实咱们上面思考过程就是在用动态规划解决问题啦,就是这么简单,稍稍总结下,所谓动态规划本质上就是对递归进行优化的一种方法,而动态规划问题有两个显著的特征,

  1. 它有不少重复的子问题(递归中遇到重复计算);
  2. 基于子问题的结果能够得出原问题的结果。

而后根据这两个特征咱们也衍生出了两种优化思路:

  1. 解决当前问题的过程当中解决包含的子问题,并把已解决的问题结果缓存起来。(思路一)
  2. 先解决子问题,而后直接合并涉及到的子问题的结果产生当前问题的结果。(思路二)

好了,相信你们对动态规划都已经有了个大体的感知了,以前提到动态规划好多人都很怕啊,以为面试遇到这种问题就确定凉了。其实没必要慌张,拿到动态规划问题后把它分解成更小更简单的子问题,而后应用咱们上面的两种思路解决问题便可。我知道一个问题到手最难的实际上是判别它是否是动态规划问题,或者说有些动态规划问题的子问题可能不像斐波那契数列这么好识别,在后续的文章里,我也会列举出常见的几种动态规划题目类型,帮助你们加强认识。

快来关注我吧
相关文章
相关标签/搜索