这篇博客记录在leetcode刷题中遇到的一些背包问题。
目录
完全背包问题
1.518. 零钱兑换 II
518. 零钱兑换 II 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
//思路:
这一题是典型的完全背包问题,即:在一组数中选出一些数,和为给定值,每个数字可以取无限个,求不同的组合(不同的取法)数。
对于背包问题,一般用二维dp,这里定义dp[i][j]的含义是,在[0, i-1]下标的数中,选一些数,可以组成和为j的组合数。
那么,对 0 <= i <= coins.size(),dp[i][0] = 1,意为无论有多少个数可以选择(包括没有数可以选的情况),1个数都不选,只有这一种取法能使和为0。
接下来考虑转移方程。dp[i][j]表示现在有[0, i-1]范围的数字可以选,对于下标为 i-1的数,可以选,也可以不选。如果不选,那么取法是dp[i-1][j]。如果选了下标是[i-1]的数,由于是完全背包,仍然可以继续选[i-1],所以取法是 dp[i][j - coins[i]],这里要保证 j >= coins[i]。由于这里的i, j是从左到右,从上到下计算的,因此 dp[i][j - coins[j]]的结果已经在 dp[i][j]之前计算出来了。
因此,转移方程即为 。
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<vector<int>> dp(coins.size() + 1, vector<int>(amount + 1, 0));
for (int i = 0; i <= coins.size(); ++i)
{
dp[i][0] = 1;
}
for (int i = 1; i <= coins.size(); ++i)
{
for (int j = 1; j <= amount; ++j)
{
dp[i][j] = dp[i-1][j];
if(j >= coins[i-1]) dp[i][j] += dp[i][j - coins[i-1]];
}
}
return dp[coins.size()][amount];
}
};
由于 dp[i][j]的值只与 dp[i-1][j] 和 dp[i][j - coins[i-1]]有关,因此可以把dp数组优化成一维的,并且 j 从左向右更新。对照转移方程
空间优化成1维dp后,由于dp[i][j]取决于dp[i][j-coins[i]],所以j的遍历依然是从左向右,依次更新dp[i][j],而未更新的部分是dp[i-1][j]。。
class Solution {
public:
int change(int amount, vector<int>& coins) {
int n = coins.size();
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (int i = 0; i < n; ++i)
{
for (int j = coins[i]; j <= amount; ++j)
{
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
};
相同的问题还有https://leetcode-cn.com/problems/coin-lcci/ 面试题 08.11. 硬币
2 322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
和上一题类似,不过这里要求的是最少需要的硬币个数。同样的,定义 dp[i][j]: 使用coins[0...i-1]来组合成j,最少需要的硬币个数。初始化 dp[i][0] = 0, for 0 <= i <= coins.size(),意为组合成0只需要0个硬币。之后,考虑 dp[i][j],如果不使用 coins[i],那么最少需要 dp[i-1][j];如果使用 coins[i],那么需要 dp[i][j-coins[i]] 个硬币,二者取较小的那个。即递推公式:
当然这里需要注意的是可能不能组成需要的值,因此初始化的时候除了j=0的情况,其它值都应该初始化为 INT32_MAX/2,/2是为了避免在j >= coins[i]时因为要计算 +1操作发生溢出。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
vector<vector<int>> dp(n+1, vector<int>(amount+1, INT32_MAX/2));
for (int i = 0; i <= n; ++i)
{
dp[i][0] = 0;
}
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= amount; ++j)
{
dp[i][j] = dp[i-1][j];
if(j >= coins[i-1]) dp[i][j] = min(dp[i][j], dp[i][j-coins[i-1]] + 1);
}
}
return dp[n][amount] < INT32_MAX/2 ? dp[n][amount] : -1;
}
};
同样的,因为求dp[i] 只涉及到 dp[i-1],可以压缩成一维dp,j的遍历顺序是 coins[i] -> amount
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
vector<int> dp(amount + 1, INT32_MAX/2);
dp[0] = 0;
for (int i = 0; i < n; ++i)
{
for (int j = coins[i]; j <= amount; ++j)
{
dp[j] = min(dp[j], dp[j-coins[i]] + 1);
}
}
return dp[amount] < INT32_MAX/2 ? dp[amount] : -1;
}
};
3. 377. 组合总和 Ⅳ
377. 组合总和 Ⅳ 给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3]
target = 4所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
//思路:
这一题看上去与完全背包问题很像,都是选出一些数,和为给定值,并且每个数可以无限选。但是不同之处在于,它要求的是排列数,因此,不能用完全背包去做,因为背包问题的结果是顺序无关的。
定义 dp[j] 是从所有数中选出和为 j的数的排列总数。需要考虑 dp[j] 和 dp[x] (x <= j)的关系。
对于 dp[j],这个 j 可以由 ? + nums[i]组成,而在最后的结果中,每一个 nums[i] 放在末尾,它能构成的排列总数就是 dp[j - nums[i]]。因此,转移方程就是
,
定义dp[0] = 1,语义是,nums[k] == j 时,dp[j - nums[k]] = 1,单独成为一种组合。
如果不好理解的话,可以类比爬楼梯问题:
https://leetcode-cn.com/problems/climbing-stairs/
70. 爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
对于这一问题,状态方程 dp[j] = dp[j-1] + dp[j-2]是怎么得到的呢?我们要爬 j 级台阶,可以先爬 j-1的台阶,最后爬1个台阶;也可以先爬 j-2 的台阶,最后爬2个台阶。因此,dp[j] = dp[j-1] + dp[j-2],可以看到方法是考虑顺序的。
对于这个问题,相当于我们一次能爬nums[0]...nums[k]级台阶,问要爬target级台阶,有多少种方法,当然是分为最后爬 nums[0]级、最后爬 nums[1]级、......最后爬 nums[k]级这些情况,并把它们相加。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int j = 1; j <= target; ++j)
{
for (int i = 0; i < nums.size(); ++i)
{
if(j >= nums[i]) dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
};
这个问题更详细的说明参考这篇题解:https://leetcode-cn.com/problems/combination-sum-iv/solution/dong-tai-gui-hua-python-dai-ma-by-liweiwei1419/
4. 1449. 数位成本和为目标值的最大数字
给你一个整数数组 cost 和一个整数 target 。请你返回满足如下规则可以得到的 最大 整数:
给当前结果添加一个数位(i + 1)的成本为 cost[i] (cost 数组下标从 0 开始)。
总成本必须恰好等于 target 。
添加的数位中没有数字 0 。
由于答案可能会很大,请你以字符串形式返回。如果按照上述要求无法得到任何整数,请你返回 "0" 。
示例 1:
输入:cost = [4,3,2,5,6,7,2,5,5], target = 9
输出:"7772"
解释:添加数位 '7' 的成本为 2 ,添加数位 '2' 的成本为 3 。所以 "7772" 的代价为 2*3+ 3*1 = 9 。 "997" 也是满足要求的数字,但 "7772" 是较大的数字。
数字 成本
1 -> 4
2 -> 3
3 -> 2
4 -> 5
5 -> 6
6 -> 7
7 -> 2
8 -> 5
9 -> 5
示例 2:输入:cost = [7,6,5,5,5,6,8,7,8], target = 12
输出:"85"
解释:添加数位 '8' 的成本是 7 ,添加数位 '5' 的成本是 5 。"85" 的成本为 7 + 5 = 12 。
示例 3:输入:cost = [2,4,6,2,4,6,4,4,4], target = 5
输出:"0"
解释:总成本是 target 的条件下,无法生成任何整数。
示例 4:输入:cost = [6,10,15,40,40,40,40,40,40], target = 47
输出:"32211"
提示:
cost.length == 9
1 <= cost[i] <= 5000
1 <= target <= 5000
本质是完全背包问题,即无限制地选一些数,它们的总和满足一定条件。但是与普通完全背包问题有两个不一样的地方:
(1) 要求选出的数的cost总和刚好等于target,而不是 <= target。
(2) 要求恢复选择的结果。
因为要使得组成的数字最大,显然长度要最长,所以dp数组应该记录最大长度的信息。定义 dp[i][j]: 从数字[1...i]里选,能够组成j的最大长度。
状态转移方程:
对于第一点不同,就要在dp数组初始化的时候小心,初始化dp[0][0] = 0, 其它dp[i][j] 都初始化为接近INT32_MIN的数,这样才能使得在不能刚好组成j的时候,dp[i][j] 依然是个负数,也就是无效结果。
对于第二点不同,在长度最长的时候,应当从最大的数字开始循环找可行的解,也就是从最大的数字 ei = 9 和 最后的总和 ej = target 开始,循环判断 dp[ei][ej] == dp[ei][ej - cost[ei-1]] + 1,如果是,那么说明ei被选择,否则,ei--。
class Solution {
public:
string largestNumber(vector<int>& cost, int target) {
int dp[10][5001];
//dp[i][j]: 从[1...i]里选,能组成代价和为j的最大长度
memset(dp, 0x80, sizeof(dp));
for(int i = 0; i <= 9; i++) dp[i][0] = 0;
for(int i = 1; i <= 9; i++)
{
for(int j = 1; j <= target; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= cost[i-1]) dp[i][j] = max(dp[i][j], dp[i][j-cost[i-1]] + 1);
}
}
if(dp[9][target] <= 0) return "0";
int ei = 9, ej = target;
string ans;
while(dp[ei][ej] > 0)
{
if(ej >= cost[ei-1] && dp[ei][ej] == dp[ei][ej-cost[ei-1]] + 1)
{
ans += ('0' + ei);
ej -= cost[ei-1];
}
else ei--;
}
return ans;
}
};
优化到一维同样可以恢复选择的信息,只需要每一次循环中从最大的数开始寻找可行解。
class Solution {
public:
string largestNumber(vector<int>& cost, int target) {
const int MAXT = 5000;
int dp[MAXT+1];
memset(dp, 0x80, sizeof(dp));
dp[0] = 0;
for(int i = 1; i <= 9; i++)
{
for(int j = cost[i-1]; j <= target; j++)
{
dp[j] = max(dp[j], dp[j-cost[i-1]] + 1);
}
}
if(dp[target] <= 0) return "0";
string ans;
int ej = target;
while(ej > 0)
{
for(int i = 9; i >= 1; i--)
{
if(ej >= cost[i-1] && dp[ej] == dp[ej-cost[i-1]] + 1)
{
ans += ('0' + i);
ej -= cost[i-1];
break;
}
}
}
return ans;
}
};
01背包问题
1. 494. 目标和
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例 1:
输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:-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一共有5种方法让最终目标和为3。
//思路
解决本题的重要突破点在于想到将+ - 两部分分开考虑,即选择一部分数取它们的和a,剩下的数取它们的相反数的和b,那么有
a+b = S, a-b = sum(nums)。从而,可以得到 a = (S + sum(nums)) / 2,问题也就转化成01背包问题:选择一些数,它们的和是a,每个数只能选一次,问有多少种取法。
注意这里 nums[i] 是非负整数,所以注意 dp[i][j] 的 j 遍历时从0开始, dp[0][0] 初始化为1,dp[i][0](i >= 1)不能确定,因为可能有一些 nums[i-1]本身就是0。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int S) {
long long sum = 0;
for(int i : nums) sum += i;
if(sum < S || (sum + S) % 2 == 1) return 0;
sum = (sum + S) / 2;
vector<vector<int>> dp(nums.size() + 1, vector<int>(sum + 1, 0));
dp[0][0] = 1;
for (int i = 1; i <= nums.size(); ++i)
{
for (int j = 0; j <= sum; ++j)
{
dp[i][j] = dp[i-1][j];
if(j >= nums[i-1]) dp[i][j] += dp[i-1][j-nums[i-1]];
}
}
return dp.back()[sum];
}
};
很容易看到,代码几乎与完全背包的(A)一模一样,分为两种情况,不取nums[i-1],和取nums[i-1]。但是由于是0-1背包,每个数只能取一次,所以选了第i-1个数之后,只能在[0, i-2]中找凑出 j - nums[i-1]的方案数目,所以取法是dp[i-1][j-nums[i]]。转移方程如下:
01背包的空间优化:一维dp注意与完全背包的一维dp的不同之处,j 的遍历方向不同。这是因为01背包的dp[i][j]取决于上一行的dp[i-1][j-nums[i-1]],所以j要从右向左进行更新,更新的部分是dp[i][j],未更新的部分是dp[i-1][j],从右向左避免覆盖。而完全背包的dp[i][j]取决于dp[i][j-nums[i-1]],所以j要从左向右更新。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int S) {
long long sum = 0;
for(int i : nums) sum += i;
if(sum < S || (sum + S) % 2 == 1) return 0;
sum = (sum + S) / 2;
vector<int> dp(sum + 1, 0);
dp[0] = 1;
for(int i : nums)
{
for (int j = sum; j >= i; --j)
{
dp[j] += dp[j - i];
}
}
return dp[sum];
}
};
2. 879. 盈利计划
帮派里有 G 名成员,他们可能犯下各种各样的罪行。
第 i 种犯罪会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。
让我们把这些犯罪的任何子集称为盈利计划,该计划至少产生 P 的利润。
有多少种方案可以选择?因为答案很大,所以返回它模 10^9 + 7 的值。
示例 1:
输入:G = 5, P = 3, group = [2,2], profit = [2,3]
输出:2
解释:
至少产生 3 的利润,该帮派可以犯下罪 0 和罪 1 ,或仅犯下罪 1 。
总的来说,有两种方案。
示例 2:输入:G = 10, P = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:
至少产生 5 的利润,只要他们犯其中一种罪就行,所以该帮派可以犯下任何罪行 。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。
提示:
1 <= G <= 100
0 <= P <= 100
1 <= group[i] <= 100
0 <= profit[i] <= 100
1 <= group.length = profit.length <= 100
也是01背包计数问题,要注意的是这里要求的是利润大于K的数量,所以在二维背包问题上又增加了一维。但是,因为所有可组合的利润最大能达到100*100,如果定义dp数组是 方案*容量*利润的话,规模太大。因为题目只要求利润大于等于P的,而P的数量级是100,所以应该定义dp数组 方案*容量*P,把所有利润大于等于P的组合都算到P里。
定义 dp[i][j][k]: 在[0...i-1]中选,背包容量是j,利润是k的组合数,注意所有大于k的组合也划到等于k里。
初始化要注意所有的k=0都有1种方案,就是都不选。
转移方程:
class Solution {
public:
int profitableSchemes(int G, int P, vector<int>& group, vector<int>& profit) {
int base = 1e9 + 7;
int n = group.size();
long long dp[101][101][101] = {0};
//dp[i][j][k]: 从 [0...i-1]里选,有j个人,利润是k的方案数
for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= G; j++)
{
dp[i][j][0] = 1;
}
}
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= G; j++)
{
for(int k = 0; k <= P; k++)
{
dp[i][j][k] = dp[i-1][j][k];
if(j >= group[i-1])
{
dp[i][j][k] += dp[i-1][j-group[i]][max(0, k-profit[i-1])];
dp[i][j][k] %= base;
}
}
}
}
return dp[n][G][P];
}
};
3. 416. 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
两种做法。
(1) 定义 dp[i][j]: 使用 nums[0...i-1], 能否组成j 。那么转移方程是:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum & 1) return false;
sum /= 2;
int n = nums.size();
vector<vector<bool>> dp(n+1, vector<bool>(sum+1, false));
//dp[i][j]: 使用nums[0...i-1]中的数,能否组成j
dp[0][0] = true;
for (int i = 1; i <= n; ++i)
{
for (int j = 0; j <= sum; ++j)
{
dp[i][j] = dp[i-1][j];
if(j >= nums[i-1]) dp[i][j] = dp[i][j] || dp[i-1][j - nums[i-1]];
}
}
return dp[n][sum];
}
};
优化成一维:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum & 1) return false;
sum /= 2;
vector<bool> dp(sum+1, false);
dp[0] = true;
for(int i : nums)
{
for(int j = sum; j >= i; j--)
{
dp[j] = dp[j] || dp[j - i];
}
}
return dp[sum];
}
};
(2) 定义dp[i][j]:使用 nums[0...i-1],能够组成的最大不超过 j 的数,最后只需要判断 dp[n][sum/2] == sum/2 即可。
转移方程是:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum & 1) return false;
sum /= 2;
int n = nums.size();
vector<vector<int>> dp(n+1, vector<int>(sum+1, 0));
//dp[i][j]: 使用nums[0...i-1]中的数,能组成最大不超过 sum 的数
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= sum; ++j)
{
dp[i][j] = dp[i-1][j];
if(j >= nums[i-1]) dp[i][j] = max(dp[i][j], dp[i-1][j-nums[i-1]] + nums[i-1]);
}
}
return dp[n][sum] == sum;
}
};
优化成一维:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum & 1) return false;
sum /= 2;
vector<int> dp(sum+1, 0);
for(int i : nums)
{
for(int j = sum; j >= i; j--)
{
dp[j] = max(dp[j], dp[j-i] + i);
}
}
return dp[sum] == sum;
}
};
4.1049. 最后一块石头的重量 II
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
输入:[2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 1000
本质是说,分成两组,并且使两组的差尽量小。那么两组的和都应该越接近sum/2越好。也就转化成,选一些数,它们的和不超过sum/2,能得到的最大和,是01背包问题。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int n = stones.size(), sum = accumulate(stones.begin(), stones.end(), 0);
vector<int> dp(sum/2+1, 0);
for (int i = 0; i < n; ++i)
{
for (int j = sum/2; j >= stones[i]; --j)
{
dp[j] = max(dp[j], dp[j-stones[i]] + stones[i]);
}
}
return abs(dp[sum/2] * 2 - sum);
}
};
5. 474. 一和零
在计算机界中,我们总是追求用有限的资源获取最大的收益。
现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。
你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。
注意:
给定 0 和 1 的数量都不会超过 100。
给定字符串数组的长度不会超过 600。
示例 1:输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
示例 2:输入: Array = {"10", "0", "1"}, m = 1, n = 1
输出: 2解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。
相当于两个维度上的01背包问题,所以不进行空间优化的话dp数组应该是三维的。
(1) 定义 dp[i][j][k] : 考察strs[0...i-1],使用 j 个0 和 k 个 1 能组成的最大字符串数。转移方程:
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
int size = strs.size();
int dp[size+1][m+1][n+1];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= size; ++i)
{
int zerocnt = 0, onecnt = 0;
for(char c : strs[i-1])
{
if(c == '0') zerocnt++;
else onecnt++;
}
for (int j = 0; j <= m; ++j)
{
for (int k = 0; k <= n; ++k)
{
dp[i][j][k] = dp[i-1][j][k];
if(j >= zerocnt && k >= onecnt) dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-zerocnt][k-onecnt] + 1);
}
}
}
return dp[size][m][n];
}
};
(2)同样的,dp[i][j][k]只和i-1有关,优化一维:
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
int dp[101][101];
memset(dp, 0, sizeof(dp));
for(const auto& i : strs)
{
int onecnt = 0, zerocnt = 0;
for(char c : i)
{
if(c == '0') zerocnt++;
else onecnt++;
}
for (int j = m; j >= zerocnt; --j)
{
for (int k = n; k >= onecnt; --k)
{
dp[j][k] = max(dp[j][k], dp[j-zerocnt][k-onecnt] + 1);
}
}
}
return dp[m][n];
}
};
二维费用背包问题还有:https://www.luogu.com.cn/problem/P1855#submit P1855 榨取kkksc03
多重背包
1. 宝物筛选
小 F 有一个最大载重为 W的采集车,洞穴里总共有 n 种宝物,每种宝物的价值为 vi,重量为wi,每种宝物有 mi 件。小 F 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。
输入格式
第一行为一个整数 n 和 W,分别表示宝物种数和采集车的最大载重。
接下来 n 行每行三个整数 vi,wi,mi。
输出格式
输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。
数据规模:n <= 100, W <= 1e4, 。
背包容量W,每个物品价值v, 重量w, 个数k,需要找到使得背包价值最大的物品组合。
最简单的做法是,把k个物品展开,本质上还是01背包问题。当然不需要真的展开,k个物品可以选1个,选2个...选k个,直接在循环里处理。
转移方程:dp[i][j]表示从[0...i-1]物品里选,重量不超过j的最大价值
#include <bits/stdc++.h>
using namespace std;
const int MAXW = 4e4 + 10;
const int MAXN = 110;
struct Node
{
int v;
int w;
int k;
};
int main()
{
int n, w;
scanf("%d%d", &n, &w);
Node all[MAXN];
for(int i = 0; i < n; i++)
{
scanf("%d%d%d", &all[i].v, &all[i].w, &all[i].k);
}
int dp[MAXN][MAXW] = {0};
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= w; j++)
{
dp[i][j] = dp[i-1][j];
for(int k = 1; k <= all[i-1].k; k++)
{
int totalw = k * all[i-1].w;
if(j < totalw) break;
dp[i][j] = max(dp[i][j], dp[i-1][j-totalw] + k * all[i-1].v);
}
}
}
printf("%d\n", dp[n][w]);
return 0;
}
优化到一维:
#include <bits/stdc++.h>
using namespace std;
const int MAXW = 4e4 + 10;
const int MAXN = 110;
struct Node
{
int v;
int w;
int k;
};
int main()
{
int n, w;
scanf("%d%d", &n, &w);
Node all[MAXN];
for(int i = 0; i < n; i++)
{
scanf("%d%d%d", &all[i].v, &all[i].w, &all[i].k);
}
int dp[MAXW] = {0};
for(int i = 0; i < n; i++)
{
for(int j = w; j >= all[i].w; j--)
{
for(int k = 1; k <= all[i].k; k++)
{
int totalw = k * all[i].w;
if(j < totalw) break;
dp[j] = max(dp[j], dp[j-k*all[i].w] + k*all[i].v);
}
}
}
printf("%d\n", dp[w]);
return 0;
}
转化为01背包的时间复杂度是,因为本质上还是对所有物品考虑选或者不选。
在比较大的时候,会超时。因为做了大量重复计算,例如,考虑了选择第1个物品的第1、2件,和考虑选择第1个物品的第2、3件,这两种选法是等效的,但是因为转换成01背包的时候把它们看成了不同的物品,也就是第i个物品贡献了ki个分组,每个分组都是1,导致了重复计算。
所以,需要减少可供选择的物品种类,才能降低复杂度。
假设第i件物品有ki个,那么它贡献的分组应该能组成<=ki的任意数,但是分组数要尽量少。这就是二进制优化的原理。例如ki = 6,它应该只贡献 1 2 3 三个分组,同时这三个分组一定可以组合出任意 <= 6 的可能。更一般的,第i个物品应该只贡献这些分组,如果ki+1不是2的幂,那么最后还剩下不能表示成2的幂的分组,要单独作为一组,保证由这些分组一定能组成 <= ki的任意数。
那么二进制分组的代码如下,在设定分组的数组大小时要注意分成的组数可能会比n大,但是一定不会超过。
//all:存放二进制分组的结果,最多是每个物品一组,一共有SUMK个物品,会分成SUMK组
Node all[MAXSUMK];
//count:最后一共分了多少组
int count = 0;
for(int i = 0; i < n; i++)
{
int vi, wi, ki;
scanf("%d%d%d", &vi, &wi, &ki);
//二进制分组
int c = 1;
while(ki >= c)
{
all[count++] = {vi*c, wi*c};
ki -= c;
c *= 2;
}
if(ki > 0) all[count++] = {vi*ki, wi*ki};
}
得到了所有的二进制分组之后,就可以像01背包一样对每个分组选或不选了。
#include <bits/stdc++.h>
using namespace std;
const int MAXW = 4e4 + 10;
const int MAXSUMK = 1e5 + 10;
struct Node
{
int v;
int w;
};
int main()
{
int n, w;
scanf("%d%d", &n, &w);
//all:存放二进制分组的结果,最多是每个物品一组,一共有MAXSUMK个物品,会分成MAXSUMK组
Node all[MAXSUMK];
//count:最后一共分了多少组
int count = 0;
for(int i = 0; i < n; i++)
{
int vi, wi, ki;
scanf("%d%d%d", &vi, &wi, &ki);
//二进制分组
int c = 1;
while(ki >= c)
{
all[count++] = {vi*c, wi*c};
ki -= c;
c *= 2;
}
if(ki > 0) all[count++] = {vi*ki, wi*ki};
}
int dp[MAXW] = {0};
for(int i = 0; i < count; i++)
{
for(int j = w; j >= all[i].w; j--)
{
dp[j] = max(dp[j], dp[j-all[i].w] + all[i].v);
}
}
printf("%d\n", dp[w]);
return 0;
}
复杂度降低到了。
2. POJ 1014 Dividing
POJ 1014也是一道多重背包问题:大意如下:
有6种物品,第i种物品(i >= 1)的重量是i,有ki个,问能否把这些物品划分为重量相等的两堆。
和01背包中划分等和子集问题很像,不过这里需要用二进制分组来转化成01背包问题。
#include <string>
#include <iostream>
#include <vector>
#include <cstdio>
using namespace std;
const int MAXSUMK = 20000;
int main()
{
string str;
int index = 1;
while(getline(cin, str))
{
int nums[7];
sscanf(str.c_str(), "%d%d%d%d%d%d", &nums[1], &nums[2], &nums[3], &nums[4], &nums[5], &nums[6]);
int sum = nums[1] + nums[2] * 2 + nums[3] * 3 + nums[4] * 4 + nums[5] * 5 + nums[6] * 6;
if(sum == 0) break;
printf("Collection #%d:\n", index++);
if(sum & 1)
{
printf("Can't be divided.\n\n");
continue;
}
sum /= 2;
vector<bool> dp(sum+1, false);
dp[0] = true;
//二进制分组
int all[MAXSUMK];
int count = 0;
for(int i = 1; i <= 6; i++)
{
int c = 1;
while(nums[i] >= c)
{
nums[i] -= c;
all[count++] = c * i;
c *= 2;
}
if(nums[i] > 0) all[count++] = nums[i] * i;
}
for(int i = 0; i < count; i++)
{
for(int j = sum; j >= all[i]; j--)
{
dp[j] = dp[j] || dp[j-all[i]];
}
}
if(dp[sum]) printf("Can be divided.\n\n");
else printf("Can't be divided.\n\n");
}
return 0;
}
3. 1363. 形成三的最大倍数
给你一个整数数组 digits,你可以通过按任意顺序连接其中某些数字来形成 3 的倍数,请你返回所能得到的最大的 3 的倍数。
由于答案可能不在整数数据类型范围内,请以字符串形式返回答案。
如果无法得到答案,请返回一个空字符串。
示例 1:
输入:digits = [8,1,9]
输出:"981"
示例 2:输入:digits = [8,6,7,1,0]
输出:"8760"
示例 3:输入:digits = [1]
输出:""
示例 4:输入:digits = [0,0,0,0,0,0]
输出:"0"
提示:
1 <= digits.length <= 10^4
0 <= digits[i] <= 9
返回的结果不应包含不必要的前导零。
可以用多重背包的思路去做,也就是找一些分组,它们的和能被3整除(且大于0),并且长度最大。最后从dp数组里恢复选择的分组信息。时间复杂度比较高,因为最大和(背包容量)可以达到90000。看评论可以用贪心去做,待补。
class Solution {
public:
string largestMultipleOfThree(vector<int>& digits) {
int h[10] = {0};
int maxsum = 0;
for(int i : digits)
{
h[i]++;
maxsum += i;
}
//数位最多的,并且和是3的倍数的分组
vector<pair<int,int>> all;
//{和,个数}
for(int i = 0; i <= 9; i++)
{
int k = h[i], c = 1;
while(k >= c)
{
k -= c;
all.push_back({c*i, c});
c *= 2;
}
if(k > 0) all.push_back({k*i, k});
}
int n = all.size();
vector<vector<int>> dp(n+1, vector<int>(maxsum + 1, INT32_MIN / 2));
dp[0][0] = 0;
//dp[i][j]: 从all[0...i-1]里选,能组成j的分组的最大长度
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= maxsum; j++)
{
dp[i][j] = dp[i-1][j];
if(j >= all[i-1].first) dp[i][j] = max(dp[i][j], dp[i-1][j-all[i-1].first] + all[i-1].second);
}
}
int ei = n, ej = -1, maxlen = 0;
for(int j = 1; j <= maxsum; j++)
{
if(j % 3 == 0)
{
if(dp[n][j] >= maxlen)
{
maxlen = dp[n][j];
ej = j;
}
}
}
if(ej < 0)
{
if(dp[n][0] > 0) return "0";
else return "";
}
vector<pair<int,int>> ans;
//{数,个数}
while(ei > 0 && ej >= 0)
{
if(dp[ei][ej] == dp[ei-1][ej]) ei--;
else
{
ans.push_back({all[ei-1].first / all[ei-1].second, all[ei-1].second});
ej -= all[ei-1].first;
ei--;
}
}
sort(ans.rbegin(), ans.rend());
string res;
for(const auto& p : ans) res += string(p.second, static_cast<char>(p.first + '0'));
return res;
}
};
混合背包
1.P1833 樱花
题目大意:
背包容量W,有n种物品,有一些物品有无限个,有一些物品只有一个,还有一些物品有大于一个的有限个,求背包容纳的最大价值。
混合背包问题只需要分情况,无限个的物品,套用完全背包模板,有限个的物品,如果大于1个就用二进制优化,转化成01背包。都可以用一维dp去解决。
#include <bits/stdc++.h>
using namespace std;
const int MAXW = 1001;
const int MAXN = 10001;
const int MAXSUMK = 1e6 + 5;
struct Node
{
int w;
int v;
int k;
};
int main()
{
int h1, m1, h2, m2, n;
scanf("%d:%d %d:%d %d\n", &h1, &m1, &h2, &m2, &n);
int w = h2 * 60 + m2 - h1 * 60 - m1;
Node all[MAXSUMK];
int count = 0;
for(int i = 0; i < n; i++)
{
int wi, vi, ki;
scanf("%d%d%d", &wi, &vi, &ki);
//01背包或者完全背包
if(ki == 0 || ki == 1) all[count++] = {wi, vi, ki};
else
{
//多重背包,二进制优化
int c = 1;
while(ki >= c)
{
ki -= c;
all[count++] = {wi*c, vi*c, 1};
c *= 2;
}
if(ki > 0) all[count++] = {wi*ki, vi*ki, 1};
}
}
int dp[MAXW] = {0};
for(int i = 0; i < count; i++)
{
if(all[i].k == 0)
{
//完全背包模板
for(int j = all[i].w; j <= w; j++)
{
dp[j] = max(dp[j], dp[j-all[i].w] + all[i].v);
}
}
else
{
//01背包模板
for(int j = w; j >= all[i].w; j--)
{
dp[j] = max(dp[j], dp[j-all[i].w] + all[i].v);
}
}
}
printf("%d\n", dp[w]);
return 0;
}
分组背包
1. P1757 通天之分组背包
题目大意:
背包容量W,有n件物品,每件物品重量wi, 价值vi,属于第 gi 组,同一组的物品只能取一件。求背包能容纳的最大价值。
分组背包问题,要求每一组的物品只能选1件,就要把每一组看成01背包中的一件物品,然后再在组内选择一件使得总价值最大。首先要想到合适的数据结构来存储物品,因为有分组的存在,所以属于第 gi 组的物品应该存放在 all[gi][cnti] 里,之后,把每一组看成一件物品,再在组内选择一件。核心代码:
for(int i = 1; i <= maxg; i++) // 循环每一组
{
for(int j = w; j >= 0; j--) // 循环背包容量
{
for(int k = 0; k < count[i]; k++) // 循环该组的每一个物品
{
if(j >= all[i][k].w)
{
dp[j] = max(dp[j], dp[j-all[i][k].w] + all[i][k].v);
}
}
}
}
要注意循环顺序,因为这里是每一组看成一件物品,所以和01背包一样,外层循环是每一组,内层循环是从总容量w逆序遍历,但是因为这里每一组里的重量并不相同,所以遍历到0,再对同一组里的每一个元素进行 dp[j] = max(dp[j], dp[ j - w[i][k] ] + v[i][k])的计算。
#include <bits/stdc++.h>
using namespace std;
const int MAXG = 1010;
const int MAXN = 1010;
const int MAXW = 1010;
struct Node
{
int w;
int v;
};
int main()
{
int w, n;
scanf("%d%d", &w, &n);
Node all[MAXG][MAXN] = {0};
int count[MAXG] = {0};
int maxg = 0;
for(int i = 0; i < n; i++)
{
int wi, vi, gi;
scanf("%d%d%d", &wi, &vi, &gi);
all[gi][count[gi]++] = {wi, vi};
maxg = max(maxg, gi);
}
int dp[MAXW] = {0};
for(int i = 1; i <= maxg; i++)
{
for(int j = w; j >= 0; j--)
{
for(int k = 0; k < count[i]; k++)
{
if(j >= all[i][k].w)
{
dp[j] = max(dp[j], dp[j-all[i][k].w] + all[i][k].v);
}
}
}
}
printf("%d\n", dp[w]);
return 0;
}
有依赖的背包
1.P1064 金明的预算方案
题目大意:
背包容量W,有m件物品,重量wi, 价值vi, 分为主件和附件,一个主件最多有2个附件。如果选择了附件那么必须也要选择主件。求背包能容纳的最大价值。
因为选附件时必须要选主件,假设主件为A,附件为B C,那么相当于 A AB AC ABC 这四种里只能选择一种,可以转换为分组背包问题。
题目的数据处理比较繁琐,首先要找到属于同一组的,再对每一组的物品进行组合得到所有可供选择的物品。同时注意题目里的价值是 pi * wi。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3201;
const int MAXG = 65;
struct Node
{
int w;
int v;
int q;
};
int main()
{
int w, m;
scanf("%d%d", &w, &m);
//价格都是10的倍数,可以减小时间复杂度。
w /= 10;
Node groups[MAXG][3];
int indexs[MAXG] = {0};
//先找到每一组里包含哪些元素
for(int i = 1; i <= m; i++)
{
int wi, vi, qi;
scanf("%d%d%d", &wi, &vi, &qi);
wi /= 10;
if(qi == 0) groups[i][indexs[i]++] = {wi, vi * wi, 0};
else groups[qi][indexs[qi]++] = {wi, vi * wi, qi};
}
//一个商品最多有2个附件,也就是一组内最多有A AB AC ABC 四种选择
Node all[MAXG][4];
int count[MAXG] = {0};
for(int i = 1; i <= m; i++)
{
if(indexs[i] > 0)
{
int master = -1, slave1 = -1, slave2 = -1;
for (int j = 0; j < indexs[i]; ++j)
{
if(groups[i][j].q == 0) master = j;
else if(slave1 == -1) slave1 = j;
else slave2 = j;
}
all[i][count[i]++] = {groups[i][master].w, groups[i][master].v , 0};
if(slave1 > -1)
{
int totalw = groups[i][master].w + groups[i][slave1].w;
int totalv = groups[i][master].v + groups[i][slave1].v ;
all[i][count[i]++] = {totalw, totalv, 0};
}
if(slave2 > -1)
{
int totalw = groups[i][master].w + groups[i][slave2].w;
int totalv = groups[i][master].v + groups[i][master].w + groups[i][slave2].v + groups[i][slave2].w;
all[i][count[i]++] = {totalw, totalv, 0};
totalw += groups[i][slave1].w;
totalv += groups[i][slave1].v * groups[i][slave1].w;
all[i][count[i]++] = {totalw, totalv, 0};
}
}
}
//分组背包模板
int dp[MAXN] = {0};
for(int i = 1; i <= m; i++)
{
for(int j = w; j >= 0; j--)
{
for(int k = 0; k < count[i]; k++)
{
if(j >= all[i][k].w)
{
dp[j] = max(dp[j], dp[j-all[i][k].w] + all[i][k].v);
}
}
}
}
printf("%d\n", dp[w] * 10);
return 0;
}