动态规划算法题


动态规划

顾名思义,动态添加可选项,每一步规划当前最优解

动态规划适用于每一步都会影响后续的情况,

或者说 每一个状态一定是由上一个状态推导出来, 某一问题有很多重叠子问题,本质是遍历

一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的

  1. 确定dp数组含义
  2. 确定递推公式
  3. 确定dp初始值
  4. 确定遍历顺序
  5. 举例推导dp数组

路径问题 背包问题 打家劫舍 股票问题 子序列问题

509. 斐波那契数

https://leetcode.cn/problems/fibonacci-number/

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

70. 爬楼梯

https://leetcode.cn/problems/climbing-stairs/

var climbStairs = function (n) {
    // 1. 确定DP数组含义,下标含义: 达到第i阶有dp[i]种方法
    // 2. 确定递推公式: dp[i] = dp[i-2] + dp[i-1]
    // 3. dp[0] = 1 (没有0台阶,n为正整数,为了满足规律,赋值个1,其实没用到)
    //    dp[1] = 1 dp[2] = 2
    if (n === 1 || n === 2) return n
    const dp = [1, 1, 2]
    for (let i = 3; i <= n; i++) {
        dp[i] = dp[i - 2] + dp[i - 1]
    }
    return dp[n]
};

746. 使用最小花费爬楼梯

https://leetcode.cn/problems/min-cost-climbing-stairs/

var minCostClimbingStairs = function(cost) {
    // 1. 确定DP数组含义,下标含义: 达到第i阶最少要dp[i]个体力
    // 2. 确定递推公式: dp[i] = dp[i-2] + const[i-2] 或
    //                dp[i] = dp[i-1] + const[i-1] 中的 minor
    // 3. 确定初始状态: dp[0] = 0 dp[1] = 0 (可以从0或1开始)
    const dp = [0,0]
    for(let i = 2; i <= cost.length; i++) {
        dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
    }
    return dp[cost.length]
};
// 优化空间复杂度,因为dp[i]就是由前两位推出来的,那么也不用dp数组了
var minCostClimbingStairs = function(cost) {
    let before = after = 0
    for(let i = 2; i <= cost.length; i++) {
        const now =  Math.min(after + cost[i-1], before + cost[i-2])
        before = after
        after = now
    }
    return after
};

62. 不同路径

https://leetcode.cn/problems/unique-paths/description/

var uniquePaths = function (m, n) {
    // dp[i][j] 到达 i j 有多少种走法,抛弃0 0,从 1 1 开始
    // dp[i][j] = dp[i-1][j] + dp[i][j-1],要走到ij,必须走到i-1 或 j-1。
    // 从[1,1] 走到 [m,n] 需要初始化第一行及第一列为1
    const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(1))
    for (let i = 2; i <= m; i++) {
        for (let j = 2; j <= n; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        }
    }

    return dp[m][n]
};

// 简化到一维,不是很懂
var uniquePaths = function (m, n) {
    const dp = new Array(n).fill(1)
    for (let j = 1; j < m; j++) {
        for (let i = 1; i < n; i++) {
            dp[i] += dp[i - 1];
        }
    }
    return dp[n - 1];
};

63. 不同路径 II

https://leetcode.cn/problems/unique-paths/solutions/856968/dai-ma-sui-xiang-lu-dong-gui-wu-bu-qu-xi-1vkb/

var uniquePathsWithObstacles = function (obstacleGrid) {
    const h = obstacleGrid.length, l = obstacleGrid[0].length
    // 初始化时,第一列 或 第一排,假设前面有障碍物,后面都是0
    // 先初始化为0,然后赋值1
    const dp = new Array(h).fill(0).map(() => new Array(l).fill(0))
    for (let i = 0; i < h && obstacleGrid[i][0] === 0; i++) dp[i][0] = 1
    for (let i = 0; i < l && obstacleGrid[0][i] === 0; i++) dp[0][i] = 1

    for (let i = 1; i < h; i++) {
        for (let j = 1; j < l; j++) {
            if (obstacleGrid[i][j]) {
                dp[i][j] = 0
                continue
            }
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        }
    }
    return dp[h - 1][l - 1]
};

343. 整数拆分

https://leetcode.cn/problems/integer-break/description/

// 首先分析题目,上一步的选择影响当前步,动态规划
var integerBreak = function (n) {
    // dp[i] 拆分 i 能得到的最大乘积,i 从 0 到 n
    // dp[i] = 遍历到j时,j是必拆。此时存在两种情况
    //         j * (i - j) 剩余的数不拆;  j * dp[i - j] 剩余的数也拆(j*剩余数拆的最大值)
    //        最后,dp[i]得是 j 遍历的循环中的最大值,所以 是三者取最大
    // dp[0]无意义 dp[1]无意义 均无法拆分 dp[2] = 1,其他得赋值0
    let dp = new Array(n + 1).fill(0) // n+1是因为得多算一个0最后才会有dp[n]
    dp[2] = 1
    for (let i = 3; i <= n; i++) {
        // 对于每个i,遍历其 先拆出j能得的最大值,遍历完j后得到当前i能拆出的最大乘积
        // 至少拆出两个数,两个数遍历到拆一半的时候肯定是最大的,
        // 进一步假设是拆出三个数,那应该遍历到1/3就行,到1/2肯定够
        // 所以j <= i / 2
        for (let j = 1; j <= i / 2; j++) {
            dp[i] = Math.max(j * (i - j), j * dp[i - j], dp[i])
        }
    }
    return dp[n]
};
// 问题来了,什么时候要一重循环,什么时候要二重循环?
// 那就是内部得多遍历一层,内部可能两种情况,三种情况,k种情况
// 这k种情况也依赖上一次的选择,总体有点像动态规划里再动态规划

