背包问题:一个背包总容量为w, 现在有n个物品, 第i个物品容量为weight[i], 价值为value[i], 现在往背包里面装东西, 怎样装才能使背包内物品总价值最大。
主要分为3类:
1. 0-1背包, 每个物品只能取0个,或者1个.
2. 完全背包, 每个物品可以取无限次.
3. 多重背包, 每种物品都有个数限制, 第i个物品最多可以为num[i]个.
背包问题多用动态规划求解,动态规划最重要的就是找到递推公式。
0-1背包
0-1背包指每一种物品都只有一件,可以选择放或者不放。现在假设有n件物品,背包承重为w。
设置二维数组dp,dp[i][j]表示,背包的容量为j,物品的数量为i的情况下,背包内物品的最大价值。
dp[i][j]分两种情况,第一种情况是不放入第i个物品,dp[i][j] = dp[i-1][j];
第二种情况是放入第i个物品,其中放入第i个物品需要weight[i]的空间,故dp[i][j] = dp[i-1][ j-weight[i] ]+value[i]
此时需要注意 j >= weight[i];
故dp[i][j] = max( dp[i-1][j] , dp[i-1][ j-weight[i] ]+value[i] ) ; 当 j >=weight[i]时
dp[i][j] = dp[i-1][j] ; 当j < weight[i]时;
现假设n和w都是非常大的数字,这样二维数组会占用很多的空间,但是,
发现dp[i][j]只和dp[i-1]行有关系,并且只和dp[i-1][<=j]有关系,故转为一维存储
公式变为dp[j] = max(dp[j] , dp[ j-weight[i] ]+value[i]),等号左边的dp[j]为dp[i][j],等号右边的dp[j]为dp[i-1][j];
影响dp[j]的元素为 dp[i-1][=j] 和 dp[i-1][ <j ]
/*
0-1背包问题
这个题目采取动态规划的思想
max dp[num][weight] num为物品的数量 weight为背包可以容纳的重量
dp[i][j]数组存储的是 i个物品,容量为j的情况下,能容纳的最大价值
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
dp[][0] =0;
dp[0][] = 0;
分析公式,可以发现dp[i][j]只和dp[i-1]行有关系,并且只和dp[i-1][<=j]有关系,故转为一维存储
*/
#include<iostream>
#include<cstring>
using namespace std;
const int maxN = 100;
int main(){
int n,w;//一共num个物品
int weight[maxN+1];
int value[maxN+1];
cin>>n>>w;
for(int i = 1; i <= n; i++){
cin>>weight[i]>>value[i];
}
int dp[maxN+1];
memset(dp,0,sizeof(dp));
for(int i = 1; i <= n;i++){ //i表示物品
for(int j = w; j >= weight[i]; j--){ //j表示重量 这个循环必须这样写
dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
//注意这个公式
//j倒着循环,为了防止第i行的dp[j]影响改行后面的dp[j]
//修改后面的,不会对前面造成影响
}
}
cout<<dp[w]<<endl;
}
这个代码需要注意j的循环方向,解释如下:
//for example i = 4,weight[i] = 3 ,j=3时
//更新了dp[3] = dp[3-3]+value[4],此时的dp[3]为dp[4][3];
//j=weight[4]+3=6的时候,dp[6] = mnax(dp[6],dp[3]+value[3]),
//而在这里的dp[3]应该为dp[i-1][3] = dp[3][3],
//用这个循环方式,其实是完全背包问题...
总结就是dp[j] = max(dp[j] , dp[ j-weight[i] ]+value[i]),等号右边的dp[j]必须为dp[i-1][j],若顺序执行,当dp[j]需要使用上一行的d[j-weight[i]]的时候,可能已经修改过了。
完全背包问题
每个物品可以取无限次.
分析同上
dp[i][j] = max( dp[i-1][j] , dp[i-1][ j- k*weight[i] ]+value[i]*k ) 0<=k<=j/weight[i];
化为一维dp[j] = max(dp[j] , dp[ j-weight[i] ]+value[i]),等号左边的dp[j]为dp[i][j],等号右边的dp[j]为dp[i][j];
影响dp[j]的元素为 dp[i-1][=j] 和 dp[i][ <j ]
dp[i][j]表示,背包的容量为j,物品的数量为i的情况下,背包内物品的最大价值。
/*
完全背包问题
这个题目采取动态规划的思想
max dp[num][weight] num为物品的数量 weight为背包可以容纳的重量
dp[i][j]数组存储的是 i个物品,容量为j的情况下,能容纳的最大价值
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
dp[][0] =0;
dp[0][] = 0;
分析公式,可以发现dp[i][j]只和dp[i-1]行有关系,并且只和dp[i-1][<=j]有关系,故转为一维存储
*/
#include<iostream>
#include<cstring>
using namespace std;
const int maxN = 100;
int main(){
int n,w;//一共num个物品
int weight[maxN+1];
int value[maxN+1];
cin>>n>>w;
for(int i = 1; i <= n; i++){
cin>>weight[i]>>value[i];
}
int dp[maxN+1];
memset(dp,0,sizeof(dp));
for(int i = 1; i <= n;i++){ //i表示物品
for(int j = weight[i]; j <= w; j++){ //j表示重量 这个循环必须这样写
dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
}
}
cout<<dp[w]<<endl;
}
另外一个版本的递归公式,只是写法不一样
d[i][j] = max(d[i-1][j] , d[i][ j-weight[i] ] + value[i]);
注意这个和0-1背包d[i][j] = max(d[i-1][j] , d[i-1][ j-weight[i] ] + value[i]); 的区别
这两个公式的对比,充分的说明了影响d[i][j]元素中的d[?][<j]到底来自于哪一行
0-1背包的d[i][j]和d[i-1]行有关系,而完全背包的d[i][j]和d[i]行有关系,
故0-1背包dp[j]从大往小修改,完全背包从小往大修改。
多重背包
每种物品都有个数限制, 第i个物品最多可以为num[i]个
首先分析,多重背包问题可以转换成0-1背包问题,把每一个物品展开成num[i]个
如果把全部的该物品i装进去已经超重,此时转换成完全背包问题;否则,转换成0-1背包问题。
采取二进制优化:k取值依次为1 2 4 8....
对于物品i,d[i][j]表示背包的容量为j的情况下,背包内物品的最大价值。
当k=1时,假设存在物品i1,质量为weight[i1] = k*weight[i];d[i1][j] 含有 包含[0,1]个i物品 的情况,并且取了容量为j的时候的最大值,num[i] -= k;从总数中去掉
当k=2时,假设存在物品i2,质量为weight[i2] = k*weight[i];d[i2][j] = max(d[i1][j],d[i1][ j-weight[i2] ]), d[i][j] = d[i2][j]含有包含[0,1,0+2,1+2] = [0,1,2,3]的情况,并且取了容量为j的时候的最大值 num[i]-=k;
当k=4时,假设存在物品i3,质量为weight[i3] = k*weight[i];d[i3][j] = max(d[i2][j],d[i2][ j-weight[i2] ]), d[i][j] =d[i2][j]含有包含[0,1,2,3,0+4,1+4,2+4,3+4]的情况,并且取了容量为j的时候的最大值 num[i]-=k;
等等 直到k >= num[i]
也就是 2^k <= num[i] < 2^(k+1) k = k*2;
最后一定要记得取k=num[i]的情况,把剩余的num[i]个也放入考虑,其实相当于加入了 [2*(k+1),,,,num[i]+2*(k+1)-1];
/*
多重背包问题
转换成0-1背包和完全背包
*/
#include<iostream>
#include<cstring>
using namespace std;
const int maxN = 6;
const int maxW = 120000;
int dp[maxW+1];
int value[maxN+1], num[maxN+1];
int weight[maxN+1] = {0,1,2,3,4,5,6};
int value[maxN+1] = {0,1,2,3,4,5,6};
int n = 6,w =0;
//weight为物品i的重量,value为物品i的价值
//0-1背包
void zeroOnePack(int weight,int value){
for(int j = w;j >= weight;j--){
dp[j] = max(dp[j],dp[j-weight]+value);
}
}
//完全背包
void completePack(int weight,int value){
for(int j = weight;j <= w;j++){
dp[j] = max(dp[j],dp[j-weight]+value);
}
}
//多重背包
void multiPack(){
memset(dp,0,sizeof(dp));
for(int i = 1; i <= n; i++){
if(num[i]*weight[i] > m){
Complete_Pack(weight[i],value[i]);
//如果全装进去超了重量,相当于这个物品是无限的
continue;
}
int k = 1;
while(k < num[i]){ //改成=应该也一样,但是while过后怎么也得一次0-1背包
//0-1bag
ZeroOnePack(k*weight[i],k*value[i]);
num[i] -= k;
k*=2;
}
ZeroOnePack(num[i]*weight[i],num[i]*value[i]);
}
}
另外附上poj上1014问题的解 url = http://poj.org/problem?id=1014;
Description
Input
The last line of the input file will be "0 0 0 0 0 0"; do not process this line.
Output
Output a blank line after each test case.
思路:
转换成0/1背包,这次的dp[j]中存放的是0/1,表示容量为j的背包可否被完全装满。
传统的0/1背包递归公式
dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]);
故若dp[j - weight[i]] == 1,给dp[j]也赋值为1
若dp[i-1][j]本来就为1,不用管。
若dp[w/2]结果为1,说明容量为w/2的背包可否被完全装满。
这个题也完全可以按照传统的背包问题求解。
#include<iostream>
#include<cstring>
using namespace std;
const int maxN = 6;
const int maxW = 120010;
int dp[maxW+1];
int n = 6,w =0;
int main(){
int index = 0,k;
int wTemp[20010];
while(true){
w = 0;
index++;
int n = 0;//真实的下标
for(int i = 1; i <= 6; i++){
cin>>k;
w += k*i;
int j = 1;
while(j <= k){
wTemp[++n] = i*j;
k -= j;
j = j<<1;
}
if(k)
wTemp[++n] = k*i;
}
if(w == 0){
break;
}
cout<<"Collection #"<<index<<":"<<endl;
if(w%2){
cout<<"Can't be divided."<<endl<<endl;
continue;
}
memset(dp,0,sizeof(dp));
dp[0] = 1;
for(int i = 1; i <= n; i++){
for(int j = w; j >= wTemp[i];j--){
if(dp[j-wTemp[i]] != 0)
dp[j] = 1;
}
}
if(dp[w/2] == 1){
cout<<"Can be divided."<<endl<<endl;
}else{
cout<<"Can't be divided."<<endl<<endl;
}
}
}