0-1背包理论基础
- 卡码网第46题
- 0-1背包问题最重要的是搞清楚dp数组的含义,
dp[i][j]的值表示从0-i件物品中选取不超过总体积j的物品能获得的最大的价值 - 掌握了上面的这个原则,我们就不难找出递推关系:
dp[i][j]=max(dp[i-1][j], dp[i-1][j-volume[i]]+value[i])- 据此可以写出代码:
#include<iostream>
#include<vector>
using namespace std;
#define M 5000
#define N 5001
int dp[M][N]; // M是物品种类,N是背包容量
int main(){
int m, n;
cin>>m>>n;
vector<int> volume(m);
vector<int> value(m);
for(int i=0; i<m; i++) cin>>volume[i];
for(int i=0; i<m; i++) cin>>value[i];
// initialize
// for(int i=0; i<m; i++) dp[i][0]=0;
// 这里也别漏了等号
for(int j=volume[0]; j<=n; j++) dp[0][j]=value[0];
for(int i=1; i<m; i++) // 这里不取等,因为物品数量是m-1
for(int j=1; j<=n; j++) //这里是背包容量取等
if(j<volume[i]) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],
dp[i-1][j-volume[i]]+value[i]);
cout<<dp[m-1][n];
}
- 一维滚动数组法(物品正序,容积倒序):
#include<iostream>
#include<vector>
using namespace std;
#define N 5001
int dp[N]; // M是物品种类,N是背包容量
int main(){
int m, n;
cin>>m>>n;
vector<int> volume(m);
vector<int> value(m);
for(int i=0; i<m; i++) cin>>volume[i];
for(int i=0; i<m; i++) cin>>value[i];
// initialize
// for(int i=0; i<m; i++) dp[i][0]=0;
// 这里也别漏了等号
for(int j=volume[0]; j<=n; j++) dp[j]=value[0];
for(int i=1; i<m; i++) // 这里不取等,因为物品数量是m-1
for(int j=n; j>=volume[i]; j--) //这里是背包容量取等
dp[j]=max(dp[j], dp[j-volume[i]]+value[i]);
cout<<dp[n];
}
分割等和子集
- 力扣题目链接
- 以前我们是通过回溯法找到,但是题目并不要求我们找出所有的方案
- 确定dp数组的含义,不超过sum/2的最大和,相等即返回true
bool canPartition(vector<int>& nums) {
int sum=0;
for(auto num:nums) sum+=num;
if(sum%2) return false;
vector<int> dp(sum/2+1, 0);
for(int j=nums[0]; j<=sum/2; j++) dp[j]=nums[0];
for(int i=1; i<nums.size(); i++)
for(int j=sum/2; j>=nums[i]; j--)
dp[j]=max(dp[j], dp[j-nums[i]]+nums[i]);
if(dp.back()==sum/2) return true;
else return false;
}
最后一块石头的重量Ⅱ
- 力扣题目链接
- 题目的解题思路是把石头分成差不多的两堆,差值就是最小的可能
int lastStoneWeightII(vector<int>& stones) {
int sum=accumulate(stones.begin(), stones.end(), 0);
vector<int> dp(sum/2+1, 0);
for(int j=stones[0]; j<=sum/2; j++) dp[j]=stones[0];
for(int i=1; i<stones.size(); i++)
for(int j=sum/2; j>=stones[i]; j--)
dp[j]=max(dp[j], dp[j-stones[i]]+stones[i]);
return (sum-dp[sum/2])-dp[sum/2];
}
目标和
-
既然为target,那么就一定有 left组合 - right组合 = target
left + right = sum,而sum是固定的。right = sum - left
公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2
-
确定递推公式
有哪些来源可以推出dp[j]呢?
只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。
例如:dp[j],j 为5,
- 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
- 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
- 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
- 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
- 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包
所以求组合类问题的公式,都是类似这种:
dp[j] += dp[j - nums[i]]
- 举例验证初始值:
如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。
int findTargetSumWays(vector<int>& nums, int target) {
int sum=accumulate(nums.begin(), nums.end(), 0);
if(abs(target)>sum) return 0;
if((target+sum)%2) return 0;
int bagSize=(target+sum)/2;
vector<int> dp(bagSize+1, 0);
dp[0]=1;
for(int i=0; i<nums.size(); i++)
for(int j=bagSize; j>=nums[i]; j--)
dp[j]+=dp[j-nums[i]];
return dp[bagSize];
}
一和零
- 力扣题目链接
- 这时dp数组应该是二维的,有1和0两个维度
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for(auto 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];
}
完全背包理论基础
- 卡码网第52题
- 01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次
- 而完全背包的物品是可以添加多次的,所以要从小到大去遍历
#include<iostream>
using namespace std;
#define N 10000 // 材料种类
#define V 10001 // 行李空间
int dp[V];
int main(){
int n, v; cin>>n>>v;
int weight, value;
for(int i=0; i<n; i++){
cin>>weight>>value;
for(int j=weight; j<=v; j++)
dp[j]=max(dp[j], dp[j-weight]+value);
}
cout<<dp[v];
}
零钱兑换Ⅱ
- 力扣题目链接
- 也是完全背包一道非常简单的题目(:
int change(int amount, vector<int>& coins) {
int result=0;
vector<int> dp(amount+1, 0);
dp[0]=1;
for(auto coin: coins)
for(int j=coin; j<=amount; j++)
dp[j]+=dp[j-coin];
return dp[amount];
}
组合总和Ⅳ
- 这题看似是组合,实际是排列!
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
- 还得考虑溢出的情况
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1, 0);
dp[0]=1;
for(int i=1; i<=target; i++)
for(auto num: nums)
if(i>=num && dp[i]<INT_MAX-dp[i-num])
dp[i]+=dp[i-num];
return dp[target];
}
爬楼梯(进阶版)
- 卡码网:57. 爬楼梯
- 完全背包+排列问题,同上题一样
#include<iostream>
#include<vector>
using namespace std;
int main(){
int n, m; cin>>n>>m;
vector<int> dp(n+1, 0);
dp[0]=1;
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(i>=j)
dp[i]+=dp[i-j];
cout<<dp[n];
}
零钱兑换
- 力扣题目链接
- 这道题无所谓什么顺序,但是注意初始化和递推公式
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, INT_MAX); // 区别
dp[0]=0;
for(auto coin: coins)
for(int j=coin; j<=amount; j++)
if(dp[j-coin]!=INT_MAX) // 如果dp[j-coin]是初始值则跳过
dp[j]=min(dp[j], dp[j-coin]+1);
return dp[amount]==INT_MAX ? -1 : dp[amount];
}
完全平方数
- 力扣题目链接
- dp[0]=0完全是为了递推公式
- 遍历顺序无所谓!
int numSquares(int n) {
vector<int> dp(n+1, INT_MAX);
dp[0]=0;
for(int i=1; i<=n; i++)
for(int j=1; j*j<=i; j++)
dp[i]=min(dp[i], dp[i-j*j]+1);
return dp[n];
}
参考资料
[1] 代码随想录
[2] Leetcode题解