96. 不同的二叉搜索树

https://leetcode.cn/problems/unique-binary-search-trees/description/


——背包问题——

  1. 是排序问题还是组合问题?(外层循环是物品i还是容量j?)

组合问题不考虑顺序,固定顺序,外层循环为 遍历物品i
排序问题考虑顺序, 外层循环为 遍历容量j

  1. 是完全背包问题,还是01背包问题?(内层循环是正序还是倒序?)

完全背包物品i数量无限,01背包物品仅存在两种状态

01背包 不重复计算当前 物品i, 内层容量j从后往前循环
完全背包 重复计算当前 物品i, 内层容量j从前往后循环

01背包、完全背包、多重背包、分组背包和混合背包

注意很多 dp[i]的定义 dp[i] i既是value又是weight

416. 分割等和子集(01背包)

https://leetcode.cn/problems/partition-equal-subset-sum/description/

698.划分为k个相等的子集
473.火柴拼正方形

var canPartition = function (nums) {
    const l = nums.length
    if(nums.length < 2) return false
    const sum = nums.reduce((pre, cur) => pre + cur, 0)
    if (sum % 2 !== 0) return false
    const capacity = sum / 2
    // dp[i][j] 代表前i个元素能否组成和为j的结果
    // [1, 5, 11, 5]
    const dp = new Array(l).fill(0).map(() => new Array(capacity).fill(false))

    // 第0项 满足 容量为nums[0]的背包
    dp[0][nums[0]] = true

    // 一步步扩大可选放入背包的物品范围,每片i轮询可理解为,i-1项中加入此i项能否满足容量j
    for (let i = 1; i < l; i++) {
        // 该层for循环,相当于遍历出了,前i项,能组合出来哪些容量。
        for (let j = 0; j <= capacity; j++) {
            // 对每新增的第i项进行容量遍历,判断 第i项 能不能放入 容量j 的背包,
            // 1. 如果 nums[i]  > 容量j 太大不能放入背包,那容量dp[i][j] 沿用之前 dp[i-1][j]的结果(可能i-1项可以满足 容量j,也算)
            // 2. 如果 nums[i] <= 容量j 那就有两种情况,
            // 情况1 第i项 放入, 那剩余的空间 j - nums[i],看看dp[i-1]中是否能恰好填充,即dp[i-1][j - nums[i]]是否为true
            // 情况2 第i项 不放入, 那沿用 dp[i-1][j] 的结果(前i-1项能满足和为容量j也行)
            // 二者有一个成功即为true
            // 在遍历的过程中,已经考虑了背包容量 为 0 到 j 的所有情况
            if (nums[i] > j) {
                dp[i][j] = dp[i - 1][j]
            } else if (nums[i] < j) {
                // 也可理解为,i-1项能满足容量j也行,i-1项不能满足容量j的话就把 第i项 放入背包,看看i-1项,能不能满足剩余空间。
                dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]
            } else {
                dp[i][j] = true
            }
        }
        console.log(i,nums[i],dp[i])
    }
    // dp[l-1][capacity] 代表 用上nums所有元素(l-1),能否组合出容量capacity
    return dp[l - 1][capacity]
};

canPartition([1,5,11,5])
// 滚动数组,由于每次的数据只依赖于上一层i-1的数据,所以可以直接在当前一维数组比
var canPartition = function(nums) {
    const sum = (nums.reduce((p, v) => p + v));
    if (sum & 1) return false;
    const dp = Array(sum / 2 + 1).fill(0);
    for(let i = 0; i < nums.length; i++) {
        // 容量j >= nums[i]代表当前i在这个容量都是有选择的,可放进去的
        for(let j = sum / 2; j >= nums[i]; j--) {
            // 假设 [1,5,11,5]
            // 第一轮就是 1 能放进容量 11,并与0比谁大。一轮下来就是[0,1,1,1,...]
            // 第二轮就是 5 能放进容量 11 到 0 的话, value加上 并且剩余质量能否补上,并与1比大
            dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            if (dp[j] === sum / 2) {
                return true;
            }
        }
    }
    return dp[sum / 2] === sum / 2;
};

