大厂算法面试之leetcode精讲3.动态规划

大厂算法面试之leetcode精讲3.动态规划

视频教程(高效学习):点击学习
目录:

1.开篇介绍

2.时间空间复杂度

3.动态规划

4.贪心

5.二分查找

6.深度优先&广度优先

7.双指针

8.滑动窗口

9.位运算

10.递归&分治

11剪枝&回溯

12.堆

13.单调栈

14.排序算法

15.链表

16.set&map

17.栈

18.队列

19.数组

20.字符串

21.树

22.字典树

23.并查集

24.其他类型题

什么是动态规划

动态规划,英文:Dynamic Programming,简称DP,将问题分解为互相重叠的子问题,通过反复求解子问题来解决原问题就是动态规划,如果某一问题有很多重叠子问题,使用动态规划来解是比较有效的。

求解动态规划的核心问题是穷举,但是这类问题穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下。动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程」**才能正确地穷举。重叠子问题、最优子结构、状态转移方程就是动态规划三要素

动态规划和其他算法的区别
  1. 动态规划和分治的区别:动态规划和分治都有最优子结构 ,但是分治的子问题不重叠
  2. 动态规划和贪心的区别:动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优解,所以它永远是局部最优,但是全局的解不一定是最优的。
  3. 动态规划和递归的区别:递归和回溯可能存在非常多的重复计算,动态规划可以用递归加记忆化的方式减少不必要的重复计算
动态规划的解题方法
  • 递归+记忆化(自顶向下)
  • 动态规划(自底向上)

ds_135

解动态规划题目的步骤
  1. 根据重叠子问题定义状态
  2. 寻找最优子结构推导状态转移方程
  3. 确定dp初始状态
  4. 确定输出值
斐波那契的动态规划的解题思路

ds_3

动画过大,点击查看

暴力递归
//暴力递归复杂度O(2^n)
var fib = function (N) {
   
    if (N == 0) return 0;
    if (N == 1) return 1;
    return fib(N - 1) + fib(N - 2);
};
递归 + 记忆化
var fib = function (n) {
   
    const memo = {
   }; // 对已算出的结果进行缓存

    const helper = (x) => {
   
        if (memo[x]) return memo[x];
        if (x == 0) return 0;
        if (x == 1) return 1;
        memo[x] = fib(x - 1) + fib(x - 2);
        return memo[x];
    };

    return helper(n);
};


