• 动态规划(1)使用斐波那契数列引入了动态规划的概念


    9-1 使用斐波那契数列引入了动态规划的概念

    一、计算斐波那契数列的第 (n) 项数值

    1、斐波那契数列的定义

    斐波那契数列是通过"递归"定义的,通过这个递归关系式,我们可以知道斐波那契数列中任意一个位置的数值。

    [egin{equation}egin{split} F(0) & = 0,\ F(1) & = 1,\ F(n) & = F(n-1) + F(n-2),\ end{split}end{equation} ]

    2、第 1 版 Python 代码实现:使用斐波那契数列的定义式子递归实现

    很容易地,我们能写出下面的代码:

    def fib(n):
        if n == 0:
            return 0
        if n == 1:
            return 1
        return fib(n - 1) + fib(n - 2)
    

    说明:

    1. 代码本身用于计算是没有问题的,但是仔细研究,我们就会发现,我们虽然使用递归实现了斐波那契数列在任意位置的值的计算,但是,如果要我们自己计算的话,肯定不会这样计算,因为太耗时了。

    2. 耗时的原因在于,在上述的递归实现中,存在大量的重复计算,例如:
      要计算 fib(4),就得计算 fib(3) 和 fib(2),
      要计算 fib(3),就得计算 fib(2) 和 fib(1),
      此时 fib(2) 就被重复计算了,下面是一张图,展示了部分重复计算的过程。

    1. 要解决上一步的问题,就要避免重复计算,我们可以引入一个 memo 数组,用于存入已经计算过一次的 fib 的值,下一次需要这个值的时候,再从中取,下面是代码实现。

    3、第 2 版 Python 代码实现:加入了记忆化搜索,即使用了缓存数组,以避免重复计算

    memo = None
    
    
    def _fib(n):
        if memo[n] != -1:
            return memo[n]
        if n == 0:
            return 0
        if n == 1:
            return 1
        memo[n] = _fib(n - 1) + _fib(n - 2)
        return memo[n]
    
    
    def fib(n):
        global memo
        memo = [-1] * (n + 1)
        return _fib(n)
    

    4、第 3 版 Python 代码实现:虽然很简单,但是我们就可以称之为“动态规划”的解法

    这个版本是最接近我们自己去计算斐波那契数列的第 (n) 项。想一想的确是这样,聪明的你一定不会递归去计算波那契数列的,因为我们的脑容量是有限,不太适合做太深的递归思考,虽然计算机在行递归,但是我们也没有必要让计算机做重复的递归工作。

    def fib(n):
        memo = [-1] * (n + 1)
        memo[0] = 0
        memo[1] = 1
    
        for i in range(2, n + 1):
            memo[i] = memo[i - 1] + memo[i - 2]
        return memo[n]
    

    二、什么是“记忆化搜索”

    针对一个递归问题,如果它呈树形结构,并且出现了很多”重叠子问题”,会导致计算效率低下,“记忆化搜索”就是针对”重叠子问题”的一个解决方案,实际上就是”加缓存避免重复计算”。

    三、什么是“动态规划”

    (1)比较“记忆化搜索”与“动态规划”

    由上面的介绍我们就可以引出动态规划的概念:

    • "记忆化搜索"或者我们称"重叠子问题"的加缓存优化的实现,我们的思考路径是"自顶向下"。即为了解决数据规模大的问题,我们“假设”已经解决了数据规模较小的子问题。
    • “动态规划”就是上述"循环版本"的实现,我们思考问题路径是"自下而上"。实际上,我们是先“真正地”解决了数据规模较小的问题,然后一步一步地解决了数据规模较大的问题。

    (2)“动态规划”的官方定义

    下面我们给出“动态规划”的官方定义:

    dynamic programming (also known as dynamic optimization) is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions – ideally, using a memory-based data structure.

    将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。

    (3)针对“动态规划”问题的一般思考路径

    我们通常的做法是:使用记忆化搜索的思路思考原问题,但是使用动态规划的方法来实现。即“从上到下”思考,但是“从下到上”实现。

    四、总结

    对于一个递归结构的问题,如果我们在分析它的过程中,发现了它有很多“重叠子问题”,虽然并不影响结果的正确性,但是我们认为大量的重复计算是不环保,不简洁,不优雅,不高效的,因此,我们必须将“重叠子问题”进行优化,优化的方法就是“加入缓存”,“加入缓存”的一个学术上的叫法就是“记忆化搜索”。

    另外,我们还发现,直接分析递归结构,是假设更小的子问题已经解决给出的实现,思考的路径是“自顶向下”。但有的时候,“自底向上”的思考路径往往更直接,这就是“动态规划”,我们是真正地解决了更小规模的问题,在处理更大规模的问题的时候,直接使用了更小规模问题的结果。

  • 相关阅读:
    读书笔记—CLR via C#线程25-26章节
    算法回顾--N皇后问题简单回顾
    编程拾趣--集合子集问题
    读书笔记—CLR via C#异常和状态管理
    读书笔记—CLR via C#字符串及文本
    设计模式---抽象工厂
    读书笔记—CLR via C#反射
    读书笔记—CLR via C#委托和attribute
    C#编程实践—EventBroker简单实现
    Linux平台屏幕录像工具RecordMyDesktop
  • 原文地址:https://www.cnblogs.com/liweiwei1419/p/8616113.html
Copyright © 2020-2023  润新知