canPartition([1,5,11,5])
0 1  [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
1 5  [ 0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 6 ]
2 11 [ 0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 11 ]
3 5  [ 0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 10, 11 ]

1049. 最后一块石头的重量 II(01背包)

https://leetcode.cn/problems/last-stone-weight-ii/description/

// 前面的选择影响后面的选择,动态规划
var lastStoneWeightII = function (stones) {
    // 这题本质是找到尽可能接近 sum/2 的组,也即 背包的 value/容量 最大 为 sum/2。
    // dp[j]: 前j项最大价值为dp[j]
    // 总容量j 减去当前j的容量,剩余的容量的价值, 加上当前j的价值;与上一次ap[j]比出最大值
    // dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    const l = stones.length
    if (l < 2) return stones[0]
    const sum = stones.reduce((p, c) => p + c)
    const maxCapacity = Math.ceil(sum / 2)
    const dp = new Array(maxCapacity + 1).fill(0)
    for (let i = 0; i < l; i++) {
        // 在前几次的遍历中,后面的j基本无意义,因为肯定会被覆盖,后续遍历中i物品变多,更能撑满背包
        for (let j = maxCapacity; j >= stones[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i])
        }
        // console.log(i,stones[i],dp)
    }
    return Math.abs(dp[maxCapacity] * 2 - sum)
};

lastStoneWeightII([2,7,4,1,8,1])

494. 目标和(01背包,组合)

https://leetcode.cn/problems/target-sum/

const findTargetSumWays = (nums, target) => {
    // 本质是 将数组分成两个集合,其中一个集合比另一个集合多result
    // 那就是 多的那个集合x = (sum + result) / 2,要想办法把这个集合放满有多少种方法
    
    const sum = nums.reduce((p, c) => p + c);
    if (Math.abs(target) > sum) return 0
    if ((target + sum) % 2) return 0
    // 正数集合,和必须为maxCapacity
    const maxCapacity = (target + sum) / 2;
    // console.log('sum', sum)
    // console.log('maxCapacity', maxCapacity)

    let dp = new Array(maxCapacity + 1).fill(0);
    dp[0] = 1;

    for (let i = 0; i < nums.length; i++) {
        // 相当于从前往后遍历i,前几轮遍历相当于看i能满足哪个容量
        // 后面的遍历,可以满足的容量更多,直至i = 4,
        for (let j = maxCapacity; j >= nums[i]; j--) {
            // 每新一轮j的遍历,都在上一轮能达到容量j的基础上 dp[j],
            // + 假设j项已入集合,将剩余空间(j - nums[i]) 补满有多少种次数 dp[j - nums[i]]
            dp[j] = dp[j] + dp[j - nums[i]];
        }
        // console.log(i, nums[i], dp)
    }

    return dp[maxCapacity];
};

findTargetSumWays([2, 1, 2, 3, 5], 3)

// sum 13
// maxCapacity 8 正数集合和必须为8
// 0 2 [ 1, 0, 1, 0, 0, 0, 0, 0, 0 ]
// 1 1 [ 1, 1, 1, 1, 0, 0, 0, 0, 0 ]
// 2 2 [ 1, 1, 2, 2, 1, 1, 0, 0, 0 ]
// 3 3 [ 1, 1, 2, 3, 2, 3, 2, 1, 1 ]
// 4 5 [ 1, 1, 2, 3, 2, 4, 3, 3, 4 ]

474. 一和零

https://leetcode.cn/problems/ones-and-zeroes/description/

// 物品的重量有了两个维度的01背包问题
var findMaxForm = function (strs, m, n) {

    // dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
    const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0))
    // dp[i][j] = Math.max(dp[i][j], dp[i- zeroNum][j- oneNum]+1)

    let zero = one = 0
    // 老规矩循环物品
    // 物品一个维度,物品的重量两个维度,需要三重循环
    for (let str of strs) {
        for(let c of str) {
            if(c==='0')zero++
            else one++
        }

        // 内层其实是滚动数组,滚动数组为了保证每个物品只被添加一次,需要倒叙遍历,这里两个都要倒序遍历
        for (let i = m; i >= zero; i--) {
            for (let j = n; j >= one; j--) {
                // ij不放进去 就继承dp[i][j], ij放进去就计算剩余的位置放满要多少dp[i - zero][j - one]
                dp[i][j] = Math.max(dp[i][j], dp[i - zero][j - one] + 1)
            }
        }
    }

    return dp[m][n]
};

为什么滚动数组需要倒叙遍历?

因为滚动数组复用需要i-1轮遍历的结果,

假设从后往前做遍历,我们在做第i轮循环的时候,先计算 容量j 较大的数据,此数据会依赖 i-1 轮的遍历结果

前面还依旧保留者 i-1次遍历的dp数据,以供第i轮使用。

假设从前往后做遍历,可以预见 容量j 较小的dp数据会先被覆盖成为 第i轮的数据,使得 容量j较大的数据无法复用 i-1轮的结果,

或者说这样遍历,导致每轮i种,每个容量j 都基于了新的当前i去计算(视为有无数个i物品,也即完全背包问题) 

以01背包的思路 甚至可以认为,这样从前往后遍历,已经不仅仅有i层,而是 i*j 层。因为每个 j-1 其实都已经被覆盖为了第j层的数据

——– 完全背包 ——–

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

