0. 前言
学习算法以来,总会遇到背包问题,包括0-1背包问题、完全背包问题等,但还没做过一次正式的总结,所以,这就来啦!
1. 背包问题概述
背包问题通常指的是给出一个有最大承重的背包,和一系列有固定价值和重量的物品,求出背包中装载最大价值物品的方案。根据物品的数量限制与分割限制,有如下四种背包问题:
- 部分背包问题:每种物品只有一件,物品可分割
- 0-1背包问题:每种物品只有一件,不可分割
- 完全背包问题:每种物品有无限件,不可分割
- 多重背包问题:每种物品有有限件(大于1),不可分割
这里先仅讨论1、2个问题。
2. 部分背包问题(贪心算法)
部分背包问题由于物品可分割,因此可以采用贪心算法求解,优先装入单位价值高的物品,装载完后依次装入单位价格次高的物品。
void knapsack(int n,float M,float y[],float w[],float x[]){//其中n指物品的件数,M指背包最大承重量,y[]存储每种物品的价值,w[]
//存储每种物品的质量,x[]存储背包最佳装载方案中,每种物品的数量
sort(n,v,w);//将数组按照单位价值由高到低排序
int i;
float c=M;
for(i=0;i<n;i++) x[i]=0;//初始化
for(i=0;i<n;i++){
if(w[i]>c)//若背包剩余容量不足以装载整个物品
break;
else{
x[i]=1;
c-=w[i];
}
}
if(i!=n){
x[i]=c/w[i];
}
}
3. 0-1背包问题
对于每件物品,0-1背包问题只有两种选择,装或者不装,这种选或不选的问题,首先可以用动态规划解决,其次,由于可选方案为有限种,也可对所有可能的方案采用回溯法进行遍历搜索。
3.1 动态规划法(C++)
使用动态规划法的关键是找到问题中的递归式,即前一个状态或后一个状态与当前状态的关系。用m(i,j)表示当第[i,n]个物品可选,背包剩余容量j。
因此,有如下递归关系式:
基于上述递归关系式,将m(i,j)保存在数组中(条件是每个物品的质量为整数),代码如下所示:
int dp(int n,int M,int w[],int v[]){
int m[n][M];
int i,j;
for(i=0;i<M;i++){//初始化可选择物品仅为n的情况
if(i>w[n-1])
m[n-1][i]=v[n-1];
else
m[n-1][i]=0;
}
for(i=n-2;i>=0;i--){//从后向前
for(j=0;j<M;j++){//遍历所有背包容量
if(j>=w[i])//若背包容量大于等于该物品重量
m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
else//否则无法装入
m[i][j]=m[i+1][j];
}
}
return max(m[0][M-1],m[0][M-w[0]]+v[0]);//第一个物品可能装入,可能不装入,需要进行判断
}
3.2 回溯法(C++)
回溯法即对解空间进行有限制条件的深度优先搜索,此问题中,在向下搜索时,需满足背包容量大于等于所选物品质量。实现回溯方法,可以采用递归回溯、迭代回溯两种。在这里采用递归回溯实现:
int backtrack(int i,int j){//w[]中存放每个物品的重量,v[]中存放每个物品的价值,n为物品数量,
int l=0,r=0;
if(i<n){
if(j>=w[i]){
r=backtrack(i+1,j-w[i])+v[i];
}
l=backtrack(i+1,j);
return max(l,r);
}
else if(i==n){
return 0;
}
}