[LeetCode]-动态规划-3

文章介绍了如何使用动态规划解决LeetCode中的零钱兑换问题,包括求最少硬币个数和避免重复兑换的优化策略。同时,还涵盖了交错字符串验证和最大正方形问题的动态规划解法。此外,讨论了最低票价、最长公共子序列和最小路径和等其他动态规划应用场景。
摘要由CSDN通过智能技术生成

前言

记录 LeetCode 刷题时遇到的动态规划相关题目,第三篇

322. 零钱兑换

dp[i] 表示凑成 i 块钱时所需的最少的硬币个数。每个硬币的面额为 coins[j],那么 dp[i] 就是凑成 i - coins[j] 块钱时所需要的最少的硬币个数加 1,加的这个 “1” 就是面额为 coins[j] 的那枚硬币

public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];
    //后续需要Math.min来选较小数,所以dp数组每个元素先初始化为一个较大值。
    //注意到下面有“dp[i - coins[j]] + 1”,
    //所以为了防止溢出,不能直接使用Integer.MAX_VALUE,需要小一点,这里只减一即可
    Arrays.fill(dp, Integer.MAX_VALUE - 1);
    dp[0] = 0; //边界,凑0块钱需要0个硬币
    for (int i = 1; i <= amount; i++) {
        for (int j = 0; j < coins.length; j++) {
            if (coins[j] <= i) { //coins[j]小于等于i才能尝试用coins[j]来凑出i块钱
                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
            }
        }
    }
    //如果dp[amount]没有改变值说明无法凑出amount,返回-1
    return dp[amount] == Integer.MAX_VALUE - 1 ? -1 : dp[amount];
}

518. 零钱兑换 II

dp[i] 表示金额 i 的兑换方式数。对一个金额 i,枚举每一个面值比它小或相等的硬币 coin,那么 i 就可以用这个硬币跟 i 减去这个硬币剩下的金额来兑换,所以 dp[i] += dp[i - coin]。基于这个思路可以写出下面的代码

public int change(int amount, int[] coins) {
    int[] dp = new int[amount + 1];
    dp[0] = 1;
    for(int i = 1;i <= amount;i++){
        for(int j = 0;j < coins.length;j++){
            if(i - coins[j] >= 0) dp[i] += dp[i - coins[j]];
        }
    }
    return dp[amount];
}

这个代码是错的。举个例子:amount = 5, coins = [1, 2, 5],金额 1 有一种兑换方式 1;金额 2 有两种兑换方式 1 + 1,2;那么计算到金额 3 时,按照上面的算法会得到 3 种兑换方式,1 + 1 + 1,1 + 2,2 + 1。可以发现有重复的兑换方式,即有的硬币被使用了不同的兑换方式中使用了多次。
那么怎么让一个硬币只会被使用一次?只需交换一下两层 for 循环的顺序,先遍历硬币,再遍历金额即可:

public int change(int amount, int[] coins) {
    int[] dp = new int[amount + 1];
    dp[0] = 1;
    for(int coin : coins){
        for(int i = coin;i <= amount;i++){
            dp[i] += dp[i - coin];
        }
    }
    return dp[amount];
}

97. 交错字符串

参考官方题解

状态定义:dp[i][j] 表示 s1 中前 i 个字符跟 s2 中前 j 个字符 (注意下标表示的是字符个数而不是字符在原字符串中的索引) 能否组成 s3 中前 i + j 个字符

状态转移:如果 s1 中前 i - 1 个字符跟 s2 中前 j 个字符能组成 s3 中前 i + j - 1 个字符,那么如果 s1 中第 i - 1 (按照下标从 0 开始) 个字符跟 s3 中第 i + j - 1 个字符相同,就能说明 s1 中前 i 个字符跟 s2 中前 j 个字符能组成 s3 中前 i + j 个字符,也即 dp[i][j] |= dp[i - 1][j] && c1[i - 1] == c3[i + j - 1]

同理可得 dp[i][j] |= dp[i][j - 1] && c2[j - 1] == c3[i + j - 1]

边界:dp[0][0] = true,表示两个空串能交错形成一个空串

public boolean isInterleave(String s1, String s2, String s3) {
    char[] c1 = s1.toCharArray();
    char[] c2 = s2.toCharArray();
    char[] c3 = s3.toCharArray();
    int l1 = c1.length;
    int l2 = c2.length;
    //s1跟s2的长度和不等于s3的长度的话肯定就不能交错形成s3
    if(l1 + l2 != c3.length) return false;
    /*
    boolean[][] dp = new boolean[l1 + 1][l2 + 1];
    dp[0][0] = true;
    for(int i = 0;i <= l1;i++){
        for(int j = 0;j <= l2;j++){
        	//根据状态转移方程,可以使用状态压缩
            if(i > 0){
            	//这里的dp[i][j]一定是false,所以没必要使用|=
                dp[i][j] = dp[i - 1][j] && c1[i - 1] == c3[i + j - 1];
            }
            //原本dp[i][j]都是false(除了(0,0)),在判断判断j>0之前先判断了i>0,所以在
            //此处dp[i][j]可能已经被修改了,所以要使用|=
            if(j > 0){
                dp[i][j] |= dp[i][j - 1] && c2[j - 1] == c3[i + j - 1];
            }
        }
    }
    return dp[l1][l2];
    */
    boolean[] dp = new boolean[l2 + 1];
    dp[0] = true;
    for(int i = 0;i <= l1;i++){
        for(int j = 0;j <= l2;j++){
            if(i > 0){
                dp[j] = dp[j] && c1[i - 1] == c3[i + j - 1];
            }
            if(j > 0){
                dp[j] |= dp[j - 1] && c2[j - 1] == c3[i + j - 1];
            }
        }
    }
    return dp[l2];
}