01背包和完全背包唯一不同就是体现在遍历顺序上

01背包滚动数组是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历

518. 零钱兑换 II

https://leetcode.cn/problems/coin-change-ii/description/

var change = function (amount, coins) {

    // dp[i] 前i项能组成的总金额组合数
    const dp = new Array(amount + 1).fill(0)
    dp[0] = 1
    // 上一轮dp[i]有多少种方法 + 这轮剩余空间有多少种方法
    // dp[i] = dp[i] + dp[j-coins[i]]
    for (let i = 0; i < coins.length; i++) {
        for (let j = coins[i]; j <= amount; j++) {
            dp[j] = dp[j] + dp[j - coins[i]]
        }
    }

    return dp[amount]
};

——– 排列和组合 ——–

这种遍历顺序中dp[j]里计算的是组合数,因为物品放入的顺序固定,不存在重复计算

for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

// [1,2,5] 5
0 1 [ 1, 1, 1, 1, 1, 1 ]
1 2 [ 1, 1, 2, 2, 3, 3 ]
2 5 [ 1, 1, 2, 2, 3, 4 ]

这种遍历顺序中dp[j]里算出来的就是排列数!

dp[j] = dp[j] + dp[j - coins[i]] 的过程就好像先插入当前i,再放置dp[j - coins[i]]的

var change = function (amount, coins) {

    const dp = new Array(amount + 1).fill(0)
    dp[0] = 1
    for (let j = 0; j <= amount; j++) { // 1.遍历背包容量
        // 针对每一个容量j,遍历i
        for (let i = 0; i < coins.length; i++) { // 2.遍历物品
            if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
        }
        console.log(j,dp)
    }
    return dp[amount]
};

// [1,2,5] 5
//  0  1  2  3  4  5
0 [ 1, 0, 0, 0, 0, 0 ]
1 [ 1, 1, 0, 0, 0, 0 ]
2 [ 1, 1, 2, 0, 0, 0 ]
3 [ 1, 1, 2, 3, 0, 0 ]
4 [ 1, 1, 2, 3, 5, 0 ]
5 [ 1, 1, 2, 3, 5, 9 ]

377. 组合总和 Ⅳ

https://leetcode.cn/problems/combination-sum-iv/description/

var combinationSum4 = function(nums, target) {
    // dp[i] 前i项和为i的排列个数
    const dp = new Array(target + 1).fill(0)
    dp[0] = 1 // 0项组成0有一种方式
    // dp[i] = dp[i] + dp[i - nums[i]]
    for(let i = 0; i <= target; i++) {
        for(let j = 0; j < nums.length; j++) {
            if(i >= nums[j]) dp[i] = dp[i] + dp[i - nums[j]]
        }
    }
    return dp[target]
};

// [1,2,3] 4
0 [ 1, 0, 0, 0, 0 ]
1 [ 1, 1, 0, 0, 0 ]
2 [ 1, 1, 2, 0, 0 ]
3 [ 1, 1, 2, 4, 0 ]
4 [ 1, 1, 2, 4, 7 ]

70. 爬楼梯(每次任意级台阶)

https://leetcode.cn/problems/climbing-stairs/description/

题目修改为每次可走任意级台阶m

var climbStairs = function(n) {
    const dp = new Array(n + 1).fill(0);
    const m = 2;
    dp[0] = 1;
    // 每次走j级台阶存在先后顺序,外层遍历
    for(let i = 1; i <= n; i++){
        for(let j = 1; j <= m; j++){
            if(i >= j) {
	    	dp[i] += dp[i - j];
	     }
        }
    }
    return dp[n];
};

322. 零钱兑换

https://leetcode.cn/problems/coin-change/description/

var coinChange = function (coins, amount) {

    // i既是value又是weight,要求weight和为amount
    // dp[i] ,组成i需要至少dp[i]个硬币
    // 必须初始化为最大的数
    const dp = new Array(amount + 1).fill(Number.POSITIVE_INFINITY)
    dp[0] = 0
    for (let i = 0; i < coins.length; i++) {
        for (let j = coins[i]; j <= amount; j++) {
            dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1)
        }
        // console.log(i,coins[i],dp)
    }
    return dp[amount] === Number.POSITIVE_INFINITY ? -1 : dp[amount]
};

coinChange([1,2,5], 11)

0 1 [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ]
1 2 [ 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5,  6  ]
2 5 [ 0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 2,  3  ]

279. 完全平方数

https://leetcode.cn/problems/perfect-squares/description/

var numSquares = function(n) {
    // 前一选择影响后一步的选择容量等于价值
    // 是组合,外层遍历 物品i
    // 是完全背包,内层正序遍历 容量j
    // dp[i]: 和为i的最少完全平方数的数量
    // dp[i] = Math.min(dp[i], dp[j - Math.pow(i,2)])
    const dp = new Array(n+1).fill(Number.POSITIVE_INFINITY)
    dp[0] = 0
    for(let i = 1; i <= Math.sqrt(n); i++) {
        const weight = Math.pow(i,2)
        for(let j = weight; j <= n; j++) {
            dp[j] = Math.min(dp[j], dp[j - weight] + 1)
        }

    }
    return dp[n]
};

