01背包,完全背包,多重背包详解
01背包
01背包就是给n个物体,每个物体都有代价w[i] 和 价值 v[i];
给定一个容量为t的背包,每个物体只能选一次装入背包,所能得到的最大价值是多少
动态规划就是找状态转移方程,
dp[ i ] [ j ]=max ( dp [ i - 1 ] [ j ] , dp [ i -1 ] [ j - w [ i ] ]+ v [ i ] )
dp[ i ] [ j ] 表示从前 i 个物品中挑选物品放入容量为 j 的背包可以获得的最大价值。
就是在当前状态有两种选择;
-
不选择当前物品 :即当前的i个物品在容量为j的背包里的最大价值就等于前i-1个物品在容量为j的背包里的最大价值 dp[i] [j]=dp[i-1] [j]
-
选择当前物品 :那当前的i个物品在容量为j的背包里的最大价值就等于前i-1个物品在容量为j-w[i]的背包里的最大价值加上当前物品的价值 dp[ i ] [ j ]=d[ i - 1 ] [ j - w[ i ] ]+v[ i ]
代码
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
for(int j=1;j<=t;j++){
if (j < w[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
}
将二维优化为一维
dp[ i ] [ j ]=max ( dp [ i - 1 ] [ j ] , dp [ i -1 ] [ j - w [ i ] ]+ v [ i ] )
从上式中可以看出dp[i] []只与dp[i-1] [] 有关,所以可以进行优化为一维
dp [ j ]=max ( dp [ j ] , dp [ j - w [ i ] ]+ v [ i ] )
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
for(int j=t;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
逆序是因为每一个物品只能选一次,如果正序遍历,则前面的值将有可能被修改掉,从而导致后面(dp [j-w[i]], j-w[i] 等于前面改变的那个值)要用到这个值的数据的错误;相反如果逆序遍历,先修改后面的数据再修改前面的数据,此种情况就不会出错了;这个时候就可以联系到完全背包了,因为完全背包是可以选无限次,可以不断修改,所以就是正向了。
初始化
-
背包需完全填满
那么在初始化时除了dp[0]为0其它均设为- ∞ \infty ∞ (- ∞ \infty ∞ 即没有方案满足要求)这样就可以保证最终得到的是一种恰好装满背包的最优解。那么这是为什么呢,如果最终有方案满足背包完全填满,那么dp[0]就存在,初始化dp数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的物品恰好装满”,所以其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是- ∞ \infty ∞ 了。
-
背包不需完全填满
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时 状态的值也就全部为0了
回溯找最优解选了哪些物品
- dp[i] [j]=dp[i-1] [j] 时,说明没有选择第i 个商品,则回到dp[i-1] [j];
2. dp[i] [j]=dp[i-1] [j-w[i]]+v[i]时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到dp[i-1] [j-w[i]] 的位置。
3. 一直遍历到i=0结束为止,所有解的组成都会找到。
< https://www.luogu.com.cn/problem/P1048 > 采药 模板题
完全背包
有n种物品和一个容量为t的背包,每一种物品都有无限件可用。每一种物品都有代价w[i]和价值v[i]。求解将哪些物品放入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
两种状态转移方程
- dp[i] [j]=max( dp[i-1] [j] , dp[i-1] [j-k × \times ×w[i]]+k × \times ×v[i];
- dp[i] [j]=max(dp[i-1] [j],dp[i-1] [j-w[i]]+v[i] ) //优化后
完全背包样式和01背包的很相似,唯一不同的是对V遍历时变为正序,而01背包为逆序。01背包中逆序是因为dp[i] []只和dp[i-1] []有关,且第i件的物品加入不会对dp[i-1] [ ]状态造成影响。而完全背包则考虑的是第i种物品的出现的问题,第i种物品一旦出现它势必应该对第i种物品还没出现的各状态造成影响。也就是说,原来没有第i种物品的情况下可能有一个最优解,现在第i种物品出现了,而它的加入有可能得到更优解,所以之前的状态需要进行改变,故需要正序。
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
for(int j=w[i];j<=t;j++){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
< https://www.luogu.com.cn/problem/P1616 > 疯狂的采药 模板题
多重背包
有n种物品和一个容量为t的背包,第i种物品最多有n[i]件可用,每件费用是w[i],价值是v[i]。求解将哪些物品装入背包价值最大。
多重背包和完全背包又很相似,一个是选无限次,一个是选有限次。
状态转移方程
dp[i] [j] = max(dp[i-1] [j] , dp[i-1] [j-k × \times ×w[i]]+k × \times ×v[i]); (0=<k<=n[i])
直接用这个状态转移方程时间复杂度就有点高了 (t × \times × ∑ n [ i ] \sum n[i] ∑n[i])
我们可以对其进行优化,其实也挺好想,就是将它拆成01背包来求,比如说第2种物品有3件,那我们可以把这三件物品当成不同的物品放入01背包。但是又有一个问题,那就是怎么拆。一个一个的拆?那样时间复杂度显然没变还是(t × \times × ∑ n [ i ] \sum n[i] ∑n[i]) 。
正确的拆法是用二进制来拆,具体看代码
int cnt=1;
cin>>n>>t;
for(int i=1;i<=n;i++){
scanf("%d%d%d",&a,&b,&s);//a表示当前物品的代价,b表示价值,s表示数量
int m=1;
while(m<=s){
s-=m;
w[cnt]=m*a,v[cnt++]=m*b;//分成用每次乘二表示的数打包成一件物品成一项加入数组
m*=2;
}
if(s>0){
w[cnt]=s*a,v[cnt++]=s*b;//剩下的单成以项,加入数组
}
}
//下面就是01背包
for(int i=1;i<cnt;i++){
for(int j=t;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
< https://www.luogu.com.cn/problem/P1776 > 宝物筛选