221. 最大正方形

状态:dp[i][j] 表示以 matrix[i][j] 为右下角的只包含 1 的正方形的边长的最大值

状态转移:如果 matrix[i][j] 为 0,那么不可能存在以 matrix[i][j] 为右下角的只包含 1 的正方形,dp[i][j] 为 0;如果 matrix[i][j] 为 1,状态转移方程应为 dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1,即以左边的格子为右下角,以上边的格子为右下角,以左上边的格子为右下角的只包含 1 的正方形的边长的最小值中的最小值。为何要这么选?

假设当前要推导 dp[3][3],如果以左边的格子 matrix[3][2] 为右下角的只包含 1 的正方形的边长的最大值为 2,如果要能得到 dp[i][j] 为 3,就要求 matrix[1][1],matrix[1][2],matrix[1][3],matrix[2][3] 的值都为 1,也即,dp[2][2],dp[2][3] 必须大于等于 2。同理可以分析以上边的格子和以左上边的格子为右下角的情况。那么最终可以发现, 以 matrix[i][j] 为右下角的只包含 1 的正方形的边长的最大值必须取 dp[i - 1][j], dp[i][j - 1],dp[i - 1][j - 1] 三者中的最小值再加一

边界:以第一行跟第一列的格子作为右下角的正方形边长最大只能为 1,所以 dp[i][j] = 1 (i == 0 || j == 0)

public int maximalSquare(char[][] matrix) {
    int maxSide = 0;
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
        return maxSide;
    }
    int rows = matrix.length, columns = matrix[0].length;
    int[][] dp = new int[rows][columns];
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < columns; j++) {
            if (matrix[i][j] == '1') {
                if (i == 0 || j == 0) {
                	//边界上的点的最大边长只能为1
                    dp[i][j] = 1;
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                }
                maxSide = Math.max(maxSide, dp[i][j]);
            }
        }
    }
    return maxSide * maxSide;
}

983. 最低票价

根据题解简单总结:
首先,当天如果买了 n 天的票,那肯定是 n 天后买下一张票才不会浪费钱。
所以如果用 状态 dp[i] 表示推导到第 i 天时需要的最低花费,那么对于第 i 天,有三种决策:买为期一天的通行证 / 买为期七天的通行证 / 买为期三十天的通行证,dp[i] 应该等于这三种决策得到的票价的最小值
如果买为期一天的通行证,dp[i] 就等于 cost[0] 加上 1 天后的最低花费 dp[i + 1],即 d p [ i + 1 ] + c o s t s [ 0 ] dp[i + 1] + costs[0] dp[i+1]+costs[0]
同理,如果使用决策 2, d p [ i ] = d p [ i + 7 ] + c o s t [ 1 ] dp[i] = dp[i + 7] + cost[1] dp[i]=dp[i+7]+cost[1]
如果使用决策 3, d p [ i ] = d p [ i + 30 ] + c o s t [ 2 ] dp[i] = dp[i + 30] + cost[2] dp[i]=dp[i+30]+cost[2]
所以需要从后往前推导

在实际推导过程中,i 从 days 中的最大天数开始递减到最小天数,当遇到不是 days 中的天数时,dp[i] 就直接等于后一天的花费 dp[i + 1]

public int mincostTickets(int[] days, int[] costs) {
    int len = days.length, maxDay = days[len - 1], minDay = days[0];
    //在后续推导时需要对 cur+30,可以先判断cur的范围,也可以像下面这样直接把数组调大
    int[] dp = new int[maxDay + 31];
    int index = len - 1;
    for (int cur = maxDay;cur >= minDay;cur--) {
        if (cur == days[index]) {
            dp[cur] = Math.min(dp[cur + 1] + costs[0], dp[cur + 7] + costs[1]);
            dp[cur] = Math.min(dp[cur], dp[cur + 30] + costs[2]);
            index--; 
        } else {
            dp[cur] = dp[cur + 1];
        }
    }
    return dp[minDay]; 
}

1143. 最长公共子序列

像这种数组,字符串类型的动态规划题,如果只有一个数组或字符串,那么可以使用一维数组 dp,其中 dp[i] 表示区间 [0,i];如果有两个数组或字符串,那么使用二维数组,dp[i][j] 表示 数组1/字符串1 中的区间 [0,i] 以及 数组2/字符串2 中的区间 [0,j],然后再考虑能否降维

