背包动态规划
一、意义
1. 动态规划
动态规划(dynamic programming),将一个目标大问题“大事化小,小事化了”,分成很多的子问题,得出子问题的解后得到目标大问题的解。动态规划相当于地狱难度的递推。
2. 背包问题
背包问题是一类经典的组合优化问题,又被称为0-1背包问题。它的问题描述是:给定一组物品,每个物品有自己的价值和重量,背包有容量的限制。目标是选择一些物品放入背包中,使得背包中物品的总价值最大,同时不能超过背包的容量限制。
二、0-1 背包模型
1. 审题
题目描述
有 n n n 个物品和一个容量为 m m m 的背包,每个物品有体积 w i w_i wi 和价值 v i v_i vi 两种属性,要求选若干个物品放入背包使背包中物品的总价值最大且背包中物品的总体积不超过背包的容量。
输入描述
第 1 1 1 行 2 2 2 个正整数, n , m n, m n,m 代表物品个数与背包容量。
接下来 n n n 行每行 2 2 2 个正整数, w i w_i wi 表示第 i i i 个物品的体积, v i v_i vi 表示第 i i i 个物品的价值。
输出描述
输出 1 1 1 个整数,即能装下的最大价值。
样例1
输入
4 6 1 4 2 6 3 12 2 7
输出
23
提示
1 ≤ n ≤ 100 1≤n≤100 1≤n≤100, 1 ≤ m ≤ 1000 1≤m≤1000 1≤m≤1000, 1 ≤ w i , v i ≤ 100 1≤w_i,v_i≤100 1≤wi,vi≤100。
2. 思路
我们可以用动态规划来实现。假如有一个 dp[][]
数组,我们用 dp[i][j]
来表示容量为
j
j
j 且物品个数为
i
i
i 的情况下能装下最大的价值。
我们可以得到如下的表格(列表示背包容量,行表示物品个数):
假如有这几个物品(前面的数是价值,后面的数是体积):
(1,2)(8,3)(14,5)(21,7)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 0 | 0 | 1 | 8 | 8 | 9 | 9 | 9 | 9 |
3 | 0 | 0 | 1 | 8 | 8 | 14 | 14 | 15 | 22 |
4 | 0 | 0 | 1 | 8 | 8 | 14 | 14 | 21 | 22 |
因此最终的结果绝对是 dp[n][m]
。
那么递推式如何推导呢?有两种情况(w[]
价值,v[]
体积):
j < v[i]
:dp[i][j] = dp[i-1][j]
j >= v[i]
:- 不拿物品
dp[i][j] = dp[i-1][j]
- 拿物品
w[i] + dp[i-1][j-v[i]]
- 上述两种情况取最大值
- 不拿物品
3. 参考答案
#include <iostream>
#include <algorithm>
using namespace std;
int n, m;
int v[105]; // 体积
int e[105]; // 能量值
int dp[105][1005];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> e[i];
for (int i = 1; i <= n; i++) // 物品个数
for (int j = 1; j <= m; j++) // 背包容量
{
if (j < v[i])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = max(dp[i-1][j], e[i]+dp[i-1][j-v[i]]);
}
cout << dp[n][m];
return 0;
}
三、完全背包模型
1. 审题
题目描述
有 n n n 种物品和一个容量为 m m m 的背包,每个物品有体积 w i w_i wi 和价值 v i v_i vi 两种属性,每种物品的数量没有限制,要求选若干个物品放入背包使背包中物品的总价值最大且背包中物品的总体积不超过背包的容量。
输入描述
第 1 1 1 行 2 2 2 个正整数, n , m n, m n,m 代表物品种数与背包容量。
接下来 n n n 行每行 2 2 2 个正整数, w i w_i wi 表示第 i i i 个物品的体积, v i v_i vi 表示第 i i i 个物品的价值。
输出描述
输出 1 1 1 个整数,即能装下的最大价值。
样例1
输入
3 70 71 100 69 1 1 2
输出
140
提示
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 20 1 ≤ n ≤20 1≤n≤20, 1 ≤ m ≤ 1 0 6 1 ≤ m ≤ 10^6 1≤m≤106, 1 ≤ w i , v i ≤ 100 1 ≤ w_i, v_i ≤100 1≤wi,vi≤100。
2. 思路
还是列出表格(行表示物品种类,列表示背包容量):
(21,3)(…)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 21 | 21 | 21 | 42 | 42 | 42 | 63 |
… | … | … | … | … | … | … | … | … | … | … |
那么仍然是两种情况:
if (j < v[i])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = max(dp[i-1][j], e[i]+dp[i][j-v[i]])
3. 参考答案
#include <iostream>
#include <algorithm>
using namespace std;
int n, m;
int v[25];
int e[25];
int dp[25][1000005];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> e[i];
for (int i = 1; i <= n; i++) // 物品种数
for (int j = 1; j <= m; j++) // 背包容量
if (j < v[i])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = max(dp[i-1][j], e[i]+dp[i][j-v[i]]);
cout << dp[n][m];
return 0;
}
四、模型优化
1. 0-1 背包模型
我们可以考虑给 dp[][]
降维。只需要倒序遍历到 v[i]
进行更新即可:
#include <iostream>
#include <algorithm>
using namespace std;
int n, m;
int v[105]; // 体积
int e[105]; // 能量值
int dp[1005];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> e[i];
for (int i = 1; i <= n; i++) // 物品个数
for (int j = m; j >= v[i]; j--) // 背包容量
dp[j] = max(dp[j], e[i]+dp[j-v[i]]);
cout << dp[m];
return 0;
}
2. 完全背包模型
考虑给 dp[][]
降维。只需要正序遍历到 v[i]~m
进行更新即可:
#include <iostream>
#include <algorithm>
using namespace std;
int n, m;
int v[105]; // 体积
int e[105]; // 能量值
int dp[1005];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> e[i];
for (int i = 1; i <= n; i++) // 物品个数
for (int j = v[i]; j <= m; j++) // 背包容量
dp[j] = max(dp[j], e[i]+dp[j-v[i]]);
cout << dp[m];
return 0;
}
五、应用
1. 开心的金明(加强版)
题目描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N N N 元钱就行”。今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的 N N N 元。
于是,他把每件物品规定了一个重要度,分为 5 5 5 等:用整数 1 − 5 1-5 1−5 表示,第 5 5 5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过 N N N 元(可以等于 N N N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j j j 件物品的价格为v[j]
,重要度为w[j]
,共选中了 k k k 件物品,编号依次为 j 1 , j 2 , ⋯ , j k j_1,j_2,\cdots,j_k j1,j2,⋯,jk。
则所求的总和为: v [ j 1 ] × w [ j 1 ] + v [ j 2 ] × w [ j 2 ] + … + v [ j k ] × w [ j k ] v[j1] × w[j1] + v[j2] × w[j2] + … +v[jk] × w[jk] v[j1]×w[j1]+v[j2]×w[j2]+…+v[jk]×w[jk]
请你帮助金明设计一个满足要求的购物单。
输入描述
第一行,为 2 2 2 个正整数,用一个空格隔开: n , m n,m n,m(其中 N ≤ 10000000 N \le 10000000 N≤10000000 表示总钱数, m < 25 m < 25 m<25 为希望购买物品的个数)
从第 2 2 2 行到第 m + 1 m + 1 m+1 行,第 j j j 行给出了编号为 j − 1 j - 1 j−1 的物品的基本数据,每行有 2 2 2 个非负整数 v v v 和 p p p,其中 v v v 表示该物品的价格( v ≤ 10000 v \le 10000 v≤10000), p p p 表示该物品的重要度( 1 − 5 1 - 5 1−5)。
输出描述
1 1 1 个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值( < 100000000 <100000000 <100000000)。
样例1
输入
1000 5 800 2 400 5 300 5 400 3 200 2
输出
3900
直接用 0-1 背包模型就可以了。
#include <iostream>
#include <algorithm>
using namespace std;
int n, m;
int x, y;
int v[30];
int e[30];
int dp[10000005];
int main()
{
cin >> m >> n;
for (int i = 1; i <= n; i++)
{
cin >> x >> y;
v[i] = x;
e[i] = x*y;
}
for (int i = 1; i <= n; i++)
for (int j = m; j >= v[i]; j--)
dp[j] = max(dp[j], e[i]+dp[j-v[i]]);
cout << dp[m];
return 0;
}
2. 最大约数和
选取和不超过 S S S 的若干个不同的正整数,使得所有数的约数(不含它本身)之和最大。
#include <iostream>
#include <algorithm>
using namespace std;
int s;
int v[1005];
int e[1005];
int dp[1005];
int main()
{
cin >> s;
for (int i = 1; i <= s; i++)
{
v[i] = i;
for (int j = 1; j < i; j++)
if (i % j == 0)
e[i] += j;
}
for (int i = 1; i <= s; i++)
for (int j = s; j >= v[i]; j--)
dp[j] = max(dp[j], e[i]+dp[j-v[i]]);
cout << dp[s];
return 0;
}
3. 烹饪方案
题目描述
由于你的帮助,火星只遭受了最小的损失。但 gw 懒得重建家园了,就造了一艘飞船飞向遥远的 earth 星。不过飞船飞到一半,gw 发现了一个很严重的问题:肚子饿了~
gw 还是会做饭的,于是拿出了储藏的食物准备填饱肚子。gw 希望能在 T T T 时间内做出最美味的食物,但是这些食物美味程度的计算方式比较奇葩,于是绝望的 gw 只好求助于你了。
一共有 n n n 件食材,每件食材有三个属性, a i , b i , c i a_i,b_i,c_i ai,bi,ci,如果在 t t t 时刻完成第 i i i 样食材则得到 a i − t × b i a_i−t×b_i ai−t×bi 的美味指数,用第 i i i 件食材做饭要花去 c i c_i ci 的时间。
众所周知,gw 的厨艺不怎么样,所以他需要你设计烹调方案使得美味指数最大。
输入描述
第一行是两个正整数 T T T 和 n n n,表示到达地球所需时间和食材个数。
下面一行 n n n 个正整数, a 1 , a 2 , ⋯ , a n a_1,a_2,⋯,a_n a1,a2,⋯,an
下面一行 n n n 个正整数, b 1 , b 2 , ⋯ , b n b_1,b_2,⋯,b_n b1,b2,⋯,bn
下面一行 n n n 个正整数, c 1 , c 2 , ⋯ , c n c_1,c_2,⋯,c_n c1,c2,⋯,cn
输出描述
输出最大美味指数
样例1
输入
74 1 502 2 47
输出
408
提示
对于 40 % 40\% 40% 的数据 1 ≤ n ≤ 10 1≤n≤10 1≤n≤10
对于 100 % 100\% 100% 的数据 1 ≤ n ≤ 50 1≤n≤50 1≤n≤50
所有数字均小于 100000 100000 100000
#include <iostream>
#include <algorithm>
using namespace std;
struct Node
{
int a, b, c;
}food[55];
int t, n;
int maxn;
int dp[100005];
bool cmp(Node x, Node y)
{
return x.b*y.c > x.c*y.b;
}
int main()
{
cin >> t >> n;
for (int i = 1; i <= n; i++) cin >> food[i].a;
for (int i = 1; i <= n; i++) cin >> food[i].b;
for (int i = 1; i <= n; i++) cin >> food[i].c;
sort(food+1, food+n+1, cmp); // 细节1: 记得排序
for (int i = 1; i <= n; i++)
for (int j = t; j >= food[i].c; j--)
dp[j] = max(dp[j], food[i].a-j*food[i].b+dp[j-food[i].c]);
for (int i = 1; i <= t; i++) // 细节2: 不一定是dp[t]最大
maxn = max(maxn, dp[i]);
cout << maxn;
return 0;
}