背包问题是动态规划的经典问题,所有初次学习动态规划都会讲述这个经典问题。因此,有必要弄清跟背包问题的所有分析过程并熟练掌握各种类型的代码。
一,完全背包问题
1.问题描述:有n种物品,每种物品有无限多个,第i个物品重量是wi,价值是vi,从这些物品中挑选总重量不超过W的物品,求出挑选物品价值的最大值。
限制条件:1≤n≤100,1≤wi,vi≤100,1≤W≤10000
2.解题思路:本题类似于“硬币问题”,硬币问题只要求凑够相应的面值,这里只不过多了一个新的属性——价值,即不仅要凑够重量,还要使得价值尽可能大。按照动态规划的分析思路,先定义状态d(j):剩余重量为j时的最大价值。那么如果当前使用了第i个物品(如果可以用的话),状态变转移到j-w[i],而价值变为d[j-w[i]]+v[i],因此状态转移方程如下:
d[j]=max(d[j],d[j-w[i]]+v[i]);(0≤i<n)
这个过程中随着物品的增多,剩余的重量在逐渐减小。
3.代码:
(1)不包含打印路径
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<functional>
using namespace std;
#define maxn 1000
#define rep(i,n) for(int i=0;i<(n);i++)
int d[maxn];
int w[maxn],v[maxn];
int n, W;
int dp(int j)
{
int&ans = d[j];
if (ans>0)return ans;
for (int i = 0; i < n;i++)
if (j >= w[i])
ans = max(ans, dp(j-w[i]) + v[i]);
return ans;
}
int main()
{
//freopen("test.txt", "r", stdin);
scanf("%d%d", &n, &W);
rep(i, n)
scanf("%d", w + i);
rep(i, n)
scanf("%d", v + i);
dp(W);//从状态W开始,走到状态0
cout << d[W] << endl;
return 0;
}
(2)包含打印路径
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<functional>
using namespace std;
#define maxn 1000
#define rep(i,n) for(int i=0;i<(n);i++)
int d[maxn];
int w[maxn],v[maxn];
int n, W;
int dp(int j)
{
int&ans = d[j];
if (ans>0)return ans;
for (int i = 0; i < n;i++)
if (j >= w[i])
ans = max(ans, dp(j-w[i]) + v[i]);
return ans;
}
void print_ans(int j)
{
for (int i = 0; i < n;i++)
if (j >= w[i] && d[j] == d[j - w[i]] + v[i])
{
printf("%d ", i);
print_ans(j - w[i]);
break;
}
}
int main()
{
//freopen("test.txt", "r", stdin);
scanf("%d%d", &n, &W);
rep(i, n)
scanf("%d", w + i);
rep(i, n)
scanf("%d", v + i);
dp(W);//从状态W开始,走到状态0
cout << d[W] << endl;
print_ans(W);//打印所用物品的序号
puts("");
return 0;
}
二,0-1背包问题
1.问题描述:有n种物品,每种只有一个,,第i个物品重量是wi,价值是vi,从这些物品中挑选总重量不超过W的物品,求出挑选物品价值的最大值。
限制条件:1≤n≤100,1≤wi,vi≤100,1≤W≤10000
2.解题思路:现在的问题又增加了一个限制条件——每种物品的个数,可以发现,原来的状态转移方程已经不适用了,因为只凭借“剩余重量的大小”无法得知每个物品是否用过,因此需要修改原来定义的状态,使得以后的决策可以有序化。
想象所有的物品按照序号摆成一排,现在,定义d(i,j)表示当前正在第i个物品处,背包的剩余容量为j时的最大价值。显然,这时的决策有两个:(1)不使用物品i;(2)使用物品i;因此,状态转移方程如下:
d(i,j)=max(d(i+1,j),d(i+1,j-w[i])+v[i]);(j≥w[i])
边界:d(n-1,j)=0;
上式中,i要逆序枚举,因为公式中i时候的状态是由i+1时候的得到的。上式中的j随着i的减小而增大,这也不难由公司看出。最后的答案是d[0][W],这样代码便不难写出。
3.代码:
(1)不包含打印解
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<functional>
using namespace std;
#define maxn 1000
#define rep(i,n) for(int i=0;i<(n);i++)
int d[maxn][maxn];
int w[maxn],v[maxn];
int n, W;
int main()
{
//freopen("test.txt", "r", stdin);
memset(d, -1, sizeof(d));
scanf("%d%d", &n, &W);
rep(i, n)
scanf("%d", w + i);
rep(i, n)
scanf("%d", v + i);
for (int i = n - 1; i >= 0;i--)//i逆序枚举,由递推公式决定
for (int j = 0; j <= W; j++)//j的顺序无所谓
{
d[i][j] = (i == n - 1 ? 0 : d[i + 1][j]);
if (j >= w[i])
d[i][j] = max(d[i][j], d[i + 1][j - w[i]] + v[i]);
}
printf("%d\n", d[0][W]);
return 0;
}
(2)包含打印解
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<functional>
using namespace std;
#define maxn 1000
#define rep(i,n) for(int i=0;i<(n);i++)
int d[maxn][maxn];
int w[maxn],v[maxn];
int vis[maxn];
int n, W;
int main()
{
//freopen("test.txt", "r", stdin);
memset(d, -1, sizeof(d));
scanf("%d%d", &n, &W);
rep(i, n)
scanf("%d", w + i);
rep(i, n)
scanf("%d", v + i);
for (int i = n - 1; i >= 0; i--)//i逆序枚举,由递推公式决定
for (int j = 0; j <= W; j++)//j的顺序无所谓
{
d[i][j] = (i == n - 1 ? 0 : d[i + 1][j]);
if (j >= w[i])
d[i][j] = max(d[i][j], d[i + 1][j - w[i]] + v[i]);
}
printf("%d\n", d[0][W]);
printf("Solutions :");//打印已经选择的物品
int R = W;
for (int i = 0; i < n;i++)
for (int j = R; j >= 0;j--)
if (d[i][j] == d[i + 1][j])break;
else if (j >= w[i] && d[i][j] == d[i + 1][j - w[i]] + v[i])
{
printf(" %d", i);
R -= w[i];
break;
}
printf("\n");
return 0;
}
三,0-1背包问题的规划方向
刚刚的状态d(i,j)的定义是: 当前正在第i个物品处,背包的剩余容量为j时的最大价值。这样的定义导致了枚举i时必须逆序。其实还有一种“对称”的状态定义:当前正在第i个物品处,背包的总容量为j时的最大价值。按照这样的定义,不难得到如下的状态转移方程:
d(i,j)=max(d(i-1,j),d(i-1,j-w[i])+v[i])(j≥w[i]);
边界:d(0,j)=0;
按照上式,如果选择了物品i,那么在i-1处背包的容量必须是j-w[i],这样才能装下i物品。由上式可知,i必须顺序枚举,因为i处的状态是由i-1处的状态得到的。而且j随着i的增大而增大,这和之前说的剩余容量在逐渐减小是一致的。最终的答案是d(n-1,W)代码与上面的类似。
3.代码:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<functional>
using namespace std;
#define maxn 1000
#define rep(i,n) for(int i=0;i<(n);i++)
int d[maxn][maxn];
int w[maxn],v[maxn];
int vis[maxn];
int n, W;
int main()
{
//freopen("test.txt", "r", stdin);
memset(d, -1, sizeof(d));
scanf("%d%d", &n, &W);
rep(i, n)
scanf("%d", w + i);
rep(i, n)
scanf("%d", v + i);
for (int i = 0; i < n; i++)//i顺序枚举,由递推公式决定
for (int j = 0; j <= W; j++)//j的顺序无所谓
{
d[i][j] = (i == 0 ? 0 : d[i - 1][j]);
if (j >= w[i])
d[i][j] = max(d[i][j], d[i - 1][j - w[i]] + v[i]);
}
printf("%d\n", d[n - 1][W]);
return 0;
}
其实,这种对称的定义方法还有一个额外的好处,就是允许边读入边计算,而不必把w,v保存下来。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<functional>
using namespace std;
#define maxn 1000
#define rep(i,n) for(int i=0;i<(n);i++)
int d[maxn][maxn];
int w, v;
int vis[maxn];
int n, W;
int main()
{
//freopen("test.txt", "r", stdin);
memset(d, -1, sizeof(d));
scanf("%d%d", &n, &W);
for (int i = 0; i < n; i++)//i顺序枚举,由递推公式决定
{
scanf("%d%d", &w, &v);//读入第i个物品的重量,价值
for (int j = 0; j <= W; j++)
{
d[i][j] = (i == 0 ? 0 : d[i - 1][j]);
if (j >= w)d[i][j] = max(d[i][j], d[i - 1][j - w] + v);
}
}
printf("%d\n", d[n - 1][W]);
return 0;
}
不仅如此,甚至可以把d数组降维,将其变成一维数组。利用原来的结果产生新的结果,节约内存空间。这是因为在更新d(i,j)之前,它保存的就是上一次刚刚算过的d(i-1,j)的值。同理,如果j要逆序枚举,d(i,j-w[i])保存的也是上次刚刚算过的d(i-1,j-w[i])的值。因此,忽略i,即可实现降维。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<string>
#include<sstream>
#include<set>
#include<vector>
#include<stack>
#include<map>
#include<queue>
#include<deque>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<ctime>
#include<functional>
using namespace std;
#define maxn 1000
#define rep(i,n) for(int i=0;i<(n);i++)
int d[maxn];
int w, v;
int vis[maxn];
int n, W;
int main()
{
freopen("test.txt", "r", stdin);
memset(d, 0, sizeof(d));//将d数组初始化为0
scanf("%d%d", &n, &W);
for (int i = 0; i < n; i++)//i顺序枚举,由递推公式决定
{
scanf("%d%d", &w, &v);//读入第i个物品的重量,价值
for (int j = W; j >= 0;j--)
if (j >= w)
d[j] = max(d[j], d[j - w] + v);
}
printf("%d\n", d[W]);
return 0;
}
上述代码也被称为“滚动数组”,正是因为它相当于在不断地刷新之前的结果。但这种方法容易产生bug,而且很难打印解。当动态规划结束后,只有最后一个阶段的值,而没有前面的值。因此这种方法应该谨慎使用。