题目描述
给定n种物品和一个背包,物品i的重量是Wi,其价值为Vi,背包的容量为C。如何选择装入背包的物品,可以使得装入背包中物品的总价值最大?
0-1背包,每件物品的状态只有两个:放(1)和不放(0),不能只放入一部分(放入一部分的是部分背包问题,采取贪心策略优先选择价值重量比高的物品就好)。
0-1背包有很多种解决方法,这里只整理两种:动态规划(填表,dp)和回溯(深度搜索,dfs)
动态规划
想要使用动态规划求解0-1背包问题,对题目的条件要求有些苛刻:因为是利用背包的容量C作为要填写表格的列,所以要求物品的重量W和价值V,以及背包的容量C均必须是整数。
给出示例:
物品数量 n=5,背包总容量 C=10
物品的重量分别为:w={ 2,2,6,5,4 }
物品的价值分别是:v={ 6,3,5,4,6 }
我们定义一个二维数组 m[i] [j],意义为:背包剩余容量为 j 时,可选择物品 i,i+1,…,n 时,能获取的最大价值(最优解)
当只剩下最后第n个物品时:
其他情况:
当剩余容量 j 比当前物品 i 的重量大时:
在放入第i个物品的背包价值 m[i+1] [j-w[i]]+v[i] 和 不放入第i个物品的背包价值 m[i+1] [j] 中挑选最大值
当剩余容量 j 比当前物品 i 的重量小时:
最有价值即m[i+1] [j]
填表
以[4,9]为例,m[4][9]=10,此时剩余背包容量为9,当前物品4的重量w[4]=5,价值v[4]=4,如果不装当前的物品4,则背包中物品价值和=m[5] [9]=6,如果将当前的物品装入背包,则价值和=v[4]+m[5] [9-w[4]]=4+6=10;
完整题目
输入
每组输入包括三行,
第一行包括物品个数n,以及背包容量C。
第二、三行包括两个一维数组,分别为每一种物品的价值和重量。
输出
输出背包的最大总价值
样例输入
5 10
6 3 5 4 6
2 2 6 5 4
样例输出
15
11001
代码
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int inf=0x7fffffff;
const int maxn=1010;
int n,c;
int v[maxn],w[maxn];
int m[maxn][maxn];
int main(){
while(cin>>n>>c){
memset(m,0,sizeof(m));
for(int i=0;i<n;i++){
cin>>v[i];
}
for(int i=0;i<n;i++){
cin>>w[i];
}
//先填表格的最后一行
for(int i=0;i<=c;i++){
if(i<w[n-1]) m[n-1][i]=0;
else m[n-1][i]=v[n-1];
}
//再继续往上依次填写表格
for(int i=n-2;i>=0;i--){
for(int j=0;j<=c;j++){
if(j<w[i]) m[i][j]=m[i+1][j];
else{
m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
}
}
}
//表格右上角为最优解
cout<<m[0][c]<<endl;
}
return 0;
}
构造最优解:
上面的代码仅仅是求出了背包所能装的最大价值,我们现在引入数组 x[],来标记装入背包的是哪些物品:
int x[maxn];
for(int i=0;i<n-1;i++){
//此处c用来记录剩余背包容量
if(m[i][c]==m[i+1][c]){
x[i]=0;
}
else{
x[i]=1;
c-=w[i];
}
}
//最后一行单拎出来判断
x[n-1]=(m[n-1][c]>0?1:0);
//输出
for(int i=0;i<n-1;i++){
cout<<x[i]<<" ";
}
cout<<x[n-1]<<endl;
回溯法
因为动态规划解决的0-1背包问题必须为整数,而实际应用中,这种情况少之又少,所以引入回溯法(dfs)。
数据结构
物品i的重量是wi,价值 为vi,背包的容量为C, 物品重量wi和背包容量C可为正整数或小数。
int n;//物品数目
int v[maxn],w[maxn],C;
int cv;//当前价值
int bestv=0;//最优价值
int x[maxn];//当前解
int bestx[maxn];//当前最优解
int cw=0;//当前重量
基本思想
- 按照贪心算法的思路,优先装入价值/重量比大的物品
- 当剩余容量装不下后续的物品时,再用回溯法修改先前的装入方案
- 知道求得全局最优解为止
搜索函数
void dfs(int t){
if(t>=n){
if(bestv<cv){
bestv=cv;
for(int j=0;j<n;j++){
best[j]=x[j];
}
}
//输出
}
else{
//搜索左枝
if(ccw+w[t]<=C){
x[t]=1; cv+=v[t]; cw+=w[t];
dfs(t+1);//继续向下深度搜索
x[t]=0; cv-=v[t]; cw+=w[t];//回退
}
//搜索右枝
x[t]=0;
dfs(t+1);
}
}
改进思路
-
前述算法完全搜索解空间树时:
- 用约束条件确定是否搜索其左子树
- 直接进入右子树(没有判断)–>耗时
-
改进:
- 只要左结点是一个可行结点,搜索就进入左子树——约束函数
- 当右子树可能包含最优解时才进入右子树搜索,否则剪去右子树——限界函数
剪枝方法:
-
r:当前剩余物品价值和总和
-
cv:当前获得价值
-
bestv:当前最优价值
-
当cv+r<=bestv时,可剪去右子树–>当前价值与剩余物品价值之和小于等于已求最优价值,无法得到更优解
-
计算右子树上界:贪心
将剩余物品依其单位重量价值排序(降序),然后依次装入物品,直至装不下 时,再装入物品的一部分而装满背包,此时得到的价值是右子树中解的上界
-
改进代码
int bound(int i){//计算当前结点处的上界
int left=C-cw;//剩余容量
int b=cv;//当前价值
while(w[i]<=left&&i<=n){
b+=v[i];
left-=w[i];
i++;
}
//装满背包
if(i<=n) b+=left*v[i]*w[i];
return b;
}
void dfs(int t){//回溯
if(t>n){
//代码省略,同上
}
else{
//搜索左枝
if(ccw+w[t]<=C){
x[t]=1; cv+=v[t]; cw+=w[t];
dfs(t+1);//继续向下深度搜索
x[t]=0; cv-=v[t]; cw+=w[t];//回退
}
//搜索右枝
if(bound(t+1)>bestv){
x[t]=0;
backtrack(t+1);
}
}
}