01背包与完全背包

01背包与完全背包

动态规划背包问题有两个关键要素:物品、背包;

涉及的问题类型

01背包(每个物品用一次):

  • 给定容量背包最大能装的物品容量。(分割等和,两组数的和相差最小值)
  • 给定容量背包最多能装的物品个数。

完全背包(每个物品用多次):

  • 求组合数,即物品不要求顺序。
  • 求排列数,即物品有顺序要求。

背包问题的方法论(个人总结)

对于背包遍历顺序;由于大多时候使用的是一维背包,涉及次轮遍历与上一轮遍历的状态关系;因此在对背包进行遍历的时候有要求,判断逻辑为物品是否可以重复使用。主要是由于,dp[i]由dp[i-nums[i]]推导得来,从左向右,则对物品会使用多次。

  • 01背包←:从右向左遍历背包。
  • 完全背包→:从左向右遍历背包。

对于物品和背包的遍历顺序判断逻辑为求组合数还是排列数。我的理解为是,先遍历物品,物品是依照顺序进行遍历的,对于一个背包来说,不会出现添加物品1,添加物品2,又添加物品1的情况,因此适用于组合数。而后遍历物品,则有可能会出现这种121的情况。

  • 组合数:先物品。(后背包)
  • 排列数:后物品。(先背包)

tips: 一般而言,大多数情况是先物品,后背包。涉及完全背包,排列数时,要对上述两个方面进行考虑。

例题

LeetCode.416.分割等和子集

image-20220816131236140

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        if (sum % 2 == 1) return false;
        else sum = sum / 2;
        
        int[] dp = new int[sum + 1]; 
        // dp[i] 表示容量为i的背包能装的最大重量
        dp[0] = 0;
        for (int i = 0; i < nums.length; i++) { // 遍历物品
            for (int j = sum; j >= nums[i]; j--) { // 遍历背包,从右向左
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[sum] == sum ? true : false;
    }
}

LeetCode.322.零钱兑换(最少数量)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4s6GsNLw-1660631453606)(%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%9201%E8%83%8C%E5%8C%85%E4%B8%8E%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85.assets/image-20220816142331878.png)]

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 完全背包,从左向右
        // 组合or排列都可以
        int[] dp = new int[amount + 1]; // 凑成金额i的最少硬币数为dp[i]
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 0; i < coins.length; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                if (dp[j - coins[i]] != Integer.MAX_VALUE)
                dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }
}

LeetCode.377.组合总和IV

image-20220816130955138

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1]; // dp[i] 表示总和为i的组合的个数
        dp[0] = 1;

        // for (int i = 0; i < nums.length; i++) { 
        //     //先遍历物品,物品按顺序遍历,为组合数
        //     for (int j = nums[i]; j <= target; j++) { 
        //         //遍历背包,可以重复,左到右遍历
        //         dp[j] = dp[j] + dp[j - nums[i]];
        //     }
        // return dp[target];

        for (int j = 1; j <= target; j++) { 
            //遍历背包,可以重复,从左到右
            for (int i = 0; i < nums.length; i++) { 
                // 后遍历物品,物品顺序是可变的,排列数
                if (j >= nums[i]) {
                    dp[j] += dp[j - nums[i]];
                }
            }
        }
        return dp[target];
    }
}

LeetCode.474.一和零(二维背包)

image-20220816131517306

使用未压缩的二维数组动态规划,不涉及从左还是从右的考虑,因为两轮遍历过程中,涉及状态的复制,不涉及状态的覆盖。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][][] dp = new int[strs.length + 1][m + 1][n + 1]; // m个0,n个1的最大数量
        
        for (int b = 1; b <= strs.length; b++) {
            String str = strs[b - 1];
            int mm = 0, nn = 0;
            for (int k = 0; k < str.length(); k++){
                if (str.charAt(k) == '0') mm++;
                else nn++;
            }
            for (int i = 0; i <= m; i++) {
                for (int j = 0; j <= n; j++) {
                    
                    if (i >= mm && j >= nn) {
                        dp[b][i][j] = Math.max(dp[b - 1][i][j], dp[b - 1][i - mm][j - nn] + 1);
                    } else {
                        dp[b][i][j] = dp[b - 1][i][j];
                    }
                }
            }
        }
        return dp[strs.length][m][n];
    }
}

使用压缩的滚动数组动态规划,需要从右向左进行遍历。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m + 1][n + 1]; // m个0,n个1的最大数量
        
        for (int b = 1; b <= strs.length; b++) {
            String str = strs[b - 1];
            int mm = 0, nn = 0;
            for (int k = 0; k < str.length(); k++){
                if (str.charAt(k) == '0') mm++;
                else nn++;
            }
            for (int i = m; i >= mm; i--) {
                for (int j = n; j >= nn; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - mm][j - nn] + 1);
                    // 二维背包的滚动数组,从后向前。
                }
            }
        }
        return dp[m][n];
    }
}

参考:《代码随想录》,https://programmercarl.com

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值