从递归到动态规划(一)斐波那契数列

动态规划(dynamic programming),简称 dp。是刷leetcode、刷ob的主要算法之一。(逃c++

动态规划其实有挺多问题有他使用的场景的,好比是数据库的JOIN 。若是你深刻了解过数据库原理的话,数据库的多表 JOIN 是真的复杂。算法

动态规划,和递归是相似的,都是一种分而治之的思想。递归的思想,就一些状况下实际上是存在着重复计算的。而递归可优化成动态规划的缘由,或者说动态规划的性质吧,就是递归中的每一步都是最优解,每一步的最优解均可以根据上一步的结果得出。而且每一步中都存在着重复计算的问题。数据库

问题描述

这样说有点抽象,具个例子吧,就是几乎每本教材将递归的时候都会讲到的斐波那契数列(兔子队列)。本来的问题描述是这样的,假设第1个月有1对刚诞生的兔子,第2个月进入成熟期,第3个月开始生育兔子,而1对成熟的兔子每个月会生1对兔子,兔子永不死去……那么,由1对初生兔子开始,12个月后会有多少对兔子呢?编程

这个问题。答案确定是 当前月的兔子书 = 上个月的兔子数 + 新生的兔子数。数组

而新生的兔子数又恰好又等于 上上个月的兔子数。 因此能够得出结论是 当前月的兔子书 = 上个月的兔子数 + 上上个月的兔子数。bash

抽象出来就是 网络

用 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训练)之前的思惟很难转向计算机那种思惟,我解决问题的方式每每是先写递归的实现,再慢慢地转成自底向上的动态规划。。。

相关文章
相关标签/搜索