背包问题
0-1背包问题
1. 0-1背包问题
题目描述:
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
题目分析:
设f(n,w)前n件物品放到一个容量为w的背包中可以获得最大价值,
考虑我们的子问题,将前i件物品放到容量为v的背包中,若我们只考虑第i件物品时,它有两种选择,放或者不放。
如果第n件物品不放入背包中,那么问题就转换为:将前in- 1件物品放到容量为v的背包中,带来的收益f(n-1)
如果第n件物品能放入背包中,那么问题就转换为:将前n - 1件物品放到容量为w - weight[i]的背包中,带来的收益f(n-1) + cost[n]则有
代码
int bag_problem(const vector<int>&weight,const vector<int>&value,const int bagweight) {
int obj_size = weight.size();//物品数量
vector<vector<int>> dp(obj_size, vector<int>(bagweight+1, 0));//背包容量从0到bagweight
for (int k = weight[0]; k <= bagweight; k++) {
dp[0][k] = value[0];
}
for (int i = 1; i < obj_size; i++) {
for (int j = 1; j <= bagweight; j++) {
if (weight[i] > j) dp[i][j] = dp[i - 1][j];
else {
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}
}
}
return dp[obj_size-1][bagweight];
}
代码简化
上面的代码中,dp(i)的求解只与dp(i-1)有关,与dp(i-2),dp(i-3)....都无关,因此前面的dp(i-2),...都可以覆盖掉
因此可以只用一个一维的dp数组表示,
dp[j]表示从前i件物品选取任意物品装入容量为j的背包所得的最大价值;注意:为了便于理解前面还是加上了i,实际上dp[j]相当于前面二维dp的dp(i:0---n,j)
递推公式
i从0到i
因为求dp[j]依赖于dp[0到j]的值,因此,在i的一次循环内,应该先求j比较大的dp[j],再依次求j比较小的dp[j],这样才能保证求dp[j]的时候使用的是上一轮循环求得dp[0-j]的值,而不是在此轮循环被更新的dp[0-j]
int bag_problem(const vector<int>&weight, const vector<int>&value, const int bagweight) {
vector<int> dp(bagweight + 1, 0);//dp[j]表示在容量为j时能获得的最大价值
for (int i = 0; i < weight.size(); i++) {//依次求在0-i件物品随机选取放入容量为0-bagweight的背包的最大价值
for (int j = bagweight; j >= weight[i]; j--)
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
return dp[bagweight];
}
2.分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
分析
将一个数组划分为两个元素和相等的子集,首先,整个数组和得是偶数
设整个数组和为X,在整个数组里找某些元素使这些元素的和为X/2,找到了则返回true,反之返回false
可以类似与背包问题:
背包的体积为X / 2
背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
背包如果正好装满(达到最大价值),说明找到了总和为 sum / 2 的子集。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int ele : nums) sum += ele;
if (sum % 2 != 0) return false;
int bag_weight = sum / 2;
vector<vector<int>> dp(nums.size(), vector<int>(bag_weight + 1, 0));
for (int k = nums[0]; k <= bag_weight; k++) dp[0][k] = nums[0];
for (int i = 1; i < nums.size(); i++) {
for (int j = 1; j <= bag_weight; j++) {
if (j < nums[i]) dp[i][j] = dp[i - 1][j];
else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
}
}
}
return dp[nums.size() - 1][bag_weight] == bag_weight;
}
};
一维dp解决
物品重量:nums[1], nums[2],...,nums[n]
物品价值:nums[1], nums[2],...,nums[n]
背包容量:sum{num}/2
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int ele : nums) sum += ele;
if (sum % 2 != 0) return false;
int bag_weight = sum / 2;
vector<int> dp(bag_weight + 1, 0);
for (int i = 0; i < nums.size(); i++) {
for (int j = bag_weight; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[bag_weight] == bag_weight;
}
};
3.最后一块石头的重量 II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例 1:
输入:stones = [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],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
分析:
转换成背包问题,可以忽略价值这个属性,设石头总重为sum,任意选取石头使容量为sum/2的背包尽可能装满
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
auto sum = [](vector<int> nums)->int {int count = 0;for (int ele : nums) count += ele;return count;};
int sum_stones = sum(stones);
int bag_weight = sum_stones / 2;
vector<int> dp(bag_weight + 1, 0);
for (int i = 0; i < stones.size(); i++) {
for (int j = bag_weight; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
auto get_absolutevalues = [](int value) {if (value >= 0)return value; else return -value; };
return get_absolutevalues(sum_stones - dp[bag_weight] * 2);
}
};
4.目标和
给你一个整数数组 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
示例 2:
输入:nums = [1], target = 1
输出:1
分析
将元素分成+和-两组,假设+组记为left,-组记为right
则有: left-right=target,left+right=sum
则有:left=(target+sum)/2,问题转变为从数组中任意选取元素使这些元素的和为(target+sum)/2,有多少种不同的选法
dp[j] 表示:有dp[j]种方法填满容量为j的背包
dp[j]=dp[j-nums[i]]+dp[j]
代码:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//先计算sum
int sum = 0;
for (int ele : nums)sum += ele;
if((sum+target)%2!=0) return 0;
int target_sum = (sum + target) / 2;
if (target_sum < 0) return 0;
vector<vector<int>> dp(nums.size(), vector<int>(target_sum + 1));
if (nums[0] == 0) dp[0][0] = 2;
else dp[0][0] = 1;
if (nums[0] == 0)dp[0][nums[0]] = 2;
if (nums[0] != 0 && nums[0] <= target_sum) dp[0][nums[0]] = 1;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j <= target_sum; j++) {
if (j < nums[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
}
}
return dp[nums.size() - 1][target_sum];
}
};
边界处理心态要炸了,注意:
1.dp(0,0)根据第一个值是不是0可能等于1或者2
2. 如果target_sum<0,直接返回0,比如【100,100】,-400,dp(0,0)=1,dp(0,100)=1,dp(1,100)=dp(0,100)+dp(0,0)=2
当边界处理很麻烦时,尝试让dp数组从1开始记录,即1就表示第一个元素
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//先计算sum
int sum = 0;
for (int ele : nums)sum += ele;
if((sum+target)%2!=0) return 0;
int target_sum = (sum + target) / 2;
if (target_sum < 0) return 0;
vector<vector<int>> dp(nums.size()+1, vector<int>(target_sum + 1));
dp[0][0]=1;
for (int i = 1; i <= nums.size(); i++) {
for (int j = 0; j <= target_sum; j++) {
if (j < nums[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
}
}
return dp[nums.size()][target_sum];
}
};
一维dp
class Solution{
public:
int findTargetSumWays(vector<int>& nums, int target) {
auto sum = [](const vector<int>& vec) {int tmp = 0; for (int ele : vec)tmp += ele; return tmp; };
int vec_sum = sum(nums);
if (vec_sum < abs(target)) return 0;
if ((target+vec_sum) % 2 == 1) return 0;
int target_sum = (vec_sum + target) / 2;
vector<int> dp(target_sum + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = target_sum; j >= nums[i]; j--)
dp[j] = dp[j] + dp[j - nums[i]];
}
return dp[target_sum];
}
};
5.一和零
给你一个二进制字符串数组 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 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
分析
本题可以看作01背包问题的扩展,字符串数组里的每个字符串可以看作物品,每个物品有0和1两个属性
方法一:三维dp
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
//最多m个0,n个1
//dp(i,j,k)表示在前i个字符串任意选取,满足0个数小于m,1个数小于n的最大子集长度
vector<vector<vector<int>>> dp(strs.size() + 1, vector<vector<int>>(m + 1, vector<int>(n + 1, 0)));
for (int i = 1; i <= strs.size(); i++) {
//计算strs[i-1]的0和1的个数
int zero_num = 0, one_num = 0;
for (char ch : strs[i-1]) {
if (ch == '0') zero_num++;
else one_num++;
}
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= n; k++) {
if (j >= zero_num && k >= one_num)
dp[i][j][k] = max(1 + dp[i - 1][j - zero_num][k - one_num], dp[i - 1][j][k]);
else
dp[i][j][k] = dp[i - 1][j][k];
}
}
}
return dp[strs.size()][m][n];
}
};
dp[i][j]:最多有i个1和j个0的strs的最大子集的大小为dp[i][j]
dp(m,n)= max{dp(m,n) dp(m-zeronum, n-onenum)+1}
class Solution {
public:
struct Weight {
int zero_num;
int one_num;
};
int findMaxForm(vector<string>& strs, int m, int n) {
vector < vector<int>> dp(m+1,vector<int>(n+1,0));
//便利物品
dp[0][0] = 0;
for (string str : strs) {
int zero_num = 0, one_num = 0;
for (char ele : str) {
if (ele == '1') one_num++;
else zero_num++;
}
for (int i = m; i >= zero_num ; i--) {
for (int j = n; j >= one_num; j -- )
dp[i][j] = max(dp[i][j], 1+dp[i - zero_num][j - one_num]);
}
}
return dp[m][n];
}
};
完全背包问题
物品为:
重量 | 价值 | |
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
每件商品都有无限个!
问背包能背的物品最大价值是多少?
分析,与0-1背包问题的区别在于完全背包问题的物品是不限制物品件数的
递推公式
dp(i,j)依然表示从前i件物品中任意选择,在满足重量约束j的前提下能够达到的最大价值
dp(i,j)=max{dp(i-1,j),dp(i,j-w[i])+v[i]}
递推关系解释:
当到第i件物品时,有两个选择,装入第i个物品,或者是不装入
不装入则:dp(i,j)=dp(i-1,j)
装入则:dp(i,j)=dp(i,j-w[i])+v[i]
1.零钱兑换II
给你一个整数数组 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
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10]
输出:1
分析:关键词无限硬币,完全背包问题
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<vector<int>> dp(coins.size()+1, vector<int>(amount + 1,0));
dp[0][0] = 1;
for (int i = 1; i <= coins.size(); i++) {
for (int j = 0; j <= amount; j++) {
if (coins[i-1] > j) dp[i][j] = dp[i - 1][j];
else {
dp[i][j] = dp[i - 1][j]+dp[i][j - coins[i-1]];
}
}
}
return dp[coins.size()][amount];
}
};
滚动数组
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<vector<int>> dp(coins.size()+1, vector<int>(amount + 1,0));
dp[0][0] = 1;
for (int i = 1; i <= coins.size(); i++) {
for (int j = 0; j <= amount; j++) {
if (coins[i-1] > j) dp[i][j] = dp[i - 1][j];
else {
dp[i][j] = dp[i - 1][j]+dp[i][j - coins[i-1]];
}
}
}
return dp[coins.size()][amount];
}
};
2.零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
分析:完全背包,前一题求凑成总金额的组合数,这题求凑整总金额的最小硬币数
dp(i,j)表示在前i个不同面额硬币任意选取凑成金额为j的最小硬币数
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {//
vector<vector<int>> dp(coins.size(), vector<int>(amount + 1));
for (int i = 0; i < coins.size(); i++) {
if (i == 0) {
for (int j = 0; j <= amount; j++) {
if (j%coins[0] == 0) dp[i][j] = j / coins[0];
else dp[i][j] = -1;
}
continue;
}
for (int j = 0; j <= amount; j++) {
if (coins[i] > j)
dp[i][j] = dp[i - 1][j];
else {
if(dp[i][j-coins[i]]!=-1&& dp[i - 1][j]!=-1)
dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1);
else if(dp[i][j-coins[i]]==-1&& dp[i - 1][j]==-1) dp[i][j]=-1;//两个都等于-1
else dp[i][j] = max(dp[i - 1][j], dp[i][j - coins[i]] + 1);//有一个等于-1或
}
}
}
return dp[coins.size()-1][amount];
}
};
代码简化
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {//
vector<vector<int>> dp(coins.size() + 1, vector<int>(amount + 1, INT_MAX));
for (int k = 0; k <= coins.size(); k++) dp[k][0] = 0;
for (int i = 1; i <= coins.size(); i++) {
for (int j = 0; j <= amount; j++) {
if (coins[i - 1] > j)
dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i][j - coins[i - 1]] == INT_MAX ? dp[i - 1][j] : min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
}
}
return dp[coins.size()][amount]==INT_MAX?-1:dp[coins.size()][amount];
}
};
3.完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
分析
这一题和上一题零钱很像:1.这一题是在一堆完全平方数中找到最少数量的完全平方数的和为目标值 2.上一题零钱是在一堆零钱中找到最少数量的零钱使其和等于目标值 3。两题都不限定每种数/零钱的数量,都是完全背包问题
因此,本题首先得把完全平方数列出来
class Solution {
public:
int numSquares(int n) {
//列出小于n的完全平方数
vector<int> nums;
for (int i = 1; i <= n; i++) {
if (sqrt(i)==(int)sqrt(i)) nums.push_back(i);
}
//完全背包
vector<vector<int>> dp(nums.size() + 1, vector<int>(n + 1, INT_MAX));
for (int k = 0; k <= nums.size(); k++) dp[k][0] = 0;
for (int i = 1; i <= nums.size(); i++) {
for (int j = 0; j <= n; j++) {
if (nums[i - 1] > j) dp[i][j] = dp[i - 1][j];
else {
dp[i][j] = dp[i][j - nums[i - 1]]==INT_MAX?dp[i-1][j]: min(dp[i-1][j], dp[i][j - nums[i - 1]] + 1);
}
}
}
//return dp[nums.size()][n]==INT_MAX?-1: dp[nums.size()][n];//因为1的存在一定能凑成
return dp[nums.size()][n];
}
};
4.单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
分析
完全背包排列问题
对字符串s,设dp(i)表示子串(1---i)能否被字典里的单词拼出,其值有true和false
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.size()+1, false);
unordered_set<string> Dict(wordDict.begin(),wordDict.end());
dp[0] = true;
for (int i = 1; i <= s.size(); i++) {//遍历s
//如果(j,i)子串在字典里且dp(j)为true,则dp(i)为true
for (int j = 0; j < i; j++) {
if (dp[j]) {
string s_sub = s.substr(j, i-j);
if (Dict.find(s_sub) != Dict.end()) {
dp[i] = true;
break;
}
}
}
}
return dp[s.size()];
}
};
5.组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 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)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
分析,完全背包问题
求得是排列数目,与前面求排列数不一样
定义dp[i]表示和为i的组合个数,那么这个dp[i]怎么求呢。举个例子,比如i是5,如果要找和为5的组合,
我们可以用和为1的组合加上一个4或者用和为2的组合加上3或者用和为3的组合加上2或者用和为4的组合加上1而和为1,2,3,4的组合个数分别是dp[1],dp[2],dp[3],dp[4]。
所以和为5的组合个数就是他们几个的和,也就是dp[5]=dp[1]+dp[2]+dp[3]+dp[4];
但这里会有个问题,假如数组nums中没有2,我们是没法用和为3的组合加上2的,因为这里根本就没有2可选。那我们能选的数字有哪些呢,其实就是数组nums中元素。所以上面的推理我们再来改一下
如果要求dp[i]
我们可以用和为i-nums[0]的组合再加上nums[0]或者用和为i-nums[1]的组合再加上nums[1]或者用和为i-nums[2]的组合再加上nums[2]………所以dp[i]的组合个数就是他们的累加,也就是dp[i]=dp[i-nums[0]]+dp[i-nums[1]]+……。
但这里还要注意一点,就是i-nums[j]不能小于0
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
//dp(i)表示能凑成i的排列数
//dp[i]=dp[i-nums[0]]+dp[i-nums[1]]+……。
vector<unsigned long long> dp(target + 1, 0);
dp[0] = 1;
for (int i = 1; i <= target; i++) {//对目标值进行遍历
for (int j = 1; j <= nums.size(); j++)//对数组进行遍历
if (i>=nums[j - 1]) {
dp[i] += dp[i - nums[j - 1]];
}
}
return dp[target];
}
};