1.背包dp
背包问题有多种其中比较基础的就是01背包
其它背包问题也都是在01背包的基础上添加了一些条件和限制,所以只要01背包理解了,后面的学习也会比较轻松。
1.背包dp(选择模型)(闫式分析法)
闫式dp分析法:是一种分析dp问题的方法,可以更好理解dp问题。
1.从集合角度来分析dp问题,有限集合中的最值,比如求一个集合中的最大值,或者最小值,
第一个阶段:状态表示:化零为整 1.dp[i] 表示一个集合,它代表什么,存的是一个数(只考虑前 i个物品)
2.属性:存的这个值和集合之间是什么关系,max,min。
第二个阶段:状态计算:化整为零 1.把f[i] 化为若干个部分(若干个子集,子集满足两个条件 1.是不重复,求数量时我们要满足不重复,当我们们求最大值的时候其实是可以重复的,就比如求a,b,c 三个数的最大值,我们先求a,b最大值,再求b,c最大值,b虽然是重复了,但是并不影响我们的最大值,2.不遗漏),每一个部分分别去求,就比如求dp[i] 的最大值,我们可以对每一个子集取max再相加。相当于我们把一个集合的问题分解为每一个子集,然后每一个子集我们都可以再处理一下。
划分的依据:找最后一个不同点(选最后一个物品的方法)。
dp的优化和分析是分开的这样思路会更加清晰,dp的优化都是对代码做等价变形。
其实dp就是一种递推,我们在做dp有关的题时,要选择好状态参量,同时要想办法构造子问题也就是状态转移方程。
1. 01背包问题
1.题意
有n件物品,每件物品体积是v,价值是w,每件物品只能使用一次,有一个容量为V的背包,物品总体积不超过背包容量,求出最大价值。
2.基本思路
最基础的背包问题,每种物品只有一件,可以选择放或不放。
用子问题定义状态:即dp[i] [j] 表示前i件物品恰放入一个容量为j的背包可以获得的最大价值,很明显我们要求最大价值,那么属性就为max。为什么我们要把体积v作为一个状态参量呢,因为,我们把一个物体放入体积为v的背包中,那么剩余的空间又是一个子问题。当前的最优解不仅和当前物品价值有关,还和剩余的空间能装多少价值的物品有关。例如,你把一个体积为 1的物体放入体积为10的背包中,那么问题就转为体积是9的背包的问题了。就是从一个状态转移到另一个状态。
集合的划分就是状态转移方程,那么对于第 i 个物品,我们就有选和不选两种方案,
1.如果不选: dp[i] [j]=dp[i-1] [j](物品的总数返回前面一个状态,当前容纳的重量不变)
2.如果选:dp[i] [j]=dp[i-1] [j-v[i]]+w[i](选当前物品,物品的总数返回前一个状态,当前可容纳的重量减去物品重量,并加上物品的价值),最后我们要对两种情况取max。
例如,第一个物品为(5,1),第二个物品为(4,2)(前面为体积,后面为价值),假如此时我们在求解背包体积为 9 的问题,那么对于第二个物品我们可以选择,那么**此时就为当前物品的价值加上背包剩余空间(9-4==5)所能选择的物品的价值。**所以如果选择当前物品,此时总价值为当前物品价值 w[i]+不选当前物品时背包剩余空间所能选择前 i-1个物品的价值 dp[i-1] [j-v[i]]+w[i]
下面一张图方便大家理解,
例如,当i 等于 0时,没有物品所以价值为0;当j 等于0时,背包容量为0。
当 i=1时背包容量 j=1 时,因为物品体积为2>1,所以第 i个物品无法选择,dp[i] [j]=dp[i-1] [j]=0;
当i=1 ,j=2 时,此时可以选择第 i个物品(背包容积 j>=v[i]),dp[i] [j]=max(dp[i-1] [j],dp[i] [ j-v[i]]+w[i]),就进行判断选和不选。以此类推,我们要一行一行的进行递推。为什么我们要从小往大求,因为大的问题求解需要用到子问题的答案。
例如,当背包容量为 6时,i==2时,对于第二个物品我们可以进行选择,那么此时价值为5,背包还剩3个空间,这三个空间可以选择前 i-1个物品;所以就变成了当背包为 3时,选择前 1个物品dp[1] [3]==3,所以dp[2] [6]=max(dp[i-1] [j],dp[i-1] [j-v[i]]+w[i])=max(dp[1] [6],dp[1] [3]+5)=8。
3.代码
1.下面是朴素代码
#include<bits/stdc++.h>
using namespace std;
int v[1005],w[1005];
int dp[1005][1005]; //代表在容量为j的情况下对前i个物品选择的最大价值
int main(){
int n,v1;
cin>>n>>v1;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;i++){
for(int j=0;j<=v1;j++){
//考虑第i个物品选和不选
dp[i][j]=dp[i-1][j]; //如果不选第i个物品,
//那么背包内的价值就等于前i-1个,背包容量不变
if(j>v[j]) //如果选第i个
dp[i][j]=max(dp[i-1][j],dp[i][j-v[j]]+w[i]);
//那么就判断一下选择和不选那个价值更大
}
}
cout<<dp[n][v1];
}
2.优化代码
我们发现dp[i] [j]都是从dp[i-1] [j]推过来的,但我们进行递推的过程就是从上一个状态转移过来的,所以我们可以把这一维给取消掉。但如果我们从前往后遍历的话,一个物品有可能会被计算多次。例如下面表格,当第一个物品为(5,1)时,背包体积为10 时,背包价值为 2,很明显我们选择第一个物品,那么剩余的体积为 10-5=5,那么体积为 5的背包价值为 1,那么就会多算。这个其实就是后面的完全背包,因为完全背包一个物品可以取多次,这也是完全背包体积正向枚举的原因。
所以我们可以从后面往前遍历,进行空间优化
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int dp[N],v[N],w[N]; //dp代表当体积下背包的最大价值
int main(){
int n,V;
cin>>n>>V;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=V;j>=v[i];j--){ //只需要遍历到v[i],因为后面比它小的肯定装不下
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout<<dp[V]; //所以dp[V]就为我们所求的
return 0;
}
2.完全背包
1.思路
和 01背包类似,只不过每个物品可以选择无数次。
第 i个不选(0个):dp[i] [j]=dp[i-1] [j]
第 i个选(多个):dp[i] [j]=dp[i-1] [j-k*v[i]]+k *w[i],选当前的物品,子问题就是不选当前物品时背包剩余空间所能选择前 i个物品的最大价值,k代表我们选几个当前物品,最后我们要对两种情况取max。
2.代码
1.朴素写法
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N][N];
int v[N],w[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;i++){
for(int k=0;k*v[i]<=j;k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
}
}
cout<<dp[n][m];
}
2.代码的优化
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w , …)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-3v]+2*w , …)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
知道了上面的关系,那么k循环就可以不要了,就可以优化为下面代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int dp[N][N];
int v[N],w[N];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
if(j>=v[i])
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
}
}
cout<<dp[n][m];
}
3.深层理解
我们会发现完全背包其实和 01背包很像,只有这一句不同(注意下标)
dp[i] [j] = max(dp[i] [j],dp[i-1] [j-v[i]]+w[i]); //01背包
dp[i] [j] = max(dp[i] [j],dp[i] [j-v[i]]+w[i]); //完全背包问题
其实就相当于我们可以重复选第 i个物品,而 01背包无法重复选,所以当背包选第 i个物品时,空间有剩余我们可以继续选择第 i个物品,而不是直接转化为选前 i-1个物品。
下面一张图方便大家理解
样例为
4 10 //背包空间为10
2 1 //前面为物品的体积,后面为物品的价值
3 3
4 6
8 10
例如,背包容量为 9,i=2时,我们可以选择第2个物品,此时价值为dp[2] [9-3]+3=9,其实就是背包剩余空间我们接着选择当前物品,也就是在当前层进行自我叠加。而01背包只能在上一层叠加。
我们发现 i=3时,和 i=4时,它们的状态一样,因为我们是先继承上一层的状态,然后进行比较看是选择当前物品结果更优,还是选择上一个物品更优。例如 i==4时,物品体积为 8,空间为小于 8时,我们无法进行选择,只能继承上一个状态。当空间为 8时,如果选择当前物品的话,价值为 10,如果不选当前物品的话,价值为 12,所以很明显继承上一个状态价值更大。
dp[i][j]=dp[i-1][j];
if(j>=v[i])
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
下面是两个讲解不错的视频,供大家查阅。