文章目录
  1. 1. 引言
  2. 2. 为什么要使用动态规划?
    1. 2.1. Example
    2. 2.2. 暴力搜索法
    3. 2.3. 记忆搜索法
    4. 2.4. 动态规划
  3. 3. 总结

网上关于动态规划的资料,大部分直接给结论,所以一开始我一头雾水,搞不懂为什么要这么做,这篇博文就从实际问题出发,简单的剖析动态规划

引言

现实生活中总能找到一些问题你没法给出具体答案,比如给你一堆1块、5块、10块的零钱,要你找出多少种能够拼出100块的方法。还有就是迷宫问题这种。这种问题都有一个特征,我们没有办法立刻给出答案,而且我们对这种问题的想到的第一种解决方法就是暴力搜索,把所以的可能方案列出来然后得到答案。这种暴力搜索最终能够解决问题,但是他们在计算的时候花了很多时间在相同的计算上面。为了节省时间,所以我们使用动态规划“优化”暴力搜索

为什么要使用动态规划?

Example

我们先举一个简单的例子,大家都知道走楼梯问题,这也是教科书一个经典的递归程序

  • 有N阶台阶,一个人一次上两个或者一个,问有多少种走法?

我们拿到这道题,我们首先会想这样想,从第一个台阶开始,我们使用递归让这个人走一步或者两步,这样每次分解成为两种可能,最后直达走到N阶台阶,或者迈过去了,然后计算这种所以迈到N阶台阶的可能路径。

这种正向思维很容易理解,但是最终它直接得到的是所以可能的路径,但是这道题我们需要求的是N阶的走法,所以我们从正向思维必须要反过来思考,假如我们从一个台阶出发有两种可能,那么我们到达第N个的台阶来看,也有两种可能,第一种是N-1(迈了一步到达),第二种是N-2(迈了两步到达),这样我们就很清楚了,假如我们要想得到到达N阶台阶的走法总数,那么我们只需要把到达N-1和到达N-2的次数加起来就可以了

这是一个很重要的思想把一个复杂的问题,分解成为其他的子问题,这也是我们完成动态规划的设计的核心思想

从更好的理解动态规划的优点和源头,我们就从这个简单的例子使用不同的算法来解释为什么要用动态规划

暴力搜索法

我们成功的完成了问题的分解,为了完成计算,但是我们还得计算子问题的结果,上面得到一个很重要的公式F(N) = F(N-1) + F(N-2)

我们可以很轻松的写出代码(Python)

def f(n):
  return f(n-1) + f(n-2) if n > 2 else n

我们只用一行代码就能将这个问题解决掉,而且效果看起来还不错,我们可以试不同的n都能获取正确的结果,但是n大于30之后,当我在我的电脑上运行起来非常慢,需要几秒钟才能返回结果,而且当n越大,消耗的时间也越多。

这是为什么呢?我们现在来思考一下这个暴力算法有什么弊端

  • 暴力搜索的弊端

我们现在假设N=10,那么我们现在就把F(10)转换成为F(9)与F(8)的和,那么F(9)又分成了F(8)和F(7),而F(8)被分成了F(7)和F(6)

从这里可以看到,F(7)在第二次分解的时候计算了两次,而每次计算的结果都是一样的,所以我们相当于重复了一遍耗时的操作,知道这个问题,我们就必须改进了,我们可以用一个东西存贮计算结果,这样就不需要重复计算了

记忆搜索法

我们修改我们算法,加一个参数map

def map_get(map, n):
    v = map.get(n-1)
    if not v:
      v = f(n, map)
      map[n] = v
    return v


def f(n, map):
    if n < 3:
      return n
    if n in map:
      return map[n]
    return map_get(map, n - 1) + map_get(map, n - 2)

我们添加一个辅助的字典存贮我们中间计算过程,虽然让我们的代码臃肿了不少,但是让我们代码速度有了质的变化

我们尝试使用运行让N增大到100多都能迅速返回,但是当我们逐渐增大到1000的时候我们会发现Python直接报了超出最大堆栈限制的错误

  • 堆栈超出最大层数的原因

由于我们使用了递归,递归函数是在递归的时候当前堆栈再次申请一个堆栈待,运行递归函数,为了避免一直无限调用下去耗空堆栈空间(申请也需要一点空间),Python限制了递归层数,由于为了计算超过1000的我们必须至少要递归超过1000次(从1000减一减到小于2),所以我们光荣的被当错程序错乱被误杀了。

动态规划

反观我们这个函数,使用递归,我们很容易理解,但是对于计算机来说,只是为了计算一个数而使用递归是非常不划算的,所以我们要思考这些中间值保留有什么共同点,我们从头开始看,对于第三个来说,它只需要知道第一个和第二个的值就行,而第一个第二个我们知道分别为1和2,对于第四个来说,它只需要知道第三个和第二个,如果我们先把第三个计算下来并保留下来,我们就能知道第四个。

从头开始思考我们知道,我们只需要保留前面计算的结果就能知道后面的值,我们使用一个列表保存这个中间计算过程,我们将函数改写成下面这个

def f(n):
    if n < 3: return n
    values = [1, 2]
    for i in range(2, n):
      values.append(values[i-2]+values[i-1])
    return values[-1]

接下来我们运行这个函数,我们会发现就算N为10000都能迅速返回

来看看我们动态规划的“损失”,我们使用了一个列表存贮中间过程,使用了空间“换回”了速度

总结

我们使用了一个很简单的题目来介绍动态规划,由于这个问题太过于简单,你或许自己在不知道动态规划的时候都能写出来,但是这个从暴力搜索到记忆搜索最后动态规划的算法优化过程中,我们能够清楚的知道设计动态规范其实也非常简单,将大问题分解成小问题,然后思考小问题的如何细分,最后反过来思考从小问题逆向到最终的大问题,这就是动态规划。

当然这道问题并不是很经典的动态规划问题,为了让大家更好的理解动态规划,我在下面这篇的博文中介绍若干中经典的动态规划问题

几个有趣的动态规划

文章目录
  1. 1. 引言
  2. 2. 为什么要使用动态规划?
    1. 2.1. Example
    2. 2.2. 暴力搜索法
    3. 2.3. 记忆搜索法
    4. 2.4. 动态规划
  3. 3. 总结