LeetCode刷题之动态规划,从易到难思路总结(JS实现)

动态规划的核心思想是将问题拆分成为一个个子问题,记录下子问题的解,达到减少重复计算的目的。

写题解时我们需要考虑是1.状态转移方程 2.边界

其中难点是:如何拆分合适的子问题,以及dp数组保存值的含义是什么?

70. 爬楼梯

子问题:爬到第 i 级台阶的方案数

从如何来到第i个状态考虑:爬到第 i 级台阶的方案数是爬到第 i−1 级台阶的方案数和爬到第 i−2 级台阶的方案数的和,因为可能是爬了一级来到第 i 级,或者是爬了两级来到第 i 级。

那么状态转移方程就是:f(x) = f(x-1) + f(x-2)

边界条件:第一个台阶只能有一种方案,即dp[1] =1;考虑dp[0],因为dp[2] = dp[1] + dp[0],不妨就令dp[0] = 1;

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {

    if(n===1) return 1;
    dp = [];
    dp[0]=1;
    dp[1]=1;
    
    for (let i=2; i<=n; i++){
        //由第i-1级爬了一级 或 由第i-2级爬了两级
        dp[i] = dp[i-1] + dp[i-2];
    }
    
    return dp[n];
};

时间复杂度和空间复杂度都是O(n) 

由于这里每一个状态只与前一个和前前一个状态有关,我们可以将dp数组的空间节省为两个变量p,q,分别滚动记录dp[i-1]和dp[i-2],这样空间复杂度就变成了O(1)

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {

    res = 1;
    p = 0;
    q = 0;
    for (let i=1; i<=n; i++){
        p = q;
        q = res;
        res = p + q;
    }
    
    return res;
};

413. 等差数列划分

一般来说子序列问题划分的子问题通常是“以i结尾的序列” 

从如何来到第i个状态考虑:

如果nums[i]不与nums[i-1]和nums[i-2]构成等差数列,没有以nums[i]结尾的连续等差数列;

否则,以nums[i]结尾的等差数列必定能以nums[i-1]结尾,且多了一个 nums[i-2],nums[i-1],nums[i] 的等差数列

状态转移方程:f(x) = f(x-1) + 1, if x-2,x-1,x构成等差数列

这里需要说明的是定义dp[i]的含义:以nums[i]结尾的连续等差数列的个数。

为什么强调结尾,是因为最终要给出的是:所有为等差数列的子数组,需要将各个结尾的等差数列的个数相加,即sum(dp[2...n-1])

边界条件:由于等差数列长度至少为3,所以从下标为2开始遍历,dp[0]=0,dp[1]=0;

/**
 * @param {number[]} nums
 * @return {number}
 */
var numberOfArithmeticSlices = function(nums) {
    res = 0;
    dp = [];
    dp[0] = 0;
    dp[1] = 0;

    for ( let i=2; i<nums.length; i++) {
        if (nums[i]-nums[i-1] === nums[i-1]-nums[i-2]) {
            dp[i] = dp[i-1] + 1;
            res += dp[i];
        } else {
            dp[i] = 0;
        }
    }

    return res;
};

 322. 零钱兑换

很典型的背包问题。

子问题:凑成 i 金额的最少硬币数

从如何来到第i个状态考虑:对于 i 金额的硬币数量,第 j 个硬币,如果选择它硬币数量可以从金额为 i - coins[j] 的数量 + 1 得到,如果不选择它硬币数量保持上一个状态(j-1),比较两者之间的较小值更新dp[i]

状态转移方程:dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)

边界条件:初始化dp数组为最大值

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function(coins, amount) {
    let dp = new Array( amount + 1 ).fill( amount + 1 );
    dp[0] = 0;
    for (let i = 1; i <= amount; ++i) {
            for (let j = 0; j < coins.length; ++j) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
};

518. 零钱兑换 II

选取硬币没有顺序,即1 2与2 1是一种方案,所以本题求解的是组合数 (与排列数相对)

子问题:凑成 i 金额的方案数(方案需要考虑选取硬币的顺序,最大金额不用考虑)

从如何来到第i个状态考虑:为了保证硬币的选取是有顺序的,需要把遍历硬币数组的循环作为外循环(如果作为内循环,会出现方案的重复计数),把金额增长作为内循环。那么可以选择到第 j 个硬币时,凑成 i 金额的方案就是上一个状态(j-1)加上凑成金额 i - coin[j] 的方案数。

状态转移方程:dp[i] = dp[i] + dp[i - coins[j]]

边界条件:dp[0] = 1; 内循环金额从coin[j]开始,即能加入第 j 个硬币的金额开始

/**
 * @param {number} amount
 * @param {number[]} coins
 * @return {number}
 */
var change = function(amount, coins) {

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

    for (const coin of coins) {
        for (let i=coin; i<=amount; i++){
            dp[i] += dp[i-coin];

        }
    }
    return dp[amount];
};

如果内外循环交换,得出的解就是排列数

到这里可能会有些糊涂,因为这里dp数组都是简化为一维的,所以顺序是至关重要的。

回到二维的dp数组分析一下:

// k代表硬币,i代表金额数

if 金额数大于硬币
    DP[k][i] = DP[k-1][i] + DP[k][i-k] // 选择到第k-1个硬币时的方案 + 有了第k个硬币的方案
else
    DP[k][i] = DP[k-1][i] //没有选择第k个硬币,仍然是选择到第k-1个硬币时的方案

62. 不同路径

这是一个矩阵的动态规划,子问题就是到达某一格子的方案

从如何到达某一格子考虑 :只有两种情况就是从上面一个格子来或者从左边一个格子来,那么状态转移方程很明显就是:dp[i][j] = dp[i-1][j] + dp[i][j-1]

边界条件:第一行和第一列总是只有直线一个走法,所以初始化为1

/**
 * @param {number} m
 * @param {number} n
 * @return {number}
 */
var uniquePaths = function(m, n) {
    var dp = [];
    // 第一行初始化1,因为只有直线一种走法
    dp[1] = new Array(n+1).fill(1);
    for(let i = 2; i <= m; i++){
        for(let j = 1; j <= n; j++){
            // 第一列也初始化1,同样因为只有直线一种走法
            if (j === 1) {
                dp[i] = new Array(n+1).fill(1);
            }
            else {
                // 对于某一个格子,走法为其左+其上
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
    }
    return dp[m][n];
    

};

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

删除最少字符把两个字符串变成相同字符串,可以转化问题为,求解两个字符串的最长公共子序列。

那么子问题是:以i结尾的字符串word1 和 以j结尾的字符串word2 的最长公共子序列

从如何来到第i,j个状态考虑:考虑word1的位置 i 的字符和word2的位置 j 的字符,如果两者相等,那么就在上一个状态(i-1,j-1)在加一个长度;否则,选择(i,j-1)和(i-1,j)中最大的长度继承。

边界条件:二维dp数组全部都初始化0(没有公共子序列)

/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(word1, word2) {
    //初始化dp
    var dp = new Array(word1.length+1).fill(0).map(() => new Array(word2.length+1).fill(0));
    
    for (let i = 1; i<=word1.length; i++){
        const c1 = word1[i-1];
        for(let j = 1; j<=word2.length; j++){
            const c2 = word2[j-1];
            if (c1 === c2){
                dp[i][j] = dp[i-1][j-1] + 1
               
            }else {
                dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j])
            }
        }
    }
    // 返回的是需要删除的字符数
    return word1.length + word2.length - 2*dp[word1.length][word2.length]
};

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值