139. 单词拆分

https://leetcode.cn/problems/word-break/description/

var wordBreak = function (s, wordDict) {
    // 是排列,外层循环 重量j
    // 是完全背包,内层正序循环
    // dp[i] 为true,代表 前s[i] 可以被 wordDict表示
    const dp = new Array(s.length + 1).fill(false)
    dp[0] = true

    // 前s[i]
    for (let i = 1; i <= s.length; i++) {
        const si = s.slice(0, i)
        // 从前向后找,每s[i]遍历wordDict,看能不能实现前s[i]
        for (let j = 0; j < wordDict.length; j++) {
            // 以前到这个容量行,不然就把当前word放进末尾,在看前i是否为true
            const length = wordDict[j].length
            dp[i] = dp[i] || i >= length && s.slice(i - length, i) === wordDict[j] && dp[i - length]
        }
    }
    return dp[s.length]
};

——– 多重背包 ——–

多重背包 就是 完全背包但数量有限,其按数量摊开可视为01背包

解法为在01背包的基础上 再加一层循环,针对 每个物品i的数量进行循环

function testMultiPack() {
    const bagSize = 10;
    const weightArr = [1, 3, 4], valueArr = [15, 20, 30], amountArr = [2, 3, 2];
    const goodsNum = weightArr.length;
    const dp = new Array(bagSize + 1).fill(0);
    // 遍历物品
    for (let i = 0; i < goodsNum; i++) {
        // 遍历物品个数
        for (let j = 0; j < amountArr[i]; j++) {
            // 遍历背包容量
            for (let k = bagSize; k >= weightArr[i]; k--) {
                dp[k] = Math.max(dp[k], dp[k - weightArr[i]] + valueArr[i]);
            }
            console.log(i,j,valueArr[i],dp);
        }
    }
}
testMultiPack();

0 0 15 [ 0, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15 ]
0 1 15 [ 0, 15, 30, 30, 30, 30, 30, 30, 30, 30, 30 ]
1 0 20 [ 0, 15, 30, 30, 35, 50, 50, 50, 50, 50, 50 ]
1 1 20 [ 0, 15, 30, 30, 35, 50, 50, 55, 70, 70, 70 ]
1 2 20 [ 0, 15, 30, 30, 35, 50, 50, 55, 70, 70, 75 ]
2 0 30 [ 0, 15, 30, 30, 35, 50, 60, 60, 70, 80, 80 ]
2 1 30 [ 0, 15, 30, 30, 35, 50, 60, 60, 70, 80, 90 ]

代码随想录递推公式总结篇

https://programmercarl.com/%E8%83%8C%E5%8C%85%E6%80%BB%E7%BB%93%E7%AF%87.html#%E8%83%8C%E5%8C%85%E9%80%92%E6%8E%A8%E5%85%AC%E5%BC%8F

198.打家劫舍

https://leetcode.cn/problems/house-robber/description/

var rob = function(nums) {
    // dp[i] 前i最多打到dp[i]
    // 如果不偷i, dp[i] = dp[i - 1]
    // 如果偷i,  dp[i] = dp[i - 2] + nums[i] 
    // 从下标2开始遍历, 所以要确定 0 1
    // dp[0] 就是nums[0], dp[1]是max(nums[0], nums[1])

    const dp = [nums[0], Math.max(nums[0], nums[1])];
    for (let i = 2; i < nums.length; i++) {
        dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
    }
    return dp[nums.length - 1];
};

rob(
[ 2, 7,  9,  3,  1 ]
)
[ 2, 7, 11, 11, 12 ]

213.打家劫舍II

https://leetcode.cn/problems/house-robber-ii/description/

var rob = function (nums) {
    // 多了一层选首不能选尾,选尾不能选首,也即多一个max
    if (nums.length === 1) return nums[0]
    const front = robRange(nums, 0, nums.length - 2)
    const end = robRange(nums, 1, nums.length - 1)
    return Math.max(front, end)
};

// s -> startIndex  e -> endIndex
const robRange = (nums, s, e) => {
    if (s === e) return nums[s]
    // 这里必须nums.length创建array,因为dp得和nums的i对应上
    const dp = Array(nums.length).fill(0)
    dp[s] = nums[s]
    dp[s + 1] = Math.max(nums[s], nums[s + 1])
    for (let i = s + 2; i <= e; i++) {
        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i])
    }
    // console.log(dp);
    return dp[e]
}
rob(
[ 2, 3, 2 ]
)
[ 2, 3, 0 ]
[ 0, 3, 3 ]

337. 打家劫舍 III

https://leetcode.cn/problems/house-robber-iii/description/

const rob = root => {
    // 后序遍历函数
    const postOrder = node => {
        // 递归出口
        if (!node) return [0, 0];
        // 遍历左子树
        const left = postOrder(node.left);
        // 遍历右子树
        const right = postOrder(node.right);
        // 不偷当前节点,左右子节点都可以偷或不偷,取最大值
        const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        // 偷当前节点,左右子节点只能不偷
        const Do = node.val + left[0] + right[0];
        // [不偷,偷]
        return [DoNot, Do];
    };
    const res = postOrder(root);
    // 返回最大值
    return Math.max(...res);
};

