多种背包问题的解法模板

经典0/1背包问题

给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 w[i],价值为 v[i],现在让你用这个背包装物品,最多能装的价值是多少?

经典0/1背包的思路就是二维动态规划,定义好二维数组dp[i][j]表示从0-i个物品中选择总重量不超过j的物品的最大价值,确认边界条件,根据每一步的状态和选择找状态转移方程。

解法代码

class Solution {
    public int maxValue(int[] w, int[] v, int W) {
        int n = w.length;
        // dp[i][j]表示从0-i个物品中选择总重量不超过j的物品的最大价值
        int[][] dp = new int[n][W + 1];
        // 第一列都是0(容量为0价值为0) 第一行表示选择第一个物品时的价值
        for (int j = W; j >= w[0]; j--) {
            dp[0][j] = dp[0][j - w[0]] + v[0];
        }
        // 遍历物品 从1开始
        for (int i = 1; i < n; i++) {
            // 遍历背包容量
            for (int j = 0; j <= W; j++) {
                // 背包容量不够拿第i个物品了 最大价值就是拿第i-1个物品时的价值
                if (j < weight[i]) {
                    dp[i][j] = dp[i - 1][j];
                }
                // 足够拿第i个物品 可拿可不拿 取两个之间的较大值
                else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
                }
            }
        }
        return dp[n - 1][W];
    }
}

背包问题的扩展

背包分类

背包问题分为0/1背包、完全背包、组合背包、分组背包等不同类型。

  • 0/1背包:每个元素只能选取一次
  • 完全背包:每个元素可以重复选择
  • 组合背包:背包中的物品要考虑顺序
  • 分组背包:不止一个背包,需要遍历每一个背包

问题分类

根据需要求解的问题不同,又可以分为最值问题、存在问题、组合问题等。

  • 最值问题:求最大值/最小值
  • 存在问题:是否存在、是否满足
  • 组合问题:求满足...的排列组合

根据以上分类,可以将背包问题大致分类为0/1背包最值问题、0/1背包存在问题、0/1背包组合问题等九种不同的问题。

解法模板

背包问题的大结构是两层循环,分别遍历物品nums和容量target,然后写转移方程。根据背包的分类确定物品和容量的遍历顺序,根据问题的分类确定状态转移方程的写法。

背包分类模板

  1. 0/1背包:外循环nums,内循环target,target倒序遍历,且target>=nums[i]
  2. 完全背包:外循环nums,内循环target,target正序遍历,且target>=nums[i]
  3. 组合背包:外循环target,内循环nums,target正序遍历,且target>=nums[i]
  4. 分组背包:三重循环,外循环bags,里面两层循环根据问题类型转换成1、2、3三种类型

注意,组合背包分类中,根据是否需要考虑顺序,又分为以下两种类型。考虑顺序的意思是,相同元素的不同顺序视为不同的组合,例如(1,1,2)和(2,1,1)被视为不同的组合。

  • 考虑顺序:外循环target,内循环nums
  • 不考虑顺序:外循环nums,内循环target

问题分类模板

  • 最值问题:dp[i] = max/min(dp[i], dp[i-num]+1) or dp[i] = max/min(dp[i], dp[i-num]+num)
  • 存在问题:dp[i]=dp[i] || dp[i-num]
  • 组合问题:dp[i] += dp[i-num]

经典例题

0/1背包最值问题TODO

0/1背包存在问题

416. 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

本题可以转换为,判断是否存在一个子集,使得其总和target=sum/2。应用外循环nums,内循环target,target倒序遍历,且target>=nums[i]。

