第一个算法笔记之动态规划

166 阅读3分钟

为什么算法重要?很多时候我逃避算法,但算法真的不是想逃就能逃的。作为一个前端开发,我之前一直规避算法,老觉得嗯,我的八股文扎实了,技术项目有了,我就可以进大厂了。 but。。。三月底我参加了网易的笔试,被狠狠的伤了,我决定要好好的补算法。

借用我朋友的话警醒我自己:

算法不会,只能搬砖

image.png

动态规划

这是面试常考的一个算法,并且学会了真的很好用。

动态规划,就是利用 历史记录 ,来避免重复计算。存储这些历史记录我们一般采用 一维数组 或者 二维数组 来记录,重点!我们要明白,数组当中每个元素的含义!

1.动态规划的思路

  1. 明确数组元素的意义:假如我们使用一维数组dp来记录历史记录,这时非常重要的,就是规定这个数组的含义,dp[i]我们到底代表什么意思

  2. 确定数组元素之间的关系式:动态规划利用历史记录来推算出新的值,所以我们就要找出数组元素之间的关系式,比如青蛙跳台阶当中:dp[n] = dp[n-1]+dp[n-2],这就是我们所要确定的关系式,这是最难的,也是最重要的。

  3. 找出初始值,动态规划虽然使用 历史记录,但是 历史记录也是由初始值计算而来,dp[n] = dp[n-1]+dp[n-2]当中dp[n]也是依靠dp[n-1]与dp[n-2],但从头开始为dp[3] = dp[2]+dp[1],dp[2]与dp[1]不能再分解了,所以这就是初始值

2.案例分析

先来一个简单的一维数组的

青蛙跳台

问题描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 (1)明确数组元素的含义

按步骤,首先我们来定义 dp[i] 的含义,要求出:青蛙跳上 n 级的台阶总共由多少种跳法,那我们就定义 dp[i] 的含义为:跳上一个 i 级的台阶总共有 dp[i] 种跳法。这样最后 dp[n] 就是我们所求的结果了

(2)确定数组元素间的关系式

第二步,我们要找出元素之间的关系。这是最核心最难的一个,我们必须回到问题本身,来找出他们的关系式。 对于这道题,题中表明青蛙可以选择跳一级,也可以选择跳两级,所以青蛙到达第 n 级的台阶有两种方式:
一种是从第 n-1 级跳上来
一种是从第 n-2 级跳上来
由于我们是要算所有可能的跳法的,所以得出 dp[n] = dp[n-1] + dp[n-2]。

(3)、找出初始条件

显然由题可得初始值,dp[1] = 1,dp[0] = 1.(因为 0 个台阶,就是不跳这种跳法),dp[2] = dp[1]+dp[2](为什么不是dp[1]开始?因为dp[1]=dp[0]+dp[-1]显然不成立)

上代码:

var numWays = function(n) {
    if(n == 0 || n==1){
        return 1
    }

    let dp = [0,1]
    for(let i =2;i <= n;i++){
        dp[i] = dp[i-1]+dp[i-2]
    }
    return arr[n]
};

很多容易初始值找得有问题,比如刚刚的dp[0]很多就会找成0,这样结果都会有问题

机器人不同路径

这是一个二维数组存储历史记录的题型

(1)、定义数组元素的含义

我们的目标是求从左上角到右下角一共有多少种路径,这样我们就定义 dp[i] [j]为:当机器人从左上角走到[i, j]这个位置时,一共有 dp[i] [j] 种路径。最后dp[m-1] [n-1] 就是最终的结果。

(2)、找出数组元素间的关系式

机器人要怎么样才能到达 [i, j] ?机器人可以向下走或者向右走,
所以有两种方式:
一种是从 [i-1, j] 这个位置走一步到达;
一种是从[i, j - 1]这个位置走一步到达
因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。

(3)、找出初始值

因为当 dp[i] [j] 中,若 i 或者 j 有一个为 0,是不能的使用关系式的,因为此时把 i - 1 或者 j - 1,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [i] 和所有的 dp[j] [0]。其实就是计算机数组的首行和首列。

因此初始值如下:
dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走
dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走

var uniquePaths = function(m, n) {
    /*初始值为1(第一行与第一列),我这里其实是创建数组时,
    对(第一行与第一列)进行了初值赋值,这里我采用的简便的写法全部都赋值了
    */
    let dp = new Array(m).fill(new Array(n).fill(1))    
    if(m <= 0 || n <= 0){
        return 0
    }
    for(let i = 1;i < m;i++){
        for(let j =1;j < n;j++){
            dp[i][j] = dp[i-1][j]+dp[i][j-1]//数组元素间的关系式
        }
    }
    return res[m-1][n-1]
};

最小路径和

上代码,这里我直接上代码了,按照步骤分析

var minPathSum = function(grid) {
    let m = grid.length
    let n = grid[0].length
    if(m == 0 || n == 0){
        return 0
    }
    //dp[i][j]的意义:从开始到[i][j]最短的路径和,每一个点都是最短路径,有点类似贪心,每一步都是最优,最后整体最优
    let dp = new Array(m).fill(0).map(() => new Array(n).fill(0))
    //初值
    dp[0][0] = grid[0][0]
    for(let i = 1;i < n;i++){//第一行
        dp[0][i] = grid[0][i]+dp[0][i-1]
    }
    for(let i = 1;i < m;i++){//第一列
        dp[i][0] = grid[i][0]+dp[i-1][0]
    }
    for(let i = 1;i < m;i++){
        for(let j = 1;j < n;j++){
            dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j]//数组间关系式
        }
    }
    return dp[m-1][n-1]
};