来吧,同学,让我教你做DP!
01背包
问题基本模板 :
有一个大小为 m 的背包,有 n 个物品,每个物品有两个元素,是体积 v 和价格 w ,物品无法分割,要求拿物体,物体只能被拿一次,使得价格最大,最大为多少?
状态:dp[i][j] 表示 在第 i 个物品时,体积为 j 时,价格的最大值。
思路:首先枚举 n 个物品,再枚举背包容量,对于每一个物品来说,有拿、不拿 两种状态,顾 dp[i][j] 可以从 dp[i-1][j] 转移而来,这即是不拿的第i个物品的状态。还可以从 dp[i-1][j-v[i]] 转移来,拿第i个物品要背包内腾出 v[i] 的空间大小,故为 j-v[i] 。
转移方程:dp[i][j] = max ( dp[i-1][j] , dp[i-1][j-v[i]] + w[i])
例题:[NOIP2005 普及组] 采药
01背包模板
题目描述
医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 2 2 2 个整数 T T T( 1 ≤ T ≤ 1000 1 \le T \le 1000 1≤T≤1000)和 M M M( 1 ≤ M ≤ 100 1 \le M \le 100 1≤M≤100),用一个空格隔开, T T T 代表总共能够用来采药的时间, M M M 代表山洞里的草药的数目。
接下来的 M M M 行每行包括两个在 1 1 1 到 100 100 100 之间(包括 1 1 1 和 100 100 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
样例 #1
样例输入 #1
70 3
71 100
69 1
1 2
样例输出 #1
3
提示
【数据范围】
- 对于 30 % 30\% 30% 的数据, M ≤ 10 M \le 10 M≤10;
- 对于全部的数据, M ≤ 100 M \le 100 M≤100。
【题目来源】
NOIP 2005 普及组第三题
问题解决
根据题目的条件,判断出,时间 就是 “物体的体积”,草药的价值 就是 物体的价值,规定时间 就是 “背包容量”。所以可以判断出这是一道 01 背包的题目。
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int m,n;
int v[N],w[N];
int dp[N][N];
int main()
{
cin >> m >> n;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
memset(dp,128,sizeof(dp)); // 初始化最小
dp[0][0] = 0;
for(int i=1;i<=n;i++){ // 枚举所有物品
for(int j=0;j<=m;j++){ //枚举背包容量
if(j-v[i]>=0) dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
else dp[i][j] = dp[i-1][j]; // 边界判断 防止越界
}
}
int mx = INT_MIN;
for(int i=1;i<=m;i++) mx = max(mx,dp[n][i]);
cout << mx;
return 0;
}
优化1:
以前定义 :dp[i][j] 表示 在第 i 个物品时,体积为 j 时,价格的最大值
新定义:dp[i][j] 表示 在第 i 个物品时,体积不超过 j 时,价格的最大值
这样优化是可以默认将 dp 数组初始化为 0 ,最后直接输出 dp[n][m] ,不用再次枚举
优化代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int m,n;
int v[N],w[N];
int dp[N][N]; //开全局数组默认为 0
int main()
{
cin >> m >> n;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j-v[i]>=0) dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
else dp[i][j] = dp[i-1][j];
}
}
cout << dp[n][m]; //最后直接输出
return 0;
}
优化2
观察代码可以发现
1. j 的遍历顺序不影响结果的正确性。
2. 每次计算dp[i][j] 只会 使用到 dp[i-1]上层的内容。
所以可以利用滚动数组,对空间复杂度进行优化: dp[j] = max( dp[j] , dp[j - v[i]] + w[i])
注:此处为 dp[j] 不为 dp[j-1] 因为如果为 dp[j-1] 的话,就会将上一次的覆盖。
因为会访问到dp[j]之前的数组,所以从右往左的遍历,保留上一层的计算结果。
优化代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int m,n;
int v[N],w[N];
int dp[N];
int main()
{
cin >> m >> n;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
if(j-v[i]>=0) dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout << dp[m];
return 0;
}
优化 3(小小优化):循环中的判断比较多余,只用 j 从 m 到 v[i] 进行枚举就可以了。
最终代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int n,m;
int v[N],w[N];
int dp[N];
int main()
{
cin >> m >> n;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
dp[j] = max(dp[j],dp[j-v[i]] + w[i]);
}
}
cout << dp[m];
return 0;
}
二维背包
问题基本模板 :
有一个大小为 m 的背包,人的体力为 t ,有 n 个物品,每个物品有三个元素,是体积 v1 、价格 w、消耗的体力 v2 ,物品无法分割,要求拿物体,在体积和消耗体力 都 在范围之内,使得价格最大,最大为多少?
状态:dp[i][j][o] 表示 在第 i 个物品时,体积为 j ,所消耗的体力为 o 时,价格的最大值。
思路:任然考虑两种情况,第i个物品没取了,既是dp[i-1][j][o] ,还有第i个物品取了,体积为 j-v1[i],体力为 o-v2[i]. 所以 dp[i][j][o] = dp[ i - 1 ] [ j - v1[i] ] [ o - v2[i] ] + w[i]
转移方程:dp[i][j] = max ( dp[i - 1] [j] [o] , dp[i - 1] [j - v1[i]] [o - v2[i]] + w[i])
优化1
同理设立状态 dp[i][j][o] 表示 在第 i 个物品时,体积不超过 j ,所消耗的体力不超过 o 时,价格的最大值。 这样就不用初始化为负无穷,最后直接输出 dp[n][m][t]。
优化2
三维的数组空间太大了,可以像01背包一样优化。
直接给转移结论:dp[j] [o] = max(dp[j] [o],dp[j - v1[i]] [o - v2[i]] + w[i])
仍需要倒叙枚举到 v1[i] 、v2[i]
例题:NASA的食物计划
二维背包模板
题目描述
航天飞机的体积有限,当然如果载过重的物品,燃料会浪费很多钱,每件食品都有各自的体积、质量以及所含卡路里。在告诉你体积和质量的最大值的情况下,请输出能达到的食品方案所含卡路里的最大值,当然每个食品只能使用一次。
输入格式
第一行 2 2 2 个整数,分别代表体积最大值 h h h 和质量最大值 t t t。
第二行 1 1 1 个整数代表食品总数 n n n。
接下来 n n n 行每行 3 3 3 个数 体积 h i h_i hi,质量 t i t_i ti,所含卡路里 k i k_i ki。
输出格式
一个数,表示所能达到的最大卡路里(int
范围内)
样例 #1
样例输入 #1
320 350
4
160 40 120
80 110 240
220 70 310
40 400 220
样例输出 #1
550
提示
对于 100 % 100\% 100% 的数据, h , t , h i , t i ≤ 400 h,t,h_i,t_i \le 400 h,t,hi,ti≤400, n ≤ 50 n \le 50 n≤50, k i ≤ 500 k_i \le 500 ki≤500。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 500;
int m,t,n;
int v1[N],v2[N],w[N];
int dp[N][N];
int main()
{
cin >> m >> t >> n;
for(int i=1;i<=n;i++) cin >> v1[i] >> v2[i] >> w[i];
for(int i=1;i<=n;i++){
for(int j=m;j>=v1[i];j--){ //同样枚举到v1[i]
for(int o=t;o>=v2[i];o--){ //同样枚举到v2[i]
dp[j][o] = max(dp[j][o],dp[j-v1[i]][o-v2[i]]+w[i]);
}
}
}
cout << dp[m][t];
return 0;
}
完全背包
问题基本模板 :
有一个大小为 m 的背包,有 n 个物品,每个物品有两个元素,是体积 v 和价格 w ,物品无法分割,要求拿物体,物体能拿无数次,使得价格最大,最大为多少?
状态:dp[i][j] 表示 在第 i 个物品时,体积为 j 时,价格的最大值。
思路:完全背包 与 01背包极为相似,唯一不同之处在于,状态分为 一个都没有取 和 取了。所以当没有取时 dp[i][j] = dp[i-1][j] 当取了时,就是 dp[i][j] = dp[i][j-v[i]] + w[i]。
转移方程:dp[i][j] = max ( dp[i-1][j] , dp[i][j-v[i]] + w[i])
dp[i][j-v[i]] + w[i] 顶替了 dp[i-1][j-v[i]] + w[i]
优化
当然任然可以使用滚动数组优化,只不过,这时我们就需要 从前往后 遍历即可。
例题:疯狂的采药
完全背包模板
题目描述
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
1 1 1. 每种草药可以无限制地疯狂采摘。
2 2 2. 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入格式
输入第一行有两个整数,分别代表总共能够用来采药的时间 t t t 和代表山洞里的草药的数目 m m m。
第 2 2 2 到第 ( m + 1 ) (m + 1) (m+1) 行,每行两个整数,第 ( i + 1 ) (i + 1) (i+1) 行的整数 a i , b i a_i, b_i ai,bi 分别表示采摘第 i i i 种草药的时间和该草药的价值。
输出格式
输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
样例 #1
样例输入 #1
70 3
71 100
69 1
1 2
样例输出 #1
140
提示
数据规模与约定
- 对于 30 % 30\% 30% 的数据,保证 m ≤ 1 0 3 m \le 10^3 m≤103 。
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ m ≤ 1 0 4 1 \leq m \le 10^4 1≤m≤104, 1 ≤ t ≤ 1 0 7 1 \leq t \leq 10^7 1≤t≤107,且 1 ≤ m × t ≤ 1 0 7 1 \leq m \times t \leq 10^7 1≤m×t≤107, 1 ≤ a i , b i ≤ 1 0 4 1 \leq a_i, b_i \leq 10^4 1≤ai,bi≤104。
AC code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 10005;
const ll NN = 1e7+5;
ll m,n;
ll v[N],w[N];
ll dp[NN];
int main()
{
cin >> m >> n;
for(ll i=1;i<=n;i++) cin >> v[i] >> w[i];
for(ll i=1;i<=n;i++){
for(ll j=v[i];j<=m;j++){ // 从前往后遍历
dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout << dp[m];
return 0;
}
多重背包问题
问题基本模板 :
有一个大小为 m 的背包,有 n 个物品,每个物品有两个元素,是体积 v 和价格 w ,物品无法分割,要求拿物体,物体只能被拿 L 次,使得价格最大,最大为多少?
状态:dp[i][j] 表示 在第 i 个物品时,体积不到 j 时,价格的最大值。
思路:多重背包与01背包比较相似,一个可以取 L 次的物品,相当于 有 L 个可以取一次的物品,于是只用在第一层枚举个数和第二层枚举背包容量之间加一层枚举物品的个数。
转移方程:dp[j] = max(dp[j] , dp[j - v[i]] + w[i])
例题:[NOIP1996 提高组] 砝码称重
多重背包模板
题目描述
设有 1 g 1\mathrm{g} 1g、 2 g 2\mathrm{g} 2g、 3 g 3\mathrm{g} 3g、 5 g 5\mathrm{g} 5g、 10 g 10\mathrm{g} 10g、 20 g 20\mathrm{g} 20g 的砝码各若干枚(其总重 $ \le 1000$),可以表示成多少种重量?
输入格式
输入方式: a 1 , a 2 , a 3 , a 4 , a 5 , a 6 a_1 , a_2 ,a_3 , a_4 , a_5 ,a_6 a1,a2,a3,a4,a5,a6
(表示 1 g 1\mathrm{g} 1g 砝码有 a 1 a_1 a1 个, 2 g 2\mathrm{g} 2g 砝码有 a 2 a_2 a2 个, … \dots …, 20 g 20\mathrm{g} 20g 砝码有 a 6 a_6 a6 个)
输出格式
输出方式:Total=N
( N N N 表示用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况)
样例 #1
样例输入 #1
1 1 0 0 0 0
样例输出 #1
Total=3
提示
【题目来源】
NOIP 1996 提高组第四题
注:这道题归类于可达性dp,一般用 bool 类型来定义 dp 数组
dp[i] 表示 i 这个重量是否可以表示出来
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3+5;
int l[7],w[7] = {0,1,2,3,5,10,20};
bool dp[N];
int main()
{
for(int i=1;i<=6;i++) cin >> l[i];
dp[0] = true;
for(int i=1;i<=6;i++){
for(int j=1;j<=l[i];j++){ // 在01 背包的基础上加了一层枚举数量的循环
for(int o = N;o>=0;o--){
if(dp[o] && o+w[i]<=N) dp[o+w[i]] = true;
// 如果 dp[o] 可以被表示,那么 dp[o+w[i]] 也可以被表示
}
}
}
int cnt = 0;
for(int i=1;i<=N;i++){
if(dp[i]) cnt++;
}
cout << "Total=" << cnt;
return 0;
}
多重背包的二进制优化
不难发现, 朴素的多重背包 的 算法时间复杂度 为 O(nml).当数据上强度,就会TLE。所以就要进行优化。
首先有一个定理
例如:n = 8 时,就有 1 2 4 1 这几个数。其中的任意数可以用1 2 4 1 表示出
不严谨的证明: 将n表示成二进制的形式,0bx1 x2 x3 x4… 所以按位拆开即为 2的k次方。
所以我们只需要枚举1 2 4 1这几个数,就可以免去 枚举 8,就对时间复杂度进行优化。
优化例题:宝物筛选
多重背包优化
题目描述
终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。
这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。看来小 FF 只能含泪舍弃其中的一部分宝物了。
小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 FF 有一个最大载重为 W W W 的采集车,洞穴里总共有 n n n 种宝物,每种宝物的价值为 v i v_i vi,重量为 w i w_i wi,每种宝物有 m i m_i mi 件。小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。
输入格式
第一行为一个整数 n n n 和 W W W,分别表示宝物种数和采集车的最大载重。
接下来 n n n 行每行三个整数 v i , w i , m i v_i,w_i,m_i vi,wi,mi。
输出格式
输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。
样例 #1
样例输入 #1
4 20
3 9 3
5 9 1
9 4 2
8 1 3
样例输出 #1
47
提示
对于 30 % 30\% 30% 的数据, n ≤ ∑ m i ≤ 1 0 4 n\leq \sum m_i\leq 10^4 n≤∑mi≤104, 0 ≤ W ≤ 1 0 3 0\le W\leq 10^3 0≤W≤103。
对于 100 % 100\% 100% 的数据, n ≤ ∑ m i ≤ 1 0 5 n\leq \sum m_i \leq 10^5 n≤∑mi≤105, 0 ≤ W ≤ 4 × 1 0 4 0\le W\leq 4\times 10^4 0≤W≤4×104, 1 ≤ n ≤ 100 1\leq n\le 100 1≤n≤100。
优化代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 4e4+5;
int dp[N];
int v[N], w[N], k[N];
int main()
{
int n, m;
cin >> n >> m;
for(int i=1; i<=n; i++) cin>>w[i]>>v[i]>>k[i];
for(int i=1; i<=n; i++){
//把k[i]进行二进制分组
vector<pair<int,int>>vec;//存储分组结果
//存储pair<体积,价值>;
for(int j=1; j<=k[i]; j*=2){
vec.push_back({j*v[i], j*w[i]});
k[i] -= j;
}
if(k[i]!=0) vec.push_back({k[i]*v[i], k[i]*w[i]});
//对分组后的物品 进行01背包
for(int k=0; k<vec.size(); k++){
int vk = vec[k].first, wk = vec[k].second;
for(int j=m; j>=vk; j--){
dp[j] = max(dp[j], dp[j-vk]+wk);
}
}
}
cout << dp[m] << endl;
return 0;
}
总结
对于 背包dp问题都是基于 01背包 的基础上扩展而来,其中的枚举顺序很重要。
这里有一个大佬写的 dp 博客:大佬博客