class Solution {
    public boolean canPartition(int[] nums) {
        // dp[i]表示能否在nums中找到足够的数字刚好填满容量为i的背包
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 奇数无法分割
        if ((sum & 1) == 1) return false;
        int t = sum / 2;
        boolean[] dp = new boolean[t + 1];
        // base case
        dp[0] = true;
        // 枚举数字
        for (int i = 0; i < nums.length; i++) {
            // 枚举目标值
            for (int j = t; j >= nums[i]; j--) {
                dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        return dp[t];
    }
}

0/1背包组合问题

494. 目标和

给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

0/1背包不考虑顺序的组合问题,应用外循环nums,内循环target,target倒序遍历,且target>=nums[i]。设sum为数组和,target用s代替,假设p是正数项集合,q是负数项集合,则p+q=sum,p-q=s,两式相加可得: p=(sum+s)/2 两式相减可得: q=(sum-s)/2。因此问题转变为在集合中寻找目标和为p或q的组合数量,这就变成典型的01背包组合问题了。

class Solution {
    public int findTargetSumWays(int[] nums, int s) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 剪枝
        if (sum < s || ((sum + s) & 1) == 1) {
            return 0;
        }
        int target = Math.abs((sum + s) / 2);
        int[] dp = new int[target + 1];
        dp[0] = 1;
        // 遍历数字
        for (int num : nums) {
            // 遍历目标值
            for (int j = target; j >= num; j--) {
                dp[j] = dp[j] + dp[j - num];
            }
        }
        return dp[target];
    }
}

分组0/1背包组合问题

1155. 掷骰子等于目标和的方法数

这里有 n 个一样的骰子,每个骰子上都有 k 个面,分别标号为 1 到 k 。

给定三个整数 nk 和 target,请返回投掷骰子的所有可能得到的结果(共有 kn 种方式),使得骰子面朝上的数字总和等于 target

由于答案可能很大,你需要对 109 + 7 取模

分组背包是看成 n 组,每组内有大小分别为 1,2,...,k 的物品,计算每组恰好选一个物品,组成大小为 target 的方案数。参考灵神的解析

class Solution {
    public int numRollsToTarget(int n, int k, int target) {
        int MOD = (int) 1e9 + 7;
        int[][] dp = new int[n + 1][target + 1];
        dp[0][0] = 1;
        // 投掷n次看成n个分组
        for (int i = 1; i <= n; i++) {
            // 遍历目标值
            for (int j = 1; j <= target; j++) {
                // 遍历每次投出的骰子点数
                for (int z = 1; z <= k && j >= z; z++) {
                    dp[i][j] = (dp[i][j] + dp[i - 1][j - z]) % MOD;
                }
            }
        }
        return dp[n][target];
    }
}

完全背包最值问题

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。

根据题目要求,容易看出是一个完全背包最值问题。应用外循环nums,内循环target,target正序遍历,且target>=nums[i]。

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 数组大小为 amount + 1,初始值也为 amount + 1
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1);
        // base case
        dp[0] = 0;
        // 枚举总金额
        for (int i = 0; i < dp.length; i++) {
            // 枚举硬币
            for (int coin : coins) {
                // 子问题无解,跳过
                if (i - coin < 0) continue;
                dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
            }
        }
        return (dp[amount] == amount + 1) ? -1 : dp[amount];
    }
}

完全背包存在问题TODO

完全背包组合问题

377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。题目数据保证答案符合 32 位整数范围。

完全背包考虑顺序的组合问题,应用外循环target,内循环nums,target正序遍历,且target>=nums[i]。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        // dp[i]表示总和为i的元素组合的个数
        int[] dp = new int[target + 1];
        // 总和为0的元素组合的个数只有1个
        dp[0] = 1;
        // 遍历目标总和
        for (int i = 0; i <= target; i++) {
            // 遍历数字
            for (int num : nums) {
                if (i >= num) {
                    dp[i] = dp[i] + dp[i - num];
                }
            }
        }
        return dp[target];
    }
}

组合背包最值问题TODO

组合背包存在问题TODO

组合背包组合问题TODO

参考链接

一篇文章吃透背包问题!(细致引入+解题模板+例题分析+代码呈现)

击败 100%!从记忆化搜索到递推,教你一步步思考动态规划!

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值