——买卖股票——

121.买卖股票的最佳时机

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/

var maxProfit = function (prices) {
    // 只能买卖一次,假设昨天是卖出的状态,那今天再买入就只有-prices[i],
    const l = prices.length
    const dp = new Array(l).fill(0).map(() => new Array(2).fill(0))

    dp[0][1] = -prices[0]
    dp[0][0] = 0

    for (let i = 1; i < l; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], -prices[i])
    }
    return dp[l - 1][0]
};

// maxProfit([7, 1, 5, 3, 6, 4])
// dp: [[-7, 0], [-1, 0], [-1, 4], [-1, 4], [-1, 5], [-1, 5]]
var maxProfit = function (prices) {
    let hold = -prices[0]
    let not = 0
    for(let i = 1; i < prices.length; i++) {
        not = Math.max(not, hold + prices[i])
        hold = Math.max(hold, -prices[i])
    }
    return not
};

动态规划 本质上是遍历了所有情况,但是由于动态规划对前期每一步的情况,都进行了储存,

使得后续的遍历,减少了重复的计算

就是每一步状态都由上一步推导出来,动态规划,就是每走一步都进行规划,进行计算。

区分于贪心,贪心没有状态推导,而是从局部直接选最优的,

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

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

var maxProfit = function (prices) {
    // dp[i] 前i天能获取的最大利润
    // 对于遍历到的当前i,可以选择
    // 不持有i, dp[i][0] = dp[i-1][0],dp[i-1][1] + prices[i]
    // 持有i, dp[i][1] = dp[i-1][1],dp[i-1][0] - prices[i]

    // const dp = new Array(prices.length).fill(0).map(() => new Array(2).fill(0))
    // dp[0][0] = 0
    // dp[0][1] = -prices[0]

    let dontHold = 0
    let hold = -prices[0]
    for(let i = 1; i < prices.length; i++) {
        // dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i])
        // dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i])
        dontHold = Math.max(dontHold, hold + prices[i])
        hold = Math.max(hold, dontHold - prices[i])
    }
    return dontHold
};

console.log(maxProfit([7, 1, 5, 3, 6, 4]))

123. 买卖股票的最佳时机 III

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/

const maxProfit = prices => {
    const len = prices.length;
    const dp = new Array(len).fill(0).map(x => new Array(5).fill(0));
    dp[0][1] = -prices[0];
    dp[0][3] = -prices[0];
    for (let i = 1; i < len; i++) {
        dp[i][0] = dp[i - 1][0];
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); // 保持第一次持有,或从第一次不持有到第一次持有
        dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]); // 保持第一次不持有,或从第一次持有到第一次不持有
        dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]); // 保持第二次持有,或从第一次不持有到第二次持有
        dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]); // 保持第二次不持有,或从第二次持有到第二次不持有
    }
    // 第一次卖出和第二次卖出,并不需要状态判断,前期 第一次和第二次 可视为同线操作。
    // 只需要一次买卖的情况,dp[l-1][4] 和 dp[l-1][2] 是相等的
    return dp[len - 1][4];
};
// maxProfit([2,1,4,5,2,9,7])
const maxProfit = prices => {
    const len = prices.length;
    const dp = new Array(5).fill(0);
    dp[1] = -prices[0];
    dp[3] = -prices[0];
    for (let i = 1; i < len; i++) {
        dp[1] = Math.max(dp[1], dp[0] - prices[i]);
        dp[2] = Math.max(dp[2], dp[1] + prices[i]);
        dp[3] = Math.max(dp[3], dp[2] - prices[i]);
        dp[4] = Math.max(dp[4], dp[3] + prices[i]);
    }
    return dp[4];
};

188. 买卖股票的最佳时机 IV

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/

var maxProfit = function (k, prices) {

    const l = prices.length
    const status = 2 * k + 1
    const dp = new Array(status).fill(0)
    // 所有持有初始为 - prices[0]
    for (let k = 1; k < status; k += 2) dp[k] = - prices[0]
    for (let i = 0; i < prices.length; i++) {
        // 有点顺应排列,从前往后遍历的感觉
        for (let j = 1; j < status; j++) {
            const holdOrNot = j % 2 === 1 ? (dp[j - 1] - prices[i]) : (dp[j - 1] + prices[i])
            dp[j] = Math.max(dp[j], holdOrNot)
        }
    }
    return dp[status - 1]
};
maxProfit([3, 2, 6, 5, 0, 3])

309. 买卖股票的最佳时机含冷冻期

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/


