背包问题:有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择 0 个或 1 个,则问题称为 0-1 背包问题;如果不限定每种物品的数量,则问题称为完全背包问题。
0-1 背包问题,我们可以定义一个二维数组 dp
存储最大价值,其中 dp[i][j]
表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。在我们遍历到第 i 件物品时,在当前背包总容量为 j 的情况下,分为两种情况:(1)如果我们不将物品 i 放入背包,即当前背包容量不足或这放入当前物品无法达到最大价值,那么 dp[i][j]= dp[i-1][j]
,即前 i 个物品的最大价值等于只取前 i -1 个物品时的最大价值;(2)如果我们将物品 i 放入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 dp[i][j] = dp[i-1][j-w] + v
。
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<vector<int>> dp(N+1,vector<int>(W+1));
// 放置第i个物品
for(int i=1;i<=N;++i){
// 第 i 个物品的体积w和价值v
int w = weights[i-1], v = values[i-1];
// 遍历容量
for(int j=1;j<=W;++j){
if(j>=w){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-w]+v);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][W];
}
在完全背包问题中,一个物品可以拿多次。这里直接给出完全背包问题的状态转移方程dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v)
,其与 0-1 背包问题的差别仅仅是把状态转移方程中的第二个 i-1 变成了 i。
int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<vector<int>> dp(N+1,vector<int>(W+1));
// 放置第i个物品
for(int i=1;i<=N;++i){
// 第 i 个物品的体积w和价值v
int w = weights[i-1], v = values[i-1];
// 遍历容量
for(int j=1;j<=W;++j){
if(j>=w){
dp[i][j] = max(dp[i-1][j],dp[i][j-w]+v);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][W];
}
416 分割等和子集
给定一个正整数数组,求是否可以把这个数组分成和相等的两部分。
输入是一个一维正整数数组,输出时一个布尔值,表示是否可以满足题目要求。
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
解析:
本题等价于 0-1 背包问题,设所有数字和为 sum,我们的目标是选取一部分物品,使得它们的总和为 target = sum/2,及用部分物品填满容量为target的背包。同时本题不需要考虑价值,因此我们只需要通过一个布尔值矩阵来表示状态转移矩阵。
设置状态: dp[i][j]
表示 nums 的前 i 个整数是否能够组合成和为 j
状态转移方程:(01背包问题的子问题都很简单只用区分选择当前物品或者不选择当前物品的情况)
-
不选择
nums[i]
:nums[i]
大于于当前容量 j 时,容量不够不选择该物品,dp[i][j] = dp[i-1][j]
-
选择
nums[i]
:-
nums[i]
恰好等于当前容量 j,即nums[i]==j
时,只放该物品就可以填满容量为 j 的背包,dp[i][j] =true
-
nums[i]
小于当前容量 j,即nums[i]<j
时,只放该物品不够填满容量为 j 的背包,检测放入该物品后dp[i-1][j-nums[i]]
的情况下是否可以填满背包
总的说,当容量足够时,物品nums[i] 可选可不选(在有价值的情况下就需要选择价值较大的情况),则
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]]
-
初始情况:容量为0的时候,不选择任何物品,dp[i][0] = true
;只有一个物品的时候,当容量为j == nums[0]
时恰好满足,即dp[0][nums[0]] = true
示例的状态转移矩阵根据上述状态转移方程可以得到如下表格:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
[0] = 1 | T | T | F | F | F | F | F | F | F | F | F | F |
[1] = 5 | T | T | F | F | F | T | T | F | F | F | F | F |
[2] = 11 | T | T | F | F | F | T | T | F | F | F | F | T |
[3] = 5 | T | T | F | F | F | T | T | F | F | F | T | T |
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(),nums.end(),0);
auto maxPos = max_element(nums.begin(),nums.end());
int target = sum/2, len = nums.size();
// 注意特殊情况,和为奇数或者元素个数小与2都无法进行有效分割,而元素值大于target直接导致越界
if(len<2 || sum&1 || *maxPos > target){
return false;
}
vector<vector<bool>> dp(len,vector<bool>(target+1,false));
// 初始情况容量为0
for(int i=0;i<len;++i){
dp[i][0] = true;
}
// 初始情况物品只有一个
dp[0][nums[0]] = true;
for(int i=1;i<len;++i){
for(int j=1;j<=target;++j){
// 背包容量大于当前扫描物品,可选可不选
if(j >= nums[i]){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
// 背包容量小于当前扫描物品,没法选
}else{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[len-1][target];
}
};
494 目标和
给定义一个整数数组 nums
和一个整数 target
。向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式,返回可以完成表达式构造的、运算结果等于 target
的不同 表达式 的数目。
输入一个一维数组和整数,输出一个整数表示构造表达式结果为target的方法数
输入: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
解析:
本题可以转化为 01 背包问题,假设数组元素总和为 sum,添加 '+'
的元素和为 x,那么添加 '-'
的元素和为 sum - x
。由此可以得到 x - (sum - x) = target
,即为x = (target + sum) / 2
,那么本题就可以转化为使用 nums
中的N个物品装满容量为 x 的背包,共有多少种方法,可以看出这时一个组合问题。
设置状态:使用一个二维数组 dp[i][j]
表示数组 nums 中从开头到以 i 位置结尾的所有物品装满容量为 j 的背包的方法数。
状态转移方程:考虑第 i 个元素,如果当前背包容量 j<nums[i]
,则不能放入第 i 个元素 dp[i][j] = dp[i-1][j]
;如果当前背包容量j >= nums[i]
,不放入第 i 个元素的方法数为dp[i][j] = dp[i-1][j]
,放入第 i 个元素的方法数为dp[i][j] = dp[i-1][j-nums[i]]
,那么总的方法数为这两中情况之和即dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
边界情况:如果(target + sum)
不为偶数是无解情况,如果 abs(target) > sum
也是无解情况,没有任何构造方式能够满足题目条件。当数组为空,背包容量为0时dp[0][0] == 1
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int len = nums.size();
int sum = accumulate(nums.begin(),nums.end(),0);
if((target+sum)&1 || abs(target)>sum){
return 0;
}
int weight = (target + sum)/2;
vector<vector<int>> dp(len+1,vector<int>(weight+1));
dp[0][0] = 1;
for(int i=1;i<=len;++i){
for(int j=0;j<=weight;++j){
dp[i][j] = dp[i-1][j];
if(j>=nums[i-1]){
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
}
}
}
return dp[len][weight];
}
};
474 一和零
给定 m 个数字 0 和 n 个数字 1,以及一些由 0-1 构成的字符串,求利用这些数字最多可以构成多少个给定的字符串,字符串只可以构成一次。
输入两个整数 m 和 n,表示 0 和 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 。
解析:
本题也是一个01背包问题,而其特点在于它有两个背包,一个装0,另一个装1。
设置状态:dp[i][j][k]
表示装0背包容量为 j,装1背包容量为k的情况下,能够装入前 i 个物品中的几个
状态转移方程:仍旧划为装入当前物品和不装入当前物品两个子问题,不装入则dp[i][j][k]=dp[i-1][j][k]
,装入则dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-w0[i]][k-w1[i]] + 1)
,其中w0表示物品中0的体积,w1表示物品中1的体积。
class Solution {
public:
// 计算每个str中0和1的数量
pair<int,int> countWeight(string str){
int count0 = 0, count1 = 0;
for(int i=0;i<str.length();++i){
if(str[i] == '0'){
++count0;
}else{
++count1;
}
}
return make_pair(count0,count1);
}
int findMaxForm(vector<string>& strs, int m, int n) {
int len = strs.size();
// 用一个三维数组表示在前 i 个字符串中,使用 j 个 0 和 k 个 1 的情况下最多可以得到的字符串数量
vector<vector<vector<int>>> dp(len+1,vector<vector<int>>(m+1,vector<int>(n+1)));
for(int i=1;i<=len;++i){
auto count = countWeight(strs[i-1]);
// j 和 k 都从零开始,因为str存在'0'或'1'这种只有一种元素构成的情况
for(int j=0;j<=m;++j){
for(int k=0;k<=n;++k){
if(j >= count.first && k >= count.second){
dp[i][j][k] = max(dp[i-1][j][k],dp[i-1][j-count.first][k-count.second] + 1);
}else{
dp[i][j][k] = dp[i-1][j][k];
}
}
}
}
return dp[len][m][n];
}
};
322 零钱兑换
给定一些硬币的面额,求最少可以用多少颗硬币组成给定的金额。
输入一个一维整数数组,表示硬币的面额;以及一个整数,表示给定的金额。输出一个整数,表示满足条件的最少的硬币数量。若不存在解,则返回-1。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
解析:
因为每个硬币可以用无限多次,这道题本质上是完全背包问题。完全背包问题还是没搞懂,记录一下,以后仔细琢磨。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < (int)coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
参考资料
LeetCode 101:和你一起轻松刷题(C++) 第 7 章 深入浅出动态规划