代码随想录-背包问题
01 背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
二维dp数组01背包
定义:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
初始化:首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
使用:可以先遍历物品,再遍历背包容量
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
一维dp数组(滚动数组)
把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
定义:dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
初始化:全赋值为0即可,不会覆盖递推结果。
递推顺序:逆序,保证物品i只放入一次。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。示例 1:
输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]
此题能够通回溯算法进行枚举子序列进行判断是否有子序列之和等于sum/2。
01背包问题,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2,能否找到能装满背包的选取序列。如果nums[target]==target,集合中的子集总和正好可以凑成总和target。
bool canPartition(int* nums, int numsSize){
//dp[i]中的i表示背包内总和
int sum=0;
for(int i=0;i<numsSize;++i){
sum+=nums[i];
}
if(sum%2==1) return false;
int target=sum/2;
int dp[10002]={0};
for(int i=0;i<numsSize;++i){
for(int j=target;j>=nums[i];j--){
dp[j]=fmax(dp[j],dp[j-nums[i]]+nums[i]);
}
}
if(dp[target]==target) return true;
return false;
}
1049. 最后一块石头的重量 II
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
- 输入:[2,7,4,1,8,1]
- 输出:1
本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小。
dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]。 target=sum/2,要求的是能够装满target的最大重量。
int lastStoneWeightII(int* stones, int stonesSize) {
int sum=0;
for(int i=0;i<stonesSize;++i){
sum+=stones[i];
}
int target=sum/2;
int dp[15001]={0};
for(int i=0;i<stonesSize;++i){
for(int j=target;j>=stones[i];--j){
dp[j]=fmax(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return (sum-dp[target])-dp[target];
}
494. 目标和
给你一个非负整数数组
nums
和一个整数target
。向数组中的每个整数前添加'+'
或'-'
,然后串联起所有整数,可以构造一个 表达式 :例如,nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。返回可以通过上述方法构造的、运算结果等于
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
回溯解法:
int res;
void backtravel(int *nums,int numsSize,int target,int sum,int idx){
if(idx==numsSize){
if(sum==target){
res++;
}
}else{
backtravel(nums,numsSize,target,sum+nums[idx],idx+1);
backtravel(nums,numsSize,target,sum-nums[idx],idx+1);
}
}
int findTargetSumWays(int* nums, int numsSize, int target) {
res=0;
backtravel(nums,numsSize,target,0,0);
return res;
}
动态规划:
int findTargetSumWays(int* nums, int numsSize, int target) {
int sum=0;
for(int i=0;i<numsSize;++i) sum+=nums[i];
int diff=sum-target;
if(diff<0||diff%2==1) return 0;
diff/=2;
int dp[diff+1];
for(int i=0;i<diff+1;++i) dp[i]=0;
dp[0]=1;
for(int i=0;i<numsSize;++i){
for(int j=diff;j>=nums[i];--j){
dp[j]+=dp[j-nums[i]];
}
}
return dp[diff];
}
474. 一和零
给你一个二进制字符串数组
strs
和两个整数m
和n
。请你找出并返回strs
的最大子集的长度,该子集中 最多 有m
个0
和n
个1
。如果x
的所有元素也是y
的元素,集合x
是集合y
的 子集 。输入: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 。
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化0
for (string str : strs) { // 遍历物品
int oneNum = 0, zeroNum = 0;
for (char c : str) {
if (c == '0') zeroNum++;
else oneNum++;
}
for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
for (int j = n; j >= oneNum; j--) {
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
};
完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
518. 零钱兑换 II
给你一个整数数组
coins
表示不同面额的硬币,另给一个整数amount
表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回
0
。假设每一种面额的硬币有无限个。
输入: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[j]:凑成总金额j的货币组合数为dp[j]
求装满背包有几种方法,公式都是:dp[j] += dp[j - nums[i]];
dp[0] = 1 有没有含义,其实既可以说 凑成总金额0的货币组合数为1,也可以说 凑成总金额0的货币组合数为0,好像都没有毛病。
如果求组合数就是外层for循环遍历物品,内层for遍历背包.
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
本体要求的是组合数,{1,5}和{5,1}属于同一种情况。
int change(int amount, int* coins, int coinsSize) {
int dp[amount+1];
for(int i=0;i<=amount;++i){
dp[i]=0;
}
dp[0]=1;
for(int i=0;i<coinsSize;++i){
for(int j=coins[0];j<=amount;++j){
dp[j]+=dp[amount-coins[i]];
}
}
return dp[amount];
}
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组
nums
,和一个目标整数target
。请你从nums
中找出并返回总和为target
的元素组合的个数。输入: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的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
个数可以不限使用,说明这是一个完全背包。得到的集合是排列,说明需要考虑元素之间的顺序.本题要求的是排列,target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。
int combinationSum4(int* nums, int numsSize, int target) {
int dp[target+1];
for(int i=0;i<=target;++i){
dp[i]=0;
}
dp[0]=1;
for(int i=0;i<=target;++i){
for(int j=0;j<numsSize;++j){
if(i-nums[j]>=0&&dp[i]<INT_MAX-dp[i-nums[j]])
dp[i]+=dp[i-nums[j]];
}
}
return dp[target];
}