背包问题
- 判断是排列问题 还是 组合问题
- 确定遍历顺序:
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
解释:如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
https://leetcode-cn.com/problems/coin-change/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-sq9n/
背包问题力扣完整攻略
只要按如下顺序刷题,相信会帮你在学习背包问题的路上少走很多弯路!
「力扣」上的 0-1 背包问题
416.分割等和子集
474.一和零
494.目标和
879 :盈利计划(困难)
1049.最后一块石头的重量 II
「力扣」上的 完全背包问题:
- 零钱兑换 II
377.组合总和Ⅳ. !!不是「完全背包」问题
70.爬楼梯进阶版 - 零钱兑换
279.完全平方数
139.单词拆分
第 1449 题:数位成本和为目标值的最大数字(困难)
「力扣」上的 多重背包问题:
1. 416.分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
【0-1背包存在性问题:是否存在一个子集,其和为target=sum/2,外循环nums,内循环target倒序】
public class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
int sum = 0;
for (int num : nums) {
sum += num;
}
if ((sum & 1) == 1) {
return false;
}
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
if (nums[0] <= target) {
dp[nums[0]] = true;
}
for (int i = 1; i < len; i++) {
for (int j = target; nums[i] <= j; j--) {
if (dp[target]) {
return true;
}
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[target];
}
}
//时间复杂度:O(NC)O(NC):这里 NN 是数组元素的个数,CC 是数组元素的和的一半;
//空间复杂度:O(C)O(C):减少了物品那个维度,无论来多少个数,用一行表示状态就够了。
2. 474. 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
解析:
通常与「背包问题」相关的题考察的是 将原问题转换为「背包问题」的能力。
要将原问题转换为「背包问题」,往往需要从题目中抽象出「价值」与「成本」的概念。
这道题如果抽象成「背包问题」的话,应该是:
每个字符串的价值都是 1(对答案的贡献都是 1),选择的成本是该字符串中 1 的数量和 0 的数量。
以上是原始状态转义公式,下面代码是经过空间优化后的:
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
int[][] cnt = new int[len][2];
for (int i = 0; i < len; i++) {
int zero = 0, one = 0;
for (char c : strs[i].toCharArray()) {
if (c == '0') zero++;
else one++;
}
cnt[i] = new int[]{zero, one};
}
int[][] f = new int[m + 1][n + 1];
for (int k = 0; k < len; k++) {
int zero = cnt[k][0], one = cnt[k][1];
for (int i = m; i >= zero; i--) {
for (int j = n; j >= one; j--) {
f[i][j] = Math.max(f[i][j], f[i - zero][j - one] + 1);
}
}
}
return f[m][n];
}
}
时间复杂度:O(k∗m∗n)
空间复杂度:O(m * n)
3. 494 目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
解析:
【0-1背包不考虑元素顺序的组合问题】
当使用递推形式时,我们通常会使用「静态数组」来存储动规值,因此还需要考虑维度范围的:
第一维为物品数量:范围为 nums 数组长度
第二维为中间结果:令 s 为所有 nums 元素的总和(题目给定了 nums[i] 为非负数的条件,否则需要对 nums[i] 取绝对值再累加),那么中间结果的范围为 [-s, s]
class Solution {
public int findTargetSumWays(int[] nums, int t) {
int n = nums.length;
int s = 0;
for (int i : nums) s += Math.abs(i);
if (Math.abs(t) > s) return 0;
int[][] f = new int[n + 1][2 * s + 1];
f[0][0 + s] = 1;
for (int i = 1; i <= n; i++) {
int x = nums[i - 1];
for (int j = -s; j <= s; j++) {
if ((j - x) + s >= 0) f[i][j + s] += f[i - 1][(j - x) + s];
if ((j + x) + s <= 2 * s) f[i][j + s] += f[i - 1][(j + x) + s];
}
}
return f[n][t + s];
}
}
4. 879. 盈利计划
class Solution {
int mod = (int)1e9+7;
public int profitableSchemes(int n, int min, int[] gs, int[] ps) {
int m = gs.length;
long[][][] f = new long[m + 1][n + 1][min + 1];
for (int i = 0; i <= n; i++) f[0][i][0] = 1;
for (int i = 1; i <= m; i++) {
int a = gs[i - 1], b = ps[i - 1];
for (int j = 0; j <= n; j++) {
for (int k = 0; k <= min; k++) {
f[i][j][k] = f[i - 1][j][k];
if (j >= a) {
int u = Math.max(k - b, 0);
f[i][j][k] += f[i - 1][j - a][u];
if (f[i][j][k] >= mod) f[i][j][k] -= mod;
}
}
}
}
return (int)f[m][n][min];
}
}
空间优化版本:
class Solution {
int mod = (int)1e9+7;
public int profitableSchemes(int n, int min, int[] gs, int[] ps) {
int m = gs.length;
int[][] f = new int[n + 1][min + 1];
for (int i = 0; i <= n; i++) f[i][0] = 1;
for (int i = 1; i <= m; i++) {
int a = gs[i - 1], b = ps[i - 1];
for (int j = n; j >= a; j--) {
for (int k = min; k >= 0; k--) {
int u = Math.max(k - b, 0);
f[j][k] += f[j - a][u];
if (f[j][k] >= mod) f[j][k] -= mod;
}
}
}
return f[n][min];
}
}
5. 518. 零钱兑换II (Coin Change 2)
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
解释:完全背包问题
定义 f[i][j]为考虑前 ii 件物品,凑成总和为 j 的方案数量。
为了方便初始化,我们一般让f[0][x] 代表不考虑任何物品的情况。
因此我们有显而易见的初始化条件:f[0][0] = 1,其余 f[0][x] = 0。
代表当没有任何硬币的时候,存在凑成总和为 0 的方案数量为 1;凑成其他总和的方案不存在。
【完全背包不考虑顺序的组合问题】
class Solution {
public int change(int cnt, int[] cs) {
int n = cs.length;
int[] f = new int[cnt + 1];
f[0] = 1;
for (int i = 1; i <= n; i++) {
int val = cs[i - 1];
for (int j = val; j <= cnt; j++) {
f[j] += f[j - val];
}
}
return f[cnt];
}
}
6. 322. 零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5]
, amount = 11
输出:3
解释:11 = 5 + 5 + 1
【完全背包最值问题:外循环coins,内循环amount正序】
int coinChange(vector<int> &coins, int amount)
{
vector<long long> dp(amount, INT_MAX); //给dp数组每个位置赋初值为INT_MAX是为了最后判断是否能填满amount,要用long long 类型
dp[0] = 0; //dp[i]:换到面值i所用的最小数量
for (int coin : coins)
{
for (int i = coin; i <= amount; i++)
{
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
7. 279. 完全平方数
给你一个整数 n
,返回和为 n
的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
完全背包的最值问题:完全平方数最小为1,最大为sqrt(n),故题目转换为在nums=[1,2.....sqrt(n)]中选任意数平方和为target=n
外循环nums,内循环target正序,应用转移方程1
int numSquares(int n)
{
vector<int> dp(n + 1, INT_MAX); //dp[i]:和为i的完全平方数的最小数量
dp[0] = 0;
for (int num = 1; num <= sqrt(n); num++)
{
for (int i = 0; i <= n; i++)
{
if (i >= num * num)
dp[i] = min(dp[i], dp[i - num * num] + 1);
}
}
return dp[n];
}
8. 377. 组合总和 Ⅳ
【完全背包问题的排列问题】在nums中任选一些数,和为target
考虑顺序的组合问题:外循环target,内循环nums,应用状态方程3
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
int combinationSum4(vector<int> &nums, int target)
{
vector<int> dp(target + 1);
dp[0] = 1;
for (int i = 1; i <= target; i++)
{
for (int num : nums)
{
if (num <= i)
dp[i] += dp[i - num];
}
}
return dp[target];
}
9. 1155. 掷骰子的N种方法
投掷骰子的方法数:d个骰子,每个有f个面(点数为1,2,...f),求骰子点数和为target的方法
分组0/1背包的组合问题:dp[i][j]表示投掷i个骰子点数和为j的方法数;三层循环:最外层为背包d,然后先遍历target后遍历点数f
应用二维拓展的转移方程3:dp[i][j]+=dp[i-1][j-f];
int numRollsToTarget(int d, int f, int target)
{
vector<vector<int>> dp(d + 1, vector<int>(target + 1, 0));
dp[0][0] = 1;
for (int i = 1; i <= d; i++)
for (int j = 1; j <= target; j++)
for (int k = 1; k <= f && j >= k; k++)
dp[i][j] += dp[i - 1][j - k];
return dp[d][target];
}