本道题中,状态 dp[i][j] 表示 text1 中前 i 个字母跟 text2 中前 j 个字母中最长公共子序列的长度,那么状态转移方程就为
dp[i][j] = dp[i - 1][j - 1] + 1,text1[i - 1] == text2[j - 1]
dp[i][j] = max(dp[i][j - 1],dp[i - 1][j]),其他情况

最终答案就是 dp[len1][len2]

public int longestCommonSubsequence(String text1, String text2) {
    char[] cs1 = text1.toCharArray();
    char[] cs2 = text2.toCharArray();
    int len1 = cs1.length;
    int len2 = cs2.length;
    int[][] dp = new int[len1 + 1][len2 + 1];
    for(int i = 1;i <= len1;i++){
        for(int j = 1;j <= len2;j++){
            if(cs1[i - 1] == cs2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
            else{
                dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
            }
        }
    }
    return dp[len1][len2];
}

64. 最小路径和

使用动态规划,二维 dp 数组中 dp[i][j] 表示走到 grid[i][j] 的最小路径和

想走到一个点 grid[i][j] 只能从 grid[i - 1][j] 向下走一步或者从 grid[i][j - 1] 向右走一步到达。所以走到 grid[i][j] 的最小路径和,自然就是 min(dp[i - 1][j],dp[i][j - 1]) + grid[i][j],至于之前的路线是怎么样,我们并不关心,这就是动态规划的强大之处

public int minPathSum(int[][] grid) {
    int m = grid.length,n = grid[0].length;
    int[][] dp = new int[m][n];
    dp[0][0] = grid[0][0];
    //对于第一列的点只能从上一个点向下走一步到达
    for(int i = 1;i < m;i++){
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    //对于第一行的点只能从左边的点向右走一步到达
    for(int i = 1;i < n;i++){
        dp[0][i] = dp[0][i - 1] + grid[0][i];
    }
    //开始状态转移
    for(int i = 1;i < m;i++){
        for(int j = 1;j < n;j++){
            dp[i][j] = Math.min(dp[i - 1][j],dp[i][j - 1]) + grid[i][j];
        }
    }
    //最后返回走到右下角即grid[m - 1][n - 1]的最小路径和
    return dp[m - 1][n - 1];
}

降维

老规矩,二维 dp,根据状态转移方程发现可以降为一维 dp

public int minPathSum(int[][] grid) {
    int m = grid.length,n = grid[0].length;
    int[] dp = new int[n];
    dp[0] = grid[0][0];
    for(int i = 1;i < n;i++){
        dp[i] = dp[i - 1] + grid[0][i];
    }
    for(int i = 1;i < m;i++){
        for(int j = 0;j < n;j++){
            if(j == 0) dp[j] = dp[j] + grid[i][j];
            else dp[j] = Math.min(dp[j],dp[j - 1]) + grid[i][j];
        }
    }
    return dp[n - 1];
}

152. 乘积最大子数组

状态 dp[i][0] 表示以 nums[i] 作为最后一个元素的子数组所能得到的最大乘积,dp[i][1] 则表示能得到的最小乘积。恒有 dp[i][0] >= dp[i][1]
边界即为第一个元素,dp[0][0] = dp[0][1] = nums[0]
对于 nums[i]:

  1. 如果 nums[i] > 0:那么如果 dp[i - 1][0] 也大于 0,则 dp[i][0] 应等于 dp[i - 1][0] * nums[i];否则 dp[i - 1][0] 小于等于 0,那 dp[i - 1][1] 更小,累积到 nums[i] 能得到的最大乘积只能是 nums[i] 了。如果 dp[i - 1][1] 小于等于 0,那么最小乘积就是 dp[i - 1][1] * nums[i],否则 dp[i - 1][1] 大于 0,dp[i - 1][0] 更大,最小乘积应该是 nums[i] 本身
  2. 如果 nums[i] == 0,不管前面的数为多少,最大最小乘积肯定都为 0
  3. 最后是 nums[i] < 0,为了得到最大乘积要找负数相乘,所以如果 dp[i - 1][1] 小于 0,那么最大乘积自然就是 dp[i - 1][1] * nums[i];
public int maxProduct(int[] nums) {
        int len = nums.length;
        int max = nums[0];
        int[][] dp = new int[len][2];
        dp[0][0] = nums[0];
        dp[0][1] = nums[0];
        for(int i = 1;i < len;i++){
            if(nums[i] > 0){
                dp[i][0] = dp[i - 1][0] > 0 ? dp[i - 1][0] * nums[i] : nums[i];
                max = Math.max(max,dp[i][0]);
                dp[i][1] = dp[i - 1][1] < 0 ? dp[i - 1][1] * nums[i] : nums[i];
            }else if(nums[i] == 0){
                max = Math.max(max,0);
                dp[i][0] = 0;
                dp[i][1] = 0;
            }else{
                dp[i][0] = dp[i - 1][1] < 0 ? dp[i - 1][1] * nums[i] : nums[i];
                max = Math.max(max,dp[i][0]);
                dp[i][1] = dp[i - 1][0] > 0 ? dp[i - 1][0] * nums[i] : nums[i];
            }
        }
        return max;
    }
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法LeetCode动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值