[LeetCode]-动态规划-3

前言

记录 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;
    }

猜你喜欢

转载自blog.csdn.net/Pacifica_/article/details/126561604