算法和数据结构:动态规划

198 阅读2分钟

动态规划介绍

动态规划能做的,递归也能做,只是递归中会有很多重复的子问题,动态规划将这些子问题存储起来,需要用到的时候直接访问,相当于用空间复杂度换取时间复杂度,有时候甚至保存的内容不是很长(比如斐波那契数列),空间复杂度可以只有O(1)

将「动态规划」的概念关键点抽离出来描述就是这样的:

  1. 动态规划法试图只解决每个子问题一次
  2. 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。

509. 斐波那契数列

var fib = function(N) {
    if(N === 0){
        return 0
    }else if(N === 1){
        return 1
    }else{
        // return fib(N - 1) + fib(N - 2)
        //dp
        let dp = [01]
        for(let i = 2; i <= N; i++){
            let temp = dp[1]
            dp[1] = dp[0] + dp[1]
            dp[0] = temp
        }
        return dp[1]
    }
};

70. 爬楼梯

var climbStairs = function(n) {
    // //递归 超时
    // if (n === 1) return 1;
    // if (n === 2) return 2;
    // return climbStairs(n - 1) + climbStairs(n - 2)
    //动态规划 空间复杂度减少了,因为狠多的重复计算没有了
    if (n <= 2return n;
    let pre = 2,
        prepre = 1;
    for (i = 3; i <= n; i++) {
    //第i层的爬的方法等于 i-1 和i-2层的方法和
        cur = pre + prepre;
        prepre = pre;
        pre = cur;
    }
    return cur;
};

62. 不同路径

mxn的网格中,到达右下角的方式。

行遍历,每次只存一行。

var uniquePaths = function(m, n) {
    //动态规划
    // if ( m <= 1 && n <= 1) return 1;
    // let dp = [];
    // for (i = 0; i < m; i++) {
    //     dp[i] = [];
    //     for (j = 0; j < n; j++) {
    //         if (i == 0 || j == 0) {
    //             dp[i][j] = 1;
    //         } else {
    //             dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    //         }
    //     }
    // }
    // return dp[m - 1][n - 1];
    //动态规划——空间优化版
    if ( m <= 1 && n <= 1return 1;
    let dp = [];
    for (i = 0; i < m; i++) {
        //一次写完一行,下一行时, dp[i][j] 需要d[i-1][j]和d[i][j-1],这两个一个是上一行的,一个是刚才更新的,也正好就是当前这行的 j 和j -1
        for (j = 0; j < n; j++) {
            if (i == 0 || j == 0) {
                dp[j] = 1;
            } else {
                dp[j] = dp[j] + dp[j - 1];  //上边和左边
            }
        }
    }
    return dp[n - 1]
};

53. 最大子序和

var maxSubArray = function(nums) {
    //暴力双重循环,超时
    // let maxSum = 0
    // for(let i=0;i<nums.length;i++){
    //     let sum = 0
    //     for(let j = i;i<nums.length;j++){
    //         sum += nums[j]
    //         maxSum = Math.max(maxSum, sum)
    //     }
    // }
    // return maxSum
    
    //dp,当前的最大和只与前一个有关
    let dp = new Array(nums.length)
    dp[0] = nums[0]
    for(let i=1;i<nums.length;i++){
        dp[i] = Math.max(dp[i-1], 0) + nums[i]  //包含i的并且到i为止的最大连续和,只要之前的和为负,就抛弃前面的。
    }
    return Math.max(...dp)
};

322. 零钱对换

从0-target的dp都进行计算

var coinChange = function(coins, amount) {
    //使用回溯和剪枝会超时
    //每次都可以使用当前,也可以不使用当前,当价格相等的时候就可以比较,大于就返回
    
    //dp
    let len = coins.length
    let dp = new Array(amount + 1).fill(Infinity//已经初始化了
    dp[0] = 0
    for(let i = 1 ; i <= amount; i++){
        //1-amount 的dp依次计算
        for(let coin of coins){
            if(coin <= i){  //相等时为 dp置为 1,大于的话不可能
                dp[i] = Math.min(dp[i], dp[i - coin] + 1)  //不可能就存为无穷,当前价格减去当前硬币面值的价格所需的最少个数,也就是对当前价格分别减去所有硬币价格的价格进行比较,得出最大的。
            }
        }
    }
    return dp[amount] > amount ? -1 :dp[amount]  //也可能不存在解
};

64. 最小路径和

遍历到的点,按照边界情况,左和上分别考虑即可


var minPathSum = function(grid) {
    if (grid.length == 0return 0;
    if (grid.length === 1 && grid[0].length === 1return grid[0][0];
    //动态规划
    let m = grid.length,
        n = grid[0].length,
        dp = [...grid]; //会修改原数组,但是这里前面的变化不会影响后面的,所以没关系
    for(i = 0; i < m; i++) {
        for (j = 0; j < n; j++) {
            if (i == 0 && j == 0) dp[i][j] = grid[i][j];
            else if (i == 0 && j != 0) dp[i][j] = grid[i][j] + dp[i][j - 1];
            else if (i != 0 && j == 0) dp[i][j] = grid[i][j] + dp[i - 1][j];
            else dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);
        } 
    }
    return dp[m - 1][n - 1];
};

96. 不同的二叉搜索树

//动态规划
//因为左边和右边都是连续数字,所以不管是多少,只要连续的数字个数一样,那么能组成的树的个数也一样
var numTrees = function (n) {
    let dp = new Array(n+1).fill(0)
    dp[0] = 1
    dp[1] = 1
    for(let i = 2;i<=n;i++){
        //i个数字的的子树有i-1个节点
        for(let j = 0;j<=i-1;j++){ //j为左边个数
            dp[i] +=dp[i-1-j]*dp[j]
        }
    }
    return dp[n]
};

211. 最大正方形

var maximalSquare = function(matrix) {
    //注意,是字符串
    //返回的是面积
    //动态规划,当前点作为正方形的右下角
    //为0,则大小为0
    //为1,则看左边,上边,左上边的点各自最大正方形,短板原理,三者合并,三者中最短的+1就是当前点的最大正方形
    let dp = []
    let len = matrix.length
    if (len < 1return 0
    let result = 0
    for (let i = 0; i < len; i++) { //行遍历
        dp[i] = []
        let len2 = matrix[i].length
        for (let j = 0; j < len2; j++) { //列遍历
            if (i === 0 || j === 0) {
                dp[i][j] = parseInt(matrix[i][j])
            } else {
                if (matrix[i][j] === '1') {
                    dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j - 1], dp[i - 1][j]) + 1
                } else {
                    dp[i][j] = 0
                }
            }
            result = Math.max(dp[i][j], result)
        }
    }
    return result * result
};

1277. 统计全为1的正方形子矩阵

var countSquares = function(matrix) {
    // 与211基本一样
    //当前点所在的最大正方形的,边长的长度就是以当前点为右下角的正方形的个数
    //动态规划,当前点作为正方形的右下角
    //为0,则大小为0
    //为1,则看左边,上边,左上边的点各自最大正方形,短板原理,三者合并,三者中最短的+1就是当前点的最大正方形
    let dp = []
    let len = matrix.length
    if (len < 1return 0
    let result = 0
    for (let i = 0; i < len; i++) { //行遍历
        dp[i] = []
        let len2 = matrix[i].length
        for (let j = 0; j < len2; j++) { //列遍历
            if (i === 0 || j === 0) {
                dp[i][j] = matrix[i][j]
            } else {
                if (matrix[i][j] === 1) {
                    dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j - 1], dp[i - 1][j]) + 1
                } else {
                    dp[i][j] = 0
                }
            }
            result += dp[i][j]
        }
    }
    return result
};

338. 比特位计数

计数为是前一个偶数位加1,偶数位与二分之一一样

var countBits = function(num) {
    //动态规划
    //奇数的1的个数,是前一个最后一位变1,所以是dp[i] = dp[i-1]
    //偶数是x/2往左移一位得到的,所以与其1的个数一样
    let dp=new Array(num + 1)
    dp[0] = 0
    for(let i = 1; i <= num; i++){
        if(i % 2 === 1){
            dp[i] = dp[i - 1] + 1
        }else{
            dp[i] = dp[i / 2]
        }
    }
    return dp
};

198. 打家劫舍

var rob = function(nums) {
    //不是直接奇偶就解决了!!!
    //
    let prepre = 0,  //前前一家抢没抢的最大值
        pre = 0,  //前一家抢没抢的最大值
        cur = 0;
    for (i = 0; i < nums.length; i++) {
        cur = Math.max(prepre + nums[i], pre); //当前抢不抢的最大值 ==========>把这间想成是最后一间(如果前一个没抢,那么pre就是prepre,如果前一个抢了,就可以比大小了)
        //k-1 如果抢了,那么k不能抢,k-1没抢,那么等价于k-2的,此时k可以抢
        prepre = pre;
        pre = cur;
    }
    return cur
};

343. 整数拆分

要0-n 所有拆分的最优值,与零钱兑换有点类似

var integerBreak = function(n) {
    let max = 0
    let dp = new Array(n + 1).fill(0)
    dp[0] = 0
    dp[1] = 0
    dp[2] = 1
    for(let i = 3; i <= n; i++){
        for(let j = 1; j<=i-1;j++){  // 1 ~ i-1
            //按照前面拆过的/ 拆一次/拆一次之后剩下的部分还需要继续拆
            dp[i] = Math.max(dp[i], j * (i - j), j * dp[i - j])
        }
    }
    return dp[n]
};

股票问题系列

见详解

121. 买卖股票的最佳时机

注意:除了初始状态,每个状态都是由2部分取 max 得来

只能买一次(买入与之前的都无关,就的那个前面什么都没操作)

var maxProfit = function(prices) {
    // let dp = [] //一个存状态dp[i][0/1]  第二个状态表示是否持有,1下标存储持有
    // for(let i = 0; i < prices.length; i++){
    //     dp[i] = []
    //     if(i === 0){
    //         dp[0][1] = -prices[0]  //开始就持有,说明开始就买了
    //         dp[0][0] = 0
    //         continue
    //     }
    //     dp[i][1] = Math.max(- prices[i], dp[i - 1][1])  //如果持有的话要么当前买的要么之前买了一直持有,与之前不持有的无关
    //     dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])  
    // }
    // return dp[prices.length - 1]? dp[prices.length - 1][0] : 0 //最后持有的话,肯定不是最优的, 不如不买!!,输入为空的情况

    // //空间优化
    // let dp = [] //是否持有,只需要存前一天的即可
    // for(let i = 0; i < prices.length; i++){
    //     if(i === 0){
    //         dp[1] = -prices[0]
    //         dp[0] = 0
    //         continue
    //     }
    //     dp[1] = Math.max(- prices[i], dp[1])  
    //     dp[0] = Math.max(dp[0], dp[1] + prices[i])  
    // }
    // return dp[0] 

    //一次遍历,不是动态规划
    //遍历的过程中,记录之前出现的最低点, 当前卖出的话一定是希望之前是在最低点买入的
    let minPrice = prices[0]
    let result = 0
    for(let i = 1; i< prices.length; i++){
        minPrice = Math.min(minPrice, prices[i])
        result = Math.max(result, prices[i] - minPrice)
    }
    return result
};

122. 买卖股票的最佳时机2

买的次数任意,再次买入的时候不用重置

var maxProfit = function(prices) {
    // let dp = [] //一个存状态dp[i][0/1]  第二个状态表示是否持有
    // for(let i = 0; i < prices.length; i++){
    //     dp[i] = []
    //     if(i === 0){
    //         dp[0][1] = -prices[0]
    //         dp[0][0] = 0
    //         continue
    //     }
    //     dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]) 
    //     dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])  
    // }
    // return dp[prices.length - 1]? dp[prices.length - 1][0] : 0 //最后持有的话,肯定不是最优的, 不如不买!!,输入为空的情况
    
    //空间优化
    // let dp = [0] //当前是否持有
    // for(let i = 0; i < prices.length; i++){
    //     if(i === 0){
    //         dp[1] = - prices[0]
    //         dp[0] = 0
    //         continue
    //     }
    //     dp[1] = Math.max(dp[0] - prices[i], dp[1]) 
    //     dp[0] = Math.max(dp[0], dp[1] + prices[i])  
    // }
    // return dp[0]
    //只用2个变量 不用数组
    let dp_0 = 0 //当前是否持有
    let dp_1 = - prices[0]
    for(let i = 1; i < prices.length; i++){
        dp_1 = Math.max(dp_0 - prices[i], dp_1) 
        dp_0 = Math.max(dp_0, dp_1 + prices[i])  
    }
    return dp_0
};

188. 最多交易k次

  • 第一天初始化,每天的从未卖出过单独考虑,只记录一天的,因为后一天的只需要有前一天的最优结果即可。
  • max_k 为输入的 k 与 二分之一天数的最小值。
var maxProfit = function(k, prices) {
        //空间优化,还是只需要存上一天的就行
    if(prices.length <=1return 0
    let max_k = Math.floor(Math.min(prices.length / 2, k))
    let dp = new Array(max_k + 1) //存放每一天还能卖出次数对应的最优值
    for(let j = 0; j <= max_k; j++){
        dp[j] = []
    }
    for(let i = 0; i < prices.length; i++){
        for(let j = 0; j <= max_k; j++){
            if(i === 0){
                if(j === max_k){
                    dp[j][0] = 0 //第一天啥都每做
                    dp[j][1] = -prices[i] //第一天买入
                }else{
                    dp[j][0] = -Infinity
                    dp[j][1] = -Infinity
                }
            }else{
                if(j === max_k){ //边界单独处理
                    dp[j][0] = dp[j][0]
                    dp[j][1] = Math.max(dp[j][1], dp[j][0] - prices[i])
                }else{
                    dp[j][0] = Math.max(dp[j][0], dp[j + 1][1] + prices[i]) //上一天的未持有,上一天的持有在今天卖出了
                    dp[j][1] = Math.max(dp[j][1], dp[j][0] - prices[i]) //上一天持有,上一天未持有今天买了
                }
            }
        }
    }
    return dp.reduce((pre,cur)=>{
        return Math.max(pre, cur[0])  //最后一次一定是未持有比较好
    },0)
};