01背包问题
- 按照状态找:
dp[i][w] 表示对于前i个物品,容量为w的背包的最大价值
dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i-1]] + val[i-1]); - 到达当前dp[i][w]状态的情况综合,放入第i个物品,或者不放第i物品
- 先凑答案就是dp[N][W]的情况,这时候dp的下标是比单个val和wt的下标大一个的,因为前者是size,后者是下标
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
vector<vector<int>> dp(N+1,vector<int>(W+1,0));
for(int i=1; i<=N; i++){
for(int w=1; w<=W; w++){
// 前置条件 w-wt[i-1]不能小于0,也就是存不下第i个,那只能选不存第i个的情况
if (w - wt[i-1] <0){
dp[i][w] = dp[i-1][w];
}
dp[i][w] = max(dp[i-1][w],
dp[i-1][w-wt[i-1]] + val[i-1]);
}
}
return dp[N][W];
}
完全背包问题
- 找状态 dp[i][j]表示使用前 i 种物品,填满 j 容量背包有几种方法
- 状态转移:全存前 i-1;加入第 i 种,那要看之前缺 i 元的情况数
dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]]
class Solution {
public:
int change(int amount,vector<int>& coins){
int n =coins.size();
vector<vector<int>> dp(n+1,vector<int>(amount+1));
// base case
for(int i=0; i<=n; i++){
dp[i][0] = 1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=amount;j++){
// coins[i-1]不能大于j,否则往前推就小于0了,一个也存不下,只可能是等于取i-1中物品的情况数
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[n][amount];
}
};
子集背包问题:分割等和子集
- 先看看总和是不是奇数,奇数不能划分
- 问题转化为求找到 和为sum/2的子串
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(auto num:nums) sum+=num;
if(sum&1) return false;
sum /= 2;
int n=nums.size();
// 前i个数字,完全背包容量j
vector<vector<int>> dp(n+1,vector<int>(sum+1,false));
//base case
for(int i=0;i<=n;i++){
dp[i][0] = true;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=sum;j++){
if(j < nums[i-1]){
dp[i][j]=dp[i-1][j];
}else{
dp[i][j]= dp[i-1][j] || dp[i-1][j-nums[i-1]];//这里不同于完全背包问题,不能dp[i-1][j-coins[i-1]],因为新加的元素不是无限使用的。
}
}
}
return dp[n][sum];
}
};
dp数组空间压缩
- 当dp是二维数组[i][j],但是状态转移只和上一次的一整条 j 长的数组已经当秋安元素的相邻元素有关,那可以把 维度 i 去掉,改为[j]。 然后去修改状态转移方程和 base case 的写法,其中最主要是考虑更新dp数组的时候可能把状态转移需要的一些元素给覆盖了。重点需要考虑的是:
- 空间压缩实际上是往第二个维度的轴上投影,按这种原则来修改状态转移方程和base case。
- 考虑遍历的方向,如果这一排在做状态转移,值依赖上一排的比 j 先前位置的元素,那应该 j 考虑从后往前dp。
比如说上面那个分割等和子集(值只依赖前一排,而且依赖前一排比 j 前面的元素):
原dp:
for(int i=1;i<=n;i++){
for(int j=1;j<=sum;j++){
if(j < nums[i-1]){
dp[i][j]=dp[i-1][j];
}else{
// 这里要用到的前一排比dp[i-1][j]靠前的元素,所以为了不事先破坏这个考前的元素,dp空间压缩的时候需要从后往前遍历
dp[i][j]= dp[i-1][j] || dp[i-1][j-nums[i-1]]
}
}
}
空间压缩dp:
for(int i=1;i<=n;i++){
for(int j=sum;j >= 0;j--){
if(j >= nums[i-1]){
// 这里要用到的前一排比dp[i-1][j]靠前的元素,所以为了不事先破坏这个考前的元素,dp空间压缩的时候需要从后往前遍历
dp[j]= dp[j] || dp[j-nums[i-1]];
}
}
}
- 如果状态转移的得到的值不仅依赖上一行,而且依赖本行邻接左右的值,那需要考虑覆盖问题。
这种情况分为两种: 一种是本行依赖的在遍历方向的上游,那就是在上次状态转移的时候把本行依赖的先前的值用temp存起来,状态转换转移的时候用,在本格计算玩存。 另一种是在遍历方向的下游,那就要转变遍历方向,然后重新考虑上一种情况
比如说考虑这个4要结合3和2考虑,遍历方向是从下往上,从左往右,那就是第一种情况,存起来上游的那个值就行
|3|4|
|1|2|
for (int i = n - 2; i >= 0; i--) {
int pre = 0;
for (int j = i + 1; j < n; j++) {
int temp = dp[j];
// 状态转移方程
if (s[i] == s[j])
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j - 1]);
pre = temp;
}
}