-
写在前面
背包问题是动态规划里面很重要的一部分,彻底理解各种背包问题,对动态规划的后续学习有很大的帮助.
更全的背包问题,可参看《背包九讲》.
一.什么是“0-1背包”?
有这样一个问题:
在你面前放着n颗宝石,每颗宝石重量为wi,价值为vi;你有一个最多可以放m重量的背包。现在你想在不超重的情况下,是你带走的宝石价值最大,问最大价值是多少?
由于每种物品只有一件,对于第i件物品只有两种可能:拿或不拿。故称为“0-1”背包.
如果每种物品有多个,那就是“多重背包”.
如果每种物品的数量是无限的,那就是“完全背包”.
二.如何做?
很显然,贪心算法无法保证最优解,只能用动态规划来做.
我们从第一件物品开始,逐一往后决策.
设:
dp[i][j]表示:前i件物品,当背包限制容量为j时,能够取到的最大价值总和.
例如:有5件物品,背包容量为10.其中每件物品的重量和价值如下:
物品 | 重量 | 价值 |
1 | 2 | 6 |
2 | 2 | 3 |
3 | 6 | 5 |
4 | 5 | 4 |
5 | 4 | 6 |
初始状态dp数组:
物品 | wi | vi | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | - | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 2 | 6 | |||||||||||
2 | 2 | 3 | |||||||||||
3 | 6 | 5 | |||||||||||
4 | 5 | 4 | |||||||||||
5 | 4 | 6 |
在填dp[1][0]时,显然容量小于2以前都为0.
物品 | wi | vi | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | - | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 2 | 6 | 0 | 0 | |||||||||
2 | 2 | 3 | |||||||||||
3 | 6 | 5 | |||||||||||
4 | 5 | 4 | |||||||||||
5 | 4 | 6 |
到了dp[1][2],我们发现容量2可以装下第一件物品,所以dp[1][2]=6.
而且后面的都是为6,因为现在只决策到第1件物品,不考虑后面的物品.
物品 | wi | vi | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | - | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 2 | 6 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 2 | 3 | |||||||||||
3 | 6 | 5 | |||||||||||
4 | 5 | 4 | |||||||||||
5 | 4 | 6 |
同理,一直填到dp[2][2],这个时候有两个选择,要么拿2,要么不拿2。拿2的最大价值为3 ,不拿2的最大价值为6,所以我们选择不拿2.
物品 | wi | vi | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | - | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 2 | 6 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 2 | 3 | 0 | 0 | 6 | 6 | |||||||
3 | 6 | 5 | |||||||||||
4 | 5 | 4 | |||||||||||
5 | 4 | 6 |
关键!关键到了dp[2][4].
两种情况:
1.拿2
背包装了2号物品后,剩余容量为4-2=2。现在问题转化为:前i-1件物品,背包最大容量为2时,能够获得的最大价值,对应的也就是dp[i-1][j-w[i]]。
2.不拿2
dp[2][4]=dp[i-1][j]=dp[1][4]=6.
通过计算,我们发现拿2可以获得更大的价值,也就是dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]).
剩下的以此类推,最大的价值就是dp[n][m].
物品 | wi | vi | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 | - | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 2 | 6 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 2 | 3 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 9 | 9 | 9 |
3 | 6 | 5 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 11 | 11 | 14 |
4 | 5 | 4 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 10 | 11 | 13 | 14 |
5 | 4 | 6 | 0 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
还有一个问题:如何知道最终选择的是哪些物品?
很简单,我们从dp[n][m]开始往上走:
1.如果dp[i][j]>dp[i-1][j],说明物品i是选择了的物品,记录之。此时问题转化成了i-1件物品,背包最大容量为j-w[i]时的情况,即:跳转到dp[i-1][j-w[i]]继续判断;
2.如果dp[i][j]==dp[i-1][j],说明物品j未选择。继续考虑dp[i-1][j].
重复上述判断,直到走到i=0为止,即可得到所选择的物品。
所以选择的物品为:1,2,5.
三.空间优化
前面我们都是用一个大小为N*W的dp数组来存储状态,很容易爆内存,怎样优化内存呢?
逆序!
for(i=1 to N) for(j=W to 0) dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
看懂了没?原来的方法:在计算dp[i][j]时,只用到了上一行中第j列以及第j列之前的dp值.
将内循环逆序,保证了正确性,空间瞬间从N*W变为了W,再也不用担心爆内存了.
当然,如果需要知道选择了哪些物品的话,还是需要第一种方法.
代码1:
/* * this code is made by crazyacking * Verdict: Accepted * Submission Date: 2013-10-28-17.43 * Time: 0MS * Memory: 137KB */ #include <queue> #include <cstdio> #include <set> #include <string> #include <stack> #include <cmath> #include <climits> #include <map> #include <cstdlib> #include <iostream> #include <vector> #include <algorithm> #include <cstring> #define max(a,b) (a>b?a:b) using namespace std; typedef long long(LL); typedef unsigned long long(ULL); const double eps(1e-8); const int N=110; const int M=10010; int w[N],v[N],dp[M]; int n,m; int main() { ios_base::sync_with_stdio(false); cin.tie(0); while(cin>>n>>m) { for(int i=1;i<=n;++i) cin>>w[i]>>v[i]; for(int i=0;i<=m;++i) dp[i]=0; for(int i=1;i<=n;++i) { for(int j=m;j>=0;--j) { if(j-w[i]<0) continue; dp[j]=max(dp[j],dp[j-w[i]]+v[i]); } } printf("%d ",dp[m]); } return 0; } /* */
代码2:
/* * this code is made by crazyacking * Verdict: Accepted * Submission Date: 2013-10-26-12.36 * Time: 0MS * Memory: 137KB */ #include <queue> #include <cstdio> #include <set> #include <string> #include <stack> #include <cmath> #include <climits> #include <map> #include <cstdlib> #include <iostream> #include <vector> #include <algorithm> #include <cstring> #define max(a,b) (a>b?a:b) using namespace std; typedef long long(LL); typedef unsigned long long(ULL); const double eps(1e-8); const int N=110; const int W=10010; int n,m; int v[N],w[N]; int dp[N][W]; void print_dp() { puts("-----------------------------------------------------------------"); for(int i=0; i<=n; ++i) { for(int j=0; j<=m; ++j) { printf("%3d ",dp[i][j]); } puts(""); } puts("-----------------------------------------------------------------"); } void print_select() { vector<int> sel; int i=n,j=m; for(; i>=1; --i) { if(dp[i][j]>dp[i-1][j]) // selected { sel.push_back(i); j-=w[i]; } } reverse(sel.begin(),sel.end()); puts("-----------------------------------------------------------------"); for(int i=0; i<sel.size(); ++i) { printf("%d ",sel[i]); } puts(""); puts("-----------------------------------------------------------------"); } int main() { ios_base::sync_with_stdio(false); cin.tie(0); while(~scanf("%d %d",&n,&m)) { for(int i=1; i<=n; ++i) { scanf("%d %d",&w[i],&v[i]); } for(int i=0; i<=m; ++i) dp[0][i]=0; for(int i=1; i<=n; ++i) { for(int j=0; j<=m; ++j) { if(j-w[i]>=0) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); else dp[i][j]=max(dp[i-1][j],0); } } print_dp(); print_select(); printf("%d ",dp[n][m]); } return 0; } /* */