var maxProfit = function (prices) {
    const l = prices.length
    const dp = new Array(l).fill(0).map(() => new Array(4).fill(0))
    dp[0][0] = -prices[0] // 持有
    // 0持有 = 前一天是, 持有 或 当天买入(保持不持有) 或 当天买入(冷静期)
    // 1保持不持有 = 前一天是, 保持不持有 或 冷静期
    // 2当天卖出 = 前一天是, 持有
    // 3冷静期 = 前一天是, 当天卖出
    for (let i = 1; i < l; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][1], dp[i - 1][3]) - prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3])
        dp[i][2] = dp[i - 1][0] + prices[i]
        dp[i][3] = dp[i - 1][2]
    }
    return Math.max(dp[l - 1][1], dp[l - 1][2], dp[l - 1][3])
};

714. 买卖股票的最佳时机含手续费

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/

var maxProfit = function (prices, fee) {
    let hold = -prices[0] - fee
    let not = 0
    for (let i = 1; i < prices.length; i++) {
        hold = Math.max(hold, not - prices[i] - fee)
        not = Math.max(not, hold + prices[i])
    }
    return not
};
var maxProfit = function (prices, fee) {
    const l = prices.length
    const dp = new Array(l).fill(0).map(() => new Array(2).fill(0))

    dp[0][0] = 0
    dp[0][1] = -prices[0] - fee
    for (let i = 1; i < l; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee)
    }

    return dp[l - 1][0]
};

300. 最长递增子序列

https://leetcode.cn/problems/longest-increasing-subsequence/submissions/476442040/

const lengthOfLIS = (nums) => {
    // dp[i] 以dp[i]结尾 的最长严格递增子序列长度
    // dp[i] = 前i-1中 满足尾数小于当前数 num[j] < num[i] 中的最大值+1

    let dp = Array(nums.length).fill(1);
    let result = 1;

    for (let i = 1; i < nums.length; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        result = Math.max(result, dp[i]);
    }

    return result;
};

lengthOfLIS([1, 3, 6, 7, 9, 4, 10, 5, 6])
// [ 1, 2, 3, 4, 5, 3, 6, 4, 5 ]

674. 最长连续递增序列

https://leetcode.cn/problems/longest-continuous-increasing-subsequence/description/

1var findLengthOfLCIS = function (nums) {
    // 要求连续,那就是以i-1为基础计算i
    let pre = 1, max = 0
    for (let i = 1; i < nums.length; i++) {
        const cur = nums[i - 1] < nums[i] ? pre + 1 : 1
        max = Math.max(max, cur)
        pre = cur
    }
    return max
};

718. 最长重复子数组

https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/

const findLength = (nums1, nums2) => {
    const [m, n] = [nums1.length, nums2.length];
    const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));
    let res = 0;
    // 用二维数组记录所有比较情况
    // dp[i][j] 表示 nums1 前 i 个元素和 nums2 前 j 个元素的公共的、长度最长的子数组的长度
    // 递推公式需要用到i-1,所以要初始化0位置,并从1位置开始
    for (let i = 1; i <= m; i++) {
        // 每放入一个nums1[i],遍历nums2[j],找有没有跟自己相等的,有就是当前dp+1
        for (let j = 1; j <= n; j++) {
            if (nums1[i - 1] === nums2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            }
            res = dp[i][j] > res ? dp[i][j] : res;
        }
    }
    return res;
};

const findLength2 = (nums1, nums2) => {
    const [m, n] = [nums1.length, nums2.length];
    const dp = new Array(m + 1).fill(0)
    let res = 0;
    // 由于每次都只需要用到i-1层,可使用滚动数组
    // 由于是组合问题,需要内层倒序
    for (let i = 1; i <= m; i++) {
        for (let j = n; j > 0; j--) {
            dp[j] = nums1[i - 1] === nums2[j - 1] ? dp[j - 1] + 1 : 0
            res = Math.max(res, dp[j])
        }
        console.log(dp)
    }
    return res;
};

findLength2([1, 2, 3, 2, 1], [3, 2, 1, 4, 7])

       3, 2, 1, 4, 7
  [ 0, 0, 0, 1, 0, 0 ]
1 [ 0, 0, 0, 1, 0, 0 ]
2 [ 0, 0, 1, 0, 0, 0 ]
3 [ 0, 1, 0, 0, 0, 0 ]
2 [ 0, 0, 2, 0, 0, 0 ]
1 [ 0, 0, 0, 3, 0, 0 ]

1143. 最长公共子序列

https://leetcode.cn/problems/longest-common-subsequence/description/

const longestCommonSubsequence = (text1, text2) => {
    // dp[i][j] text1中的前i 和 text2中的前j 的公共子序列最长长度
    let dp = Array.from(Array(text1.length+1), () => Array(text2.length+1).fill(0));

    for(let i = 1; i <= text1.length; i++) {
        for(let j = 1; j <= text2.length; j++) {
            if(text1[i-1] === text2[j-1]) {
                dp[i][j] = dp[i-1][j-1] +1;;
            } else {
                // 如果不相等,那就通过i延伸,或者通过j延伸
                dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
            }
        }
    }
    return dp[text1.length][text2.length];
};
longestCommonSubsequence("abcde","ace")
[
       a  c  e
  [ 0, 0, 0, 0 ],
a [ 0, 1, 1, 1 ],
b [ 0, 1, 1, 1 ],
c [ 0, 1, 2, 2 ],
d [ 0, 1, 2, 2 ],
e [ 0, 1, 2, 3 ]
]

