【题解】【动态规划】—— [NOIP2005 普及组] 采药
01背包问题模型讲解
1.概念解析
01背包是指在N
件物品取出若干件放在空间为M
的背包里,每件物品的体积为
W
1
W_1
W1,
W
2
W_2
W2至
W
n
W_n
Wn,与之相对应的价值为
P
1
P_1
P1,
P
2
P_2
P2至
P
n
P_n
Pn。通常要求使得价值最大的方案。因为每件物品只有放或者不放两种可能,刚好对应0
和1
,所以这种问题叫做01背包。
2.举例了解
01背包是一类很经典的运用了动态规划思想解答的题目。下面我们先来看一个例子:
将下面 5 5 5个物品放入容量为 20 20 20的背包,使得背包内物品的总价值最大。
物品属性/物品编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
重量 | 10 | 13 | 5 | 8 | 20 |
价格 | 11 | 15 | 9 | 7 | 10 |
大多数人第一眼就会使用贪心来做这道题。根据贪心策略,每次选择当前价格最高的。但是我们会得到10
,即只选择
5
5
5号物品。可正解是24
,选择
2
2
2号物品和
3
3
3号物品。贪心的鼠目寸光在这里体现的淋漓尽致。
那么在这里,我们就可以使用动态规划的思想来解这道题。先来一个动态规划五步走。
1.抽象问题:在五个物品里面选择,使得重量和不超过 20 20 20的情况下总价值最大。
2.状态:dp[i][j]
表示只在前i
个物品选择,放进容量为j
的背包的最大价值。
3.初始条件:dp[0][i]=0
4.状态转移方程:
d p [ i ] [ j ] = { j < a [ i ] , d p [ i − 1 ] [ j ] j ≥ a [ i ] , m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − a [ i ] . w e i g h t ] + a [ i ] . v a l u e ) dp[i][j]= \begin{cases} j<a[i],\space dp[i-1][j]\\ j\geq a[i],\space max(dp[i-1][j],dp[i-1][j-a[i].weight]+a[i].value) \end{cases} dp[i][j]={j<a[i], dp[i−1][j]j≥a[i], max(dp[i−1][j],dp[i−1][j−a[i].weight]+a[i].value)
其中,a[i]weight
表示编号为i
的物品的重量,a[i].value
表示编号为i
的物品的价值。
5.答案:dp[n][m]
。在此题中, n = 5 n=5 n=5, m = 20 m=20 m=20。
首先是状态,根据动态规划思想的核心,我们将这个大问题拆解成相同的子问题。这就是状态。
然后是初始状态,容易知道,前i
个物品放进容量为
0
0
0的背包里,无论怎么样都放不下,也就不可能创造价值。
接下来是状态转移方程。首先,对于目前枚举的j
,如果j
小于a[i].weight
,即当前背包一件编号为i
的物品都装不下,那么就不能对总的方法数产生影响。这样我们就只能继承上一层的方法数。
否则,j
大于等于a[i].weight
,那么我们有两种选择:一种是装下这个物品,一种是不装这个物品。
装下这个物品。那么背包的容量就只剩下了j-a[i].weight
,那么我们就需要取容量为j-a[i].weight
的背包能装下的物品的最大价值。可以直接查表解决,即dp[i-1][j-a[i].weight]
。
不装这个物品,跟上面一样,直接继承上一层的方法数,即dp[i-1][j]
。
取两者最大值,即 m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − a [ i ] . w e i g h t ] + a [ i ] . v a l u e ) max(dp[i-1][j],dp[i-1][j-a[i].weight]+a[i].value) max(dp[i−1][j],dp[i−1][j−a[i].weight]+a[i].value)就行了。
最后是答案,直接就根据状态的定义理解就行了,即 d p [ 物品数 ] [ 背包容量 ] dp[物品数][背包容量] dp[物品数][背包容量]。
既然是动态规划,那必定少不了填表,以下是针对上述问题的二维表格,请耐心观察,进一步加深对动态转移方程的理解。
i/j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 |
2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 11 | 11 | 11 | 15 | 15 | 15 | 15 | 15 | 15 | 15 | 15 | 15 |
3 | 0 | 0 | 0 | 0 | 9 | 9 | 9 | 9 | 9 | 11 | 11 | 11 | 15 | 15 | 20 | 20 | 20 | 24 | 24 | 24 |
4 | 0 | 0 | 0 | 0 | 9 | 9 | 9 | 9 | 9 | 11 | 11 | 11 | 16 | 16 | 20 | 20 | 20 | 24 | 24 | 24 |
5 | 0 | 0 | 0 | 0 | 9 | 9 | 9 | 9 | 9 | 11 | 11 | 11 | 16 | 16 | 20 | 20 | 20 | 24 | 24 | 24 |
可以发现,答案刚好符合我们的预测。接下来就是编写程序了,具体请看下列代码:
//输入样例复制
5 20
10 11
13 15
5 9
8 7
20 10
3.示例程序
直接输出答案:
#include<bits/stdc++.h>
using namespace std;
struct object { // 储存物品的重量和价值
int weight, value;
}a[110];
int n, m, dp[110][1010];
// 用dp[i][j]储存在前i个物品选择放入容量为j的背包时的最大价值
int main() {
cin >> n >> m;// 输入物品数和背包重量
for (int i = 1; i <= n; i++)
cin >> a[i].weight >> a[i].value;
for (int i = 1; i <= n; i++)// 枚举物品
for (int j = 1; j <= m; j++)// 枚举重量
// 状态转移方程,每一个物品都可以选择放或不放
if (j >= a[i].weight) // 装得下
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - a[i].weight] + a[i].value);
else // 装不下
dp[i][j] = dp[i - 1][j];
cout << dp[n][m] << endl;
return 0;
}
输出表格:
#include<bits/stdc++.h>
using namespace std;
struct object { // 储存物品的重量和价值
int weight, value;
}a[110];
int n, m, dp[110][1010];
// 用dp[i][j]储存在前i个物品选择放入容量为j的背包时的最大价值
int main() {
cin >> n >> m;// 输入物品数和背包重量
for (int i = 1; i <= n; i++)
cin >> a[i].weight>> a[i].value;
for (int i = 1; i <= n; i++)// 枚举物品
for (int j = 1; j <= m; j++)// 枚举重量
// 状态转移方程,每一个物品都可以选择放或不放
if (j >= a[i].weight) // 装得下
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - a[i].weight] + a[i].value);
else // 装不下
dp[i][j] = dp[i - 1][j];
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= t; j++)
cout << dp[i][j] << " ";
puts("");
}
return 0;
}
纯净无注释版:
#include<bits/stdc++.h>
using namespace std;
struct object {
int weight, value;
}a[110];
int n, m, dp[110][1010];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i].weight >> a[i].value;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (j >= a[i].weight)
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - a[i].weight] + a[i].value);
else
dp[i][j] = dp[i - 1][j];
cout << dp[n][m] << endl;
return 0;
}
4.滚动数组优化
在上面,我们可以发现:每一个dp[i][j]
都只会查询上一层,即dp[i-1][j]
。所以,为了节省空间,我们可以只用一个一维数组记录此时的状态。
但是要注意,循环需要改成这样:
for (int j = m; j >= a[i].weight; j--)
有以下两点:
1.我们在更新
dp[j]
的值的时候,只需要用到小于j
的状态的值,即 d p [ k ] ( k < j ) dp[k](k<j) dp[k](k<j)。如果从小到大更新,会造成使用前面更新过的值的情况。
2. d p [ j ] dp[j] dp[j]在更新前本身储存的就是上一层的值,所以我们只需要枚举到需要改变的状态,即a[i].weiight
就行了。
示例代码如下:
#include<bits/stdc++.h>
using namespace std;
struct object { // 储存物品的重量和价值
int weight, value;
}a[110];
int n, m, dp[1010];
// 用dp[j]储存当前放入容量为j的背包时的最大价值
int main() {
cin >> n >> m;// 输入物品数和背包重量
for (int i = 1; i <= n; i++)
cin >> a[i].weight >> a[i].value;
for (int i = 1; i <= n; i++)// 枚举物品
for (int j = m; j >= a[i].weight; j--)// 枚举重量
// 状态转移方程,每一个物品都可以选择放或不放
dp[j] = max(dp[j], dp[j - a[i].weight] + a[i].value);
cout << dp[m] << endl;
return 0;
}
纯净无注释版:
#include<bits/stdc++.h>
using namespace std;
struct object {
int weight, value;
}a[110];
int n, m, dp[1010];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i].weight >> a[i].value;
for (int i = 1; i <= n; i++)
for (int j = m; j >= a[i].weight; j--)
dp[j] = max(dp[j], dp[j - a[i].weight] + a[i].value);
cout << dp[m] << endl;
return 0;
}
顺便提一句,这里不太建议大家使用一维 d p dp dp,因为有点难以理解。如果不完全理解一维 d p dp dp,还是老老实实使用二维 d p dp dp吧。
[NOIP2005 普及组] 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 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
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 普及组第三题
1.题意解析
这道题只需要套上面的模版就行了。把规定时间看成背包,采每株草药的时间看成重量。具体请看代码注释。
2.AC代码
2.1.二维dp
#include<bits/stdc++.h>
using namespace std;
struct medicine//储存草药的采摘时间和价值
{
int t,value;
}a[110];
//用dp[i][j]储存背包容量为j时在前i个物品选择的最大价值
int t,m,dp[110][1010];
int main()
{
cin>>t>>m;
for(int i=1;i<=m;i++)
cin>>a[i].t>>a[i].value;
for(int i=1;i<=m;i++)//枚举物品
for(int j=1;j<=t;j++)//枚举重量
//状态转移方程,每一个物品都可以选择放或不放
if(j>=a[i].t)//装得下
dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i].t]+a[i].value);
else//装不下
dp[i][j]=dp[i-1][j];
cout<<dp[m][t];
return 0;
}
2.2.一维dp
#include<bits/stdc++.h>
using namespace std;
struct medicine//储存草药的总时间和总价值
{
int t,value;
}a[110];
int t,m,dp[1010]={};//用dp[j]储存背包容量为j时的最大价值
int main()
{
cin>>t>>m;
for(int i=1;i<=m;i++)
cin>>a[i].t>>a[i].value;
for(int i=1;i<=m;i++)
for(int j=t;j>=a[i].t;j--)//一定要从后往前遍历
//状态转移方程,每一个物品都可以选择放或不放
dp[j]=max(dp[j],dp[j-a[i].t]+a[i].value);
cout<<dp[t];
return 0;
}
喜欢就订阅此专辑吧!
【蓝胖子编程教育简介】
蓝胖子编程教育,是一家面向青少年的编程教育平台。平台为全国青少年提供最专业的编程教育服务,包括提供最新最详细的编程相关资讯、最专业的竞赛指导、最合理的课程规划等。本平台利用趣味性和互动性强的教学方式,旨在激发孩子们对编程的兴趣,培养他们的逻辑思维能力和创造力,让孩子们在轻松愉快的氛围中掌握编程知识,为未来科技人才的培养奠定坚实基础。
欢迎扫码关注蓝胖子编程教育