理解01背包
最近刚刚学习背包问题,挤时间先来总结一下自己理解的01背包问题。
背包问题属于组合优化问题,我翻阅过好多运筹学书籍发现好多书上都有它的身影,解决它的方法也不少:动态规划法、回溯法、分支界定法、贪心法。作为初学者水平有限,只能浅谈一些关于动态规划算法的认识。
首先看一下01背包问题的定义(定义形式多种多样,但都大同小异):
有一背包最大容量为W。有 N 种物品,其价值和体积分为Vi,Wi,每种物品只有一件。从所有物品中选择若干件放入背包,最大能得到的价值是多少?这就 是 01 背包问题,0 和 1 指的某件物品选还是不选 。
首先从理解上采用dfs比较合适,现在先略,主要先谈动态规划想法。
定义f[i][j]表示把前i个物品装入容量为j的背包时获得的最大价值,则可得如下状态转移方程:
f[i][j]=max{f[i-1][j],f[i-1][j-w[i]]+v[i]}
边界为:i=0时为0,j<0时为负无穷,最终答案为f[n][W].
对此方程我的理解是,既然用动态规划来解,那么这个问题的状态必然只跟它前一个或者或一个相邻的状态有关,此处按f[i][j]的定义,可以发现把前i个物品放入容量为j的背包这个问题只和它的前一个状态:把前i-1个物品放入背包。如果不放第i件物品,那么问题就转化成把前i-1个物品放入容量为j的背包,如果放第i件物品,那么就需要把前i-1件物品放入容量为W-wi的背包。这里是用物品i来表示阶段i,以前理解的不深刻,只知道f[i][j]由前面f[i-1][j]和f[i-1][j-w[i]]+v[i]得来,现在想想,这种正向推的方法是因为在计算时f[i-1][j]和f[i-1][j-w[i]]已经先计算过了,所以才可以推到f[i][j]。
举个例子加深理解:(分享个在线模拟01背包的网址:http://karaffeltut.com/NEWKaraffeltutCom/Knapsack/knapsack.html :D)
比如N=,W=6 四个物品为(vi,wi) (2,3) (6,2) (12,3) (3,4):
代码如下:
//代码1.
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=105;
int v[maxn],w[maxn];
int f[maxn][maxn];
int N,W;
int main()
{
while(scanf("%d%d",&N,&W)==2)
{
memset(f,0,sizeof(f));
for (int i=1;i<=N;i++) scanf("%d",&v[i]);
for (int i=1;i<=N;i++) scanf("%d",&w[i]);
for (int i=1;i<=N;i++)
for (int j=0;j<=W;j++){
f[i][j]=(i==1?0:f[i-1][j]);
if (j>=w[i]) f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
}
printf("%d\n",f[N][W]);
}
return 0;
}
01背包一般有两种问法,一是恰好使背包装满,一是没有要求必须装满背包。两种问法的矛盾就体现在是否需要“恰好”装满背包。这个矛盾的解决方法就在初始化处理上:
恰好装满时除第零列(背包容量为零)值为零其他初始化为无穷小,而未要求装满则全值为零就行(如上程序)。
其实我以前没有想到解决这个矛盾竟然只在初始化上!感觉好神奇。按照《背包九讲》的说法:
初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
我只能写个程序看看输出来理解(数据按上面的):
恰好装满时:
参考代码:
#include<iostream> #include<algorithm> #include<cstdio> using namespace std; const int minmum=-1000; int f[5][7]={{0}}; int w[5]={0,3,2,3,4}; int v[5]={0,2,6,12,3}; int N=4,V=6; int main() { for (int i=0;i<=N;i++){ for (int j=0;j<=V;j++) f[i][j]=minmum; } for (int i=0;i<=N;i++) f[i][0]=0; cout<<endl; printf(" 0 "); for (int i = 1; i <= V; i++) printf("%-9d", i); cout << endl; for (int i=0;i<=N;i++){ for (int j=0;j<=V;j++){ if (j==0) printf("%d:",i); printf("%-9d",f[i][j]); } cout<<endl; } cout<<endl; cout<<endl; for (int i=1;i<=N;i++){ for (int j=1;j<=V;j++){ f[i][j]=f[i-1][j]; if (j>=w[i]) f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]); } } cout<<"ans:= "<<f[N][V]<<endl; printf(" 0 "); for (int i = 1; i <= V; i++) printf("%-9d", i); cout << endl; for (int i=0;i<=N;i++){ for (int j=0;j<=V;j++){ if (j==0) printf("%d:",i); printf("%-9d",f[i][j]); } cout<<endl; } return 0; }
不要求恰好装满时:
参考代码:
#include<iostream> #include<algorithm> #include<cstdio>
#include<cstring> using namespace std; const int minmum = -1000; int f[5][7] = { { 0 } }; int w[5] = { 0,3,2,3,4 }; int v[5] = { 0,2,6,12,3 }; int N = 4, V = 6; int main() { memset(f, 0, sizeof(f)); cout << endl; printf(" 0 "); for (int i = 1; i <= V; i++) printf("%-9d", i); cout << endl; for (int i = 0; i <= N; i++) { for (int j = 0; j <= V; j++) { if (j == 0) printf("%d:", i); printf("%-9d", f[i][j]); } cout << endl; } cout << endl; cout << endl; for (int i = 0; i <= N; i++) f[i][0] = 0; for (int i = 1; i <= N; i++) { for (int j = 1; j <= V; j++) { f[i][j] = f[i - 1][j]; if (j >= w[i]) f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]); } } cout << "ans:= " << f[N][V] << endl; printf(" 0 "); for (int i = 1; i <= V; i++) printf("%-9d", i); cout << endl; for (int i = 0; i <= N; i++) { for (int j = 0; j <= V; j++) { if (j == 0) printf("%d:", i); printf("%-9d", f[i][j]); } cout << endl; } return 0; }
由于背包的每个状态只跟他相邻的状态有关,所以可以想到能否用一维数组代替二维数组进行进一步优化。
答案是肯定的,用“滚动数组”,所谓“滚动数组”就是为了优化空间的,由于每个状态只跟它相邻的上一个状态有关,也就是说当前状态是由上一个状态推过来的,如果我只想要最后的结果,是没有必要保留每个阶段的状态的,自然我可以用一个一维数组每次状态更新时直接覆盖上一个状态。并且在二维时我们是先计算f[i-1][j-w[i]]+v[i],再计算f[i-1][j]的,所以若还是直接从后往前推的话会先覆盖f[i-1][j-w[i]]+v[i]的位置,那么计算f[i-1][j]时用到的就是刚才更新的结果而不是原来的结果,这样会造成一个背包被多次利用,所以要采用从前往后推的方法来计算。
参考代码:
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> using namespace std; const int minmum=-1000; int f[10]; int w[5]={0,3,2,3,4}; int v[5]={0,2,6,12,3}; int N=4,V=6; int main() { memset(f,0,sizeof(f)); for (int i=1;i<=N;i++){ for (int j=V;j>=w[i];j--) f[j]=max(f[j],f[j-w[i]]+v[i]); } cout<<f[V]<<endl; return 0; }
参考博客:http://blog.csdn.net/insistgogo/article/details/8579597