一文简单入门动态规划

174 阅读4分钟

本文正在参加「金石计划」

前言

在做题的过程中一定经常听说 dp dp,那么什么是 dp。dp 是 Dynamic Programming 动态规划的简称,用于解决一个问题有很多重叠的子问题,简单的来说,就是一个问题可以有两个小的问题组成,这比较类似于分治的思想,举一个最简单的例子--斐波那契数列

斐波那契数列为 0,1,1,2,3,5,8... 等等,不难发现,在这个数列当中,第n的数是由前两个数相加得到的,这就是典型的动态规划思路,f(n) = f(n-1) + f(n-2)

简单介绍

动态规划的题型有特别多,比如说 背包问题,子序列问题,股票问题,这在leetcode上有非常多的经典题目,但是动态规划始终逃不过的几个点就是:

  1. 你需要去定义动态规划保存状态的变量,

  2. 确定每一个状态之间的关系,

  3. 状态变量的初始值是什么,

  4. 如何去遍历状态转化的过程,

  5. 推导最后的状态值

剑指 Offer 10- I. 斐波那契数列

经典题目

斐波那契数列 : 0 1 1 2 3 5 8

f(n) = f(n-1) + f(n-2)

求第 n 个斐波那契数是多少

然后我们根据几个步骤来完成这道递归题目:

  1. 定义动态规划保存状态的变量

在动态规划当中,一半保存状态的变量都是使用数组,当然这不是一定的,在这道题当中,我们就可以定义 dp数组,并且 dp[i] 代表第 i 个斐波那契数的值,dp[0] 代表第 0 个数,也就是 0,dp[1] 为 1,dp[2] 为 1,以此类推。

  1. 确定每一个状态之间的关系,

也就是确定递推公式,这道题的递推公式题目已经告诉我们了,就是 f(n) = f(n-1) + f(n-2)

  1. 状态变量的初始值是什么

根据斐波那契数列的前几个值,以及递推公式 f(n) = f(n-1) + f(n-2) ,我们可以发现,前两个数由于不存在-1和-2的位置,所以是没有办法通过公式推导出来的,这时候就需要我们初始化数组的时候赋予初始值

let dp = [0,1]
  1. 如何去遍历状态转化的过程 按照递推公式,由第三个数开始往下进行计算遍历,当前的数是由前两个数相加得到的。

  2. 推导最后的状态值
    当输入需要的第 n 个 斐波那契数列的值得时候,我们就可以通过 递推公式 进行拆分,拆成一个个小的值相加。

为什么我们需要dp数组?从题目给的递推公式来看,似乎是一个递归就能够解决的问题,也确实,递归公式可以这样写。

function climbStairs1(n) {
    if(n==0) {
        return 0
    }
    if(n==1) {
        return 1
    }
    return climbStairs1(n-1) + climbStairs1(n-2)
};

但是这样的递归树状图为

我们会发现,很多个斐波那契数都是会重复计算的,比方说 f(2) 它是由 f(1) + f(0) 得到的,但是在递归的过程中每次碰到 f(2) 都会去递归成 f(1) + f(0),那么我们是不是可以用一个数组,把当前的斐波那契数保存下来,在下次碰到的时候,直接先判断数组中是否含有,没有再去递归获取,这样就能够大大减少递归树的分支,这也就是dp数组的作用--保存递归结果

如此一来,递归图就变成了

image.png

很明显大幅减少了计算的过程。

所以利用dp数组,我们能够得到题解:

function climbStairs2(n) {
    let map = new Map()
    // 新建表存放数据
    const dfs = (n) => {
        if(n==0) {
            return 1
        }
        if(n==1) {
            return 2
        }
        if(map.get(n)) {
            // 表中已经存在,直接返回
            return map.get(n)
        }else {
            // 表中不存在进行计算
            const t = dfs(n-1) + dfs(n-2)
            map.set(n,t)
            return t
        }
    }
    return dfs(n)
};