动态规划
const fib = (n) => {
   
    if (n <= 1) return n;
    const dp = [0, 1];
    for (let i = 2; i <= n; i++) {
   
        //自底向上计算每个状态
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};

滚动数组优化
const fib = (n) => {
   
    if (n <= 1) return n;
    //滚动数组 dp[i]只和dp[i-1]、dp[i-2]相关,只维护长度为2的滚动数组,不断替换数组元素
    const dp = [0, 1];
    let sum = null;
    for (let i = 2; i <= n; i++) {
   
        sum = dp[0] + dp[1];
        dp[0] = dp[1];
        dp[1] = sum;
    }
    return sum;
};
动态规划 + 降维,(降维能减少空间复杂度,但不利于程序的扩展)
var fib = function (N) {
   
    if (N <= 1) {
   
        return N;
    }
    let prev2 = 0;
    let prev1 = 1;
    let result = 0;
    for (let i = 2; i <= N; i++) {
   
        result = prev1 + prev2; //直接用两个变量就行
        prev2 = prev1;
        prev1 = result;
    }
    return result;
};

509. 斐波那契数(easy)
方法1.动态规划
  • 思路:自底而上的动态规划
  • 复杂度分析:时间复杂度O(n),空间复杂度O(1)

Js:

var fib = function (N) {
   
    if (N <= 1) {
   
        return N;
    }
    let prev2 = 0;
    let prev1 = 1;
    let result = 0;
    for (let i = 2; i <= N; i++) {
   
        result = prev1 + prev2;
        prev2 = prev1;
        prev1 = result;
    }
    return result;
};


Java:

class Solution {
   
    public int fib(int n) {
   
        if (n <= 1) {
   
            return n;
        }
        int prev2 = 0, prev1 = 1, result = 0;
        for (int i = 2; i <= n; i++) {
   
            result = prev2 + prev1;
            prev2 = prev1; 
            prev1 = result; 
        }
        return result;
    }
}
62. 不同路径 (medium)
方法1.动态规划

动画过大,点击查看

  • 思路:由于在每个位置只能向下或者向右, 所以每个坐标的路径和等于上一行相同位置和上一列相同位置不同路径的总和,状态转移方程:f[i][j] = f[i - 1][j] + f[i][j - 1];
  • 复杂度:时间复杂度O(mn)。空间复杂度O(mn),优化后O(n)

js:

var uniquePaths = function (m, n) {
   
    const f = new Array(m).fill(0).map(() => new Array(n).fill(0)); //初始dp数组
    for (let i = 0; i < m; i++) {
   
        //初始化列
        f[i][0] = 1;
    }
    for (let j = 0; j < n; j++) {
   
        //初始化行
        f[0][j] = 1;
    }
    for (let i = 1; i < m; i++) {
   
        for (let j = 1; j < n; j++) {
   
            f[i][j] = f[i - 1][j] + f[i][j - 1];
        }
    }
    return f[m - 1][n - 1];
};

//状态压缩
var uniquePaths = function (m, n) {
   
    let cur = new Array(n).fill(1);
    for (let i = 1; i < m; i++) {
   
        for (let r = 1; r < n; r++) {
   
            cur[r] = cur[r - 1] + cur[r];
        }
    }
    return cur[n - 1];
};


Java:

class Solution {
   
    public int uniquePaths(int m, int n) {
   
        int[][] f = new int[m][n];
        for (int i = 0; i < m; ++i) {
   
            f[i][0] = 1;
        }
        for (int j = 0; j < n; ++j) {
   
            f[0][j] = 1;
        }
        for (int i = 1; i < m; ++i) {
   
            for (int j = 1; j < n; ++j) {
   
                f[i][j] = f[i - 1][j] + f[i][j - 1];
            }
        }
        return f[m - 1][n - 1];
    }
}

//状态压缩
class Solution {
   
    public int uniquePaths(int m, int n) {
   
        int[] cur = new int[n];
        Arrays.fill(cur,1);
        for (int i = 1; i < m;i++){
   
            for (int j = 1; j < n; j++){
   
                cur[j] += cur[j-1] ;
            }
        }
        return cur[n-1];
    }
}
63. 不同路径 II(medium)
方法1.动态规划
  • 思路:和62题一样,区别就是遇到障碍直接返回0
  • 复杂度:时间复杂度O(mn),空间复杂度O(mn),状态压缩之后是o(n)

Js:

var uniquePathsWithObstacles = function (obstacleGrid) {
   
    const m = obstacleGrid.length;
    const n = obstacleGrid[0].length;
    const dp = Array(m)
        .fill()
        .map((item) => Array(n).fill(0)); //初始dp数组

    for (let i = 0; i < m && obstacleGrid[i][0] === 0; ++i) {
   
        //初始列
        dp[i][0] = 1;
    }

    for (let i = 0; i < n && obstacleGrid[0][i] === 0; ++i) {
   
        //初始行
        dp[0][i] = 1;
    }

    for (let i = 1; i < m; ++i) {
   
        for (let j = 1; j < n; ++j) {
   
            //遇到障碍直接返回0
            dp[i][j] = obstacleGrid[i][j] === 1 ? 0 : dp[i - 1][j] + dp[i][j - 1];
        }
    }

    return dp[m - 1][n - 1];
};

//状态压缩
var uniquePathsWithObstacles = function (obstacleGrid) {
   
    let m = obstacleGrid.length;
    let n = obstacleGrid[0].length;
    let dp = Array(n).fill(0); //用0填充,因为现在有障碍物,当前dp数组元素的值还和obstacleGrid[i][j]有关
    dp[0] = 1; //第一列 暂时用1填充
    for (let i = 0; i < m; i++) {
   
        for (let j = 0; j < n; j++) {
   
            if (obstacleGrid[i][j] == 1) {
   
                //注意条件,遇到障碍物dp[j]就变成0,这里包含了第一列的情况
                dp[j] = 0;
            } else if (j > 0) {
   
                //只有当j>0 不是第一列了才能取到j - 1
                dp[j] += dp[j - 1];
            }
        }
    }
    return dp[n - 1];
};


Java:

class Solution {
   
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
   
        int n = obstacleGrid.length, m = obstacleGrid[0].length;
        int[] dp = new int[m];

        dp[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
        for (int i = 0; i < n; ++i) {
   
            for (int j = 0; j < m; ++j) {
   
                if (obstacleGrid[i][j] == 1) {
   
                    dp[j] = 0;
                    continue;
                }
                if (j - 1 >= 0 && obstacleGrid[i][j - 1] == 0) {
   
                    dp[j] += dp[j - 1];
                }
            }
        }
        
        return dp[m - 1];
    }
}
70. 爬楼梯 (medium)
方法1.动态规划

ds_71

  • 思路:因为每次可以爬 1 或 2 个台阶,所以到第n阶台阶可以从第n-2或n-1上来,其实就是斐波那契的dp方程
  • 复杂度分析:时间复杂度O(n),空间复杂度O(1)

Js:

var climbStairs = function (n) {
   
    const memo = [];
    memo[1] = 1;
    memo[2] = 2;
    for (let i = 3; i <= n; i++) {
   
        memo[i] = memo[i - 2] + memo[i - 1];//所以到第n阶台阶可以从第n-2或n-1上来
    }
    return memo[n];
};

//状态压缩
var climbStairs = (n) => {
   
    let prev = 1;
    let cur = 1;
    for (let i = 2; i < n + 1; i++) {
   
        [prev, cur] = [cur, prev + cur]
        // const temp = cur;   // 暂存上一次的cur
        // cur = prev + cur;   // 当前的cur = 上上次cur + 上一次cur
        // prev = temp;        // prev 更新为 上一次的cur
    }
    return cur;
}

Java:

class Solution {
   
    public int climbStairs(int n) {
   
        int prev = 1, cur = 1;
        for (int i = 2; i < n + 1; i++) {
   
        int temp = cur;
        cur = prev + cur;  
        prev = temp; 
        }
        return cur;
    }
}
279. 完全平方数 (medium)

ds_204

方法1:动态规划
  • 思路:dp[i] 表示i的完全平方和的最少数量,dp[i - j * j] + 1表示减去一个完全平方数j的完全平方之后的数量加1就等于dp[i],只要在dp[i], dp[i - j * j] + 1中寻找一个较少的就是最后dp[i]的值。

  • 复杂度:时间复杂度O(n* sqrt(n)),n是输入的整数,需要循环n次,每次计算dp方程的复杂度sqrt(n),空间复杂度O(n)

js:

var numSquares = function (n) {
   
    const dp 
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值