0-1背包:
就是给定n个物品(每个物品只有一件),和一个容量为C的背包,每个物品的价值为v,重量为w,每个物品只可以选择放入(1)或不放入(0),让背包中的物品价值最大。
例如:一个背包容量为C=10,有n=5个物品。
价值v | 6 | 3 | 5 | 4 | 6 |
重量w | 2 | 2 | 6 | 5 | 4 |
用子问题定义状态,即前i件物品恰好放入一个容量为j的背包可以获得的最大价值,则状态转移方程为
dp[i][j]=max(dp[i-1][j],dp[i-1][j-wi]+vi)
这个方程非常重要,基本上 所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要详细讲解一下:”将前i件物品放入容量为j的背包中“这个子问题,若只考虑第i件物品的策略(放或者不放),那么就可以转化为一个只和前i-1件物品相关的问题。如果不放第i件物品,那么问题就可以转化为”前i-1件物品放入容量为j的背包中“,价值为dp[i-1][j];如果放入第i件物品,那么问题就可以转化为”前i-1件物品放入剩下的容量为j-wi的背包中“,此时能获得的最大价值就是dp[i-1][j-wi],再加上通过放入第i件物品获得的价值vi
dp[i][j]为当前需要判断的物品,可以选择放或者不放,不放的话价值为dp[i-1][j] ,放的话价值为dp[i-1][j-wi]+vi,比较这两个的大小,选择大的价值。
这个表为状态的转移过程,也是最后dp数组的状态。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
1 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 9 | 9 | 9 |
3 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 11 | 11 | 14 |
4 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 10 | 11 | 13 | 14 |
5 | 0 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
这个方法不但可以计算出背包容量为十的最优方案,背包容量小于十的背包都可以计算出来。
for( int i = 1; i <= n); i++)
{
for( int j = 1; j<=10; 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]);
}
}
明显的,时间复杂度是O(n*C)
但是我们还能将空间复杂度降低,从二维降为一维。
看下面这段代码:
memset(dp, 0, sizeof(dp));
for(int i=0; i<n; i++){
for(int j=c; j>=w[i]; j--){
dp[j] = max(dp[j], dp[j-w[i]]+v[i])
}
}
如何理解二维降一维呢?对于外层循环中的每一个i值,其实都是不需要记录的,在第i次循环时,所有的dp[0…c]都还未更新时,dp[j]还记录着前i-1个物品在容量为j时的最大价值,这样就相当于还记录着dp[i-1][j]和dp[i-1][j-w[i]]+v[i]。
关键是内循环中为什么是逆序的呢,因为要记算当前状态的dp[j],需要的是前一状态的dp[j](即dp [j-1]),而逆序循环时前面的一定就是前一状态的dp[j],可以直接拿来用,而正序循环之所以不可以,是因为当你计算完前面的dp[j]时,dp[j-1]存的就不是i-1时刻的值了,而你后面还要用dp[j-1]。当内循环是逆序时,就可以保证后一个dp[j]和dp[j-w[i]]+v[i]是前一状态的!
完全背包:
就是给定n个物品(单个物品无限件),和一个容量为C的背包,每个物品的价值为v,重量为w,每个物品只可以选择放入(1)或不放入(0),让背包中的物品价值最大。
memset(dp, 0, sizeof(dp));
for(int i=0; i<n; i++){
for(int j=w[i]; j<=c; j++){
dp[j] = max(dp[j], dp[j-w[i]]+v[i])
}
}
完全背包相比较于0-1背包,只需要将内循环由逆序遍历改为正序遍历就好了
为什么0-1背包的是逆序而完全背包的是正序的呢?
举个例子
假设n=1,背包容量c=5,只有一个物品,w和v都为1。
0-1背包:
0 | 1 | 2 | 3 | 4 | 5 | |
1 | 0 | 1 | 1 | 1 | 1 | 1 |
完全背包:
0 | 1 | 2 | 3 | 4 | 5 | |
1 | 0 | 1 | 2 | 3 | 4 | 5 |
0-1背包因为每个物品只有一件,所以最后答案为1,而完全背包答案为5,当0-1背包逆序时,你每次用到的dp[j-1]都是0,完全背包正序时,每次用的dp[j-1]是当前状态的dp[j],这里我们要添加的不是前一个背包,而是当前背包。
多重背包:
就是给定n个物品(单个物品有限件),和一个容量为C的背包,每个物品的价值为v,重量为w,每个物品只可以选择放入(1)或不放入(0),让背包中的物品价值最大。
多重背包相当于0-1背包和完全背包的结合,当一个物品的数量*重量大于背包容量时,这个物品相对于这个背包就是无限的件的,即完全背包问题,解法和完全背包一致;小于时,0-1背包中允许放入的物品有重复,即0-1背包中如果考虑要放入的物品的重量和价格相同,不影响最终的结果,因为我们可以考虑把多重背包问题中限制数目的物品拆分成单独的一件件物品,作为0-1背包问题考虑。问题解法和0-1背包一致。
二进制分解思想:
例如有22个重量和价值都为1的物品,如果按照常规方法的话,需要循环22次,每次放入一个重量质量都为1的物品,而用二进制分解的话,可以将22个物品转化为5个物品,5个物品的重量和价值为1,2,4,8,7;这样致需要循环5次就可以了。
把22进行二进制拆分:
成为1,2,4,8,7;由1,2,4,8可以组成1--15之间所有的数,而对于16--22之间的数,可以先减去剩余的7,那就是1--15之间的数可以用1,2,4,8表示了。
int k = 1;
while(k<=number)
{
ZeroOnePack(k*weight,k*value);
number = number-k;
k = 2*k;//这里采用二进制思想
}
ZeroOnePack(number*weight,number*value);
万能模板:
#include <stdio.h>
#include <iostream>
#include <algorithm>
#include <cstring>
#define MAX 1000000
using namespace std;
int dp[MAX];//存储最后背包最大能存多少
int value[MAX],weight[MAX],number[MAX];//分别存的是物品的价值,每一个的重量以及数量
int bag;
void ZeroOnePack(int weight,int value )//01背包
{
int i;
for(i = bag; i>=weight; i--)
{
dp[i] = max(dp[i],dp[i-weight]+value);
}
}
void CompletePack(int weight,int value)//完全背包
{
int i;
for(i = weight; i<=bag; i++)
{
dp[i] = max(dp[i],dp[i-weight]+value);
}
}
void MultiplePack(int weight,int value,int number)//多重背包
{
if(bag<=number*weight)//如果总容量比这个物品的容量要小,那么这个物品可以直到取完,相当于完全背包
{
CompletePack(weight,value);
return ;
}
else//否则就将多重背包转化为01背包
{
int k = 1;
while(k<=number)
{
ZeroOnePack(k*weight,k*value);
number = number-k;
k = 2*k;//这里采用二进制思想
}
ZeroOnePack(number*weight,number*value);
}
}
int main()
{
int n;
while(~scanf("%d%d",&bag,&n))
{
int i,sum=0;
for(i = 0; i<n; i++)
{
scanf("%d",&number[i]);//输入数量
scanf("%d",&value[i]);//输入价值 此题没有物品的重量,可以理解为体积和价值相等
}
memset(dp,0,sizeof(dp));
for(i = 0; i<n; i++)
{
MultiplePack(value[i],value[i],number[i]);//调用多重背包,注意穿参的时候分别是重量,价值和数量
}
cout<<dp[bag]<<endl;
}
return 0;
}