问题描述
0-1背包问题:给定n种物品和一-背包。物品i的重量是wi, 其价值为Vi, 背包的容量为C。问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大?在选择装入背包的物品时,对每种物品i只有两种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。因此,该问题称为0-1背包问题。
最优子结构分析
确定递推方程
对于这个整数规划问题,我们用m(i,j)来表述可选择物品为i,i+1,i+2…n在背包容量为j,时的最优值。那么我们可以得出如下状态转移方程:
- m(i,j)=max{m(i+1,j),m(i+1,j-wi)+vi},j>=wi
- m(i,j)=m(i+1,j),j<wi
- 最优解即为
m[1][c]
以下是一个实例:
算法实现
实现1:
对于这个递推方程我们最简单的实现是定义一个n+1*n+1的数组,将其初始化为0,然后根据递推式直接填充,易于实现,预处理十分简单:
int dpFunc1(int m[][maxn],int n,int c)
{
for(int i=0;i<n+1;i++) m[i][0]=0;//初始化第0列
for(int j=0;j<c+1;j++) m[0][j]=0;//初始化第0行
for(int i=n;i>=1;i--)//序号
{
for(int j=1;j<=c;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 m[1][c];//返回最优解
}
实现2:knapsack算法
这个算法相较于实现1,有效避免了不必要及比较过程,当j<w[i]的时候直接填充m[i+1][j]而不需要比较,另外由于最后一行的前c-1个数据并没有使用,所以无需计算。
int knapsack(int m[][maxn],int n,int c)
{
int jMax=min(w[n],c);
//预处理第一行,j小于w[n]填充物为0,否则填充为v[n]
for(int j=0;j<jMax;j++) m[n][j]=0;
for(int j=jMax;j<c;j++) m[n][j]=v[n];
for(int i=n-1;i>1;i--)//最后一行不计算,因为有一部分填充是无效的
{
jMax=min(w[i],c);
for(int j=0;j<jMax;j++)
m[i][j]=m[i+1][j];//不选
for(int j=jMax;j<c+1;j++)
m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
}
m[1][c]=max(m[2][c],m[2][c-w[1]]+v[1]);//处理最后一行的最后一个数据
return m[1][c];
}
求解最优解的选取方式traceBack回溯
形成以上的一组表格之后我们可以很容易得到最优解m[1][c]
,但是如何求解选取方式呢?我们仍然可以根据递推式回溯:
- 如果
m[i][c]==m[i+1][c]
,说明i号物品并没有被选取,i++ - 如果
m[i][c]!=m[i+1][c]
,说明i号物品被选取,i++,c-wi - 当i=n时,如果
m[n][c]!=
0说明n号物品被选取,否则未被选取
void traceBack(int m[maxn][maxn],int w[],int c,int n,int x[])
{
for(int i=1;i<n;i++)
{
if(m[i][c]==m[i+1][c]) x[i]=0;
else{
x[i]=1;
c-=w[i];
}
}
x[n]=(m[n][c])?1:0;
}
总算法实现
#include <iostream>
using namespace std;
const int maxn=1005;
int m[maxn][maxn]={0};
int w[maxn]={0};
int v[maxn]={0};
int dpFunc1(int m[][maxn],int n,int c)
{
for(int i=0;i<n+1;i++) m[i][0]=0;//初始化第0列
for(int j=0;j<c+1;j++) m[0][j]=0;//初始化第0行
for(int i=n;i>=1;i--)//序号
{
for(int j=1;j<=c;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 m[1][c];//返回最优解
}
int knapsack(int m[][maxn],int n,int c)
{
int jMax=min(w[n],c);
//预处理第一行,j小于w[n]填充物为0,否则填充为v[n]
for(int j=0;j<jMax;j++) m[n][j]=0;
for(int j=jMax;j<c;j++) m[n][j]=v[n];
for(int i=n-1;i>1;i--)//最后一行不计算,因为有一部分填充是无效的
{
jMax=min(w[i],c);
for(int j=0;j<jMax;j++)
m[i][j]=m[i+1][j];//不选
for(int j=jMax;j<c+1;j++)
m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
}
m[1][c]=max(m[2][c],m[2][c-w[1]]+v[1]);//处理最后一行的最后一个数据
return m[1][c];
}
void traceBack(int m[maxn][maxn],int w[],int c,int n,int x[])
{
for(int i=1;i<n;i++)
{
if(m[i][c]==m[i+1][c]) x[i]=0;
else{
x[i]=1;
c-=w[i];
}
}
x[n]=(m[n][c])?1:0;
}
int main()
{
int n,c,x[maxn]={0};
cin>>n>>c;
for(int i=1;i<=n;i++)
{
cin>>w[i]>>v[i];
}
int res=dpFunc1(m,n,c);//函数调用
cout<<res<<endl;
res=knapsack(m,n,c);
cout<<res<<endl;
traceBack(m,w,c,n,x);
for(int i=1;i<n+1;i++) cout<<i<<' '<<x[i]<<endl;
system("pause");
return 0;
}
/*
5 7
2 4
3 4
1 2
2 7
4 8
*/
算法复杂度分析
时间复杂度:T(n)=O(n^2)
空间复杂度:S(n)=O(n^2)