1035.不相交的线

https://leetcode.cn/problems/uncrossed-lines/description/

var maxUncrossedLines = function (nums1, nums2) {
    // 找相同元素连线,而线不相交
    // 也即,在两个数组间,找相同顺序的子序列,也即最长公共子序列
    const dp = new Array(nums1.length + 1).fill(0).map(() => new Array(nums2.length + 1).fill(0))
    for(let i = 1; i <= nums1.length; i++) {
        for(let j = 1; j <= nums2.length; j++) {
            // 考虑顺序,也即排列,每次的计算都基于当前轮的计算,最新j的排列
        if(nums1[i-1] === nums2[j-1]) {
            dp[i][j] = dp[i-1][j-1] + 1
        } else {
            // 如果不相等,就考虑采用i更多,还是采用j更多,双重动态规划
            dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1])
        }
        }
    }
    return dp[nums1.length][nums2.length]
};

53. 最大子序和

https://leetcode.cn/problems/maximum-subarray/

var maxSubArray = function (nums) {
    // 连续:基于上一个计算,并且是排列
    let pre = max = Number.NEGATIVE_INFINITY
    // 如果pre + nums[i] 还没nums[i]大,那nums[i]就是新的头
    for (const item of nums) {
        pre = Math.max(item, pre + item)
        max = Math.max(max, pre)
    }
    return max
};
// 标准动态规划
const maxSubArray = nums => {
    const len = nums.length;
    let dp = new Array(len).fill(0);
    dp[0] = nums[0];
    let max = dp[0];
    for (let i = 1; i < len; i++) {
        dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
        max = Math.max(max, dp[i]);
    }
    return max;
};

392.判断子序列

https://leetcode.cn/problems/is-subsequence/

const isSubsequence = (s, t) => {
    // 1143.最长公共子序列的递推公式基本一样,区别是本题如果删元素一定是字符串t
    // dp[i][j] = dp[i-1][j-1] + 1
    // dp[i][j] = dp[i][j-1]
    const dp = new Array(s.length + 1).fill(0).map(() => new Array(t.length + 1).fill(0))
    for (let i = 1; i <= s.length; i++) {
        // 针对每个i遍历j,找有没有相等的,有相等的就在上一轮的基础+1
        // 如果存在i遍历j,都没找到,则都是0.则后续也全为0
        for (let j = 1; j <= t.length; j++) {
            if (s[i - 1] === t[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1
            } else {
                dp[i][j] = dp[i][j - 1]
            }
        }
    }
    return dp[s.length][t.length] === s.length
};
// 每次都不是 只需要上一层,或者说左上角的数据,所以不可以用滚动数组优化
// dp[i-1] 和 dp[i] 都要用到
isSubsequence("abc", "ahbgdc")

[
    [0, 0, 0, 0, 0, 0, 0],
    a[0, 1, 1, 1, 1, 1, 1],
    b[0, 0, 0, 2, 2, 2, 2],
    c[0, 0, 0, 0, 0, 0, 3]
]
// 其实只要按顺序能在t中找到所有s就行,遍历s,在t中按顺序找
var isSubsequence = function(s, t) {
    let i = j = 0
    for(; j < t.length; j++) s[i] === t[j] && i++
    return i === s.length
};

115.不同的子序列

var numDistinct = function (s, t) {

    // dp[i][j] i中出现了j多少次
    // s[i-1] === t[j-1]
    // dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] 代表继承数量 + 前面匹配上的次数
    // dp[i][j] = dp[i - 1][j]  代表 新进的j和i不匹配,数量不加减
    // 不能用滚动数组化简,因为需要用到当前层前面的j
    const dp = new Array(s.length + 1).fill(0).map(() => new Array(t.length + 1).fill(0))

    // s中找 t长度为0,初始化为1
    for (let i = 0; i <= s.length; i++) {
        dp[i][0] = 1;
    }
    for (let i = 1; i <= s.length; i++) {
        for (let j = 1; j <= t.length; j++) {
            if (s[i - 1] === t[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
            } else {
                dp[i][j] = dp[i - 1][j]
            }
        }
    }
    console.log(dp)
    return dp[s.length][t.length]
};

numDistinct("rabbbit","rabbit")
[
          r, a, b, b, i, t 
     [ 1, 0, 0, 0, 0, 0, 0 ],
    r[ 1, 1, 0, 0, 0, 0, 0 ],
    a[ 1, 1, 1, 0, 0, 0, 0 ],
    b[ 1, 1, 1, 1, 0, 0, 0 ],
    b[ 1, 1, 1, 2, 1, 0, 0 ],
    b[ 1, 1, 1, 3, 3, 0, 0 ],
    i[ 1, 1, 1, 3, 3, 3, 0 ],
    t[ 1, 1, 1, 3, 3, 3, 3 ]
]

583. 两个字符串的删除操作

72. 编辑距离

647. 回文子串

516.最长回文子序列


文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录