P1048 [NOIP2005 普及组] 采药(01背包问题)

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 2 个整数 T(1≤T≤1000)和 M(1≤M≤100),用一个空格隔开 T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。

接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

输入输出样例

输入 #1

70 3
71 100
69 1
1 2

输出 #1

3

思路

这道题是经典的01背包问题。之前讲的P1164 小A点菜就是01背包的变形,看完这道题目的同学可以再去看一下这篇,可能可以加深对01背包问题的理解。

先来讲讲什么是01背包问题吧。

01背包问题最初始的场景是:有一个背包,其容量为W。同时有n个物品,每个物品i有一个重量w和价值v。问题的目标是在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大。

了解了01背包的基本场景,那么我们要如何来解决这一问题呢?在一个固定的容量下,我们要尽可能地使装的东西有价值,我们还得考虑体积是否合适,好像考虑起来很复杂。要怎么处理这一情况呢,我们可以将其分阶段,进行有序化处理。

这里就需要用到动态规划这一思路,动态规划是解决01背包问题较为常见的一种解决思路。动态规划呢,其具有解决重叠子问题和最优子结构的特性,解决重叠子问题的话可以去参考P1255 数楼梯 中的思路,通过记忆化搜索减少计算重复问题的次数。我们这里主要是表现出最优子结构的特性,我们将这个问题划分成一个阶段一个阶段来解决,每个结构都选取最优的选择,然后将这些组合起来,我们总的结果就肯定也是最优的了。

所谓分阶段,那么就是一个接着一个来考虑这些物品。首先当我们面临第一个物品时,我们还有x(1 <= x <= V)的容量,我们只有两种情况,装得下或者装不下,装不下的话就只能不要,若是装得下,那么就还有两种选择要或者不要,我们得做出个最优的选择,即价值最大的选择,到底是选了价值大还是不选的大,这里就需要进行一个判断,因为是第一件物品,所以在x大于等于其重量的情况下,我们只有选它才是最大价值。考虑完第一个了,接下来就是有第二个在的情况,每一个物品都是在上一个物品的最优解的基础上来考虑,x小于其重量就没办法,只能与只有第一个的情况的价值一样,这个很好理解;在x大于等于其重量的情况下,情况就和只有第一个的不同了,我们要判断哪个最大,(1)不要:那么就像x小于其重量一样,与只有第一个的情况的价值一样(2)要:既然要了,那么我们就得给他腾出占地,那么我们的背包的x就得相应减掉其重量,然后再x - m(其重量)对应的价值加上其本身价值,就是要这个物品后的价值。将(1)和(2)的价值一比较,选出最大的,就是最优解。之后对于第三个,第四个……都是一个道理,依次类推,一直到最后一个为止。

上述中在每一个物品下每一个 x 下所对应的价值就是当前位置的状态,选择价值更大的那个的公式就是状态转移方程。动态规划的核心就是状态和状态转移方程。

具体思路就是如此,那么要如何写成代码呢?上面描述的既要一个一个物品,还要在物品对于每一个容量,那不就和一个二维表格一样吗,那就是一个二维数组来实现,质量和价值就以两个一维数组来记录,使用两个for循环,内层每次都判断 j 与 质量mi的关系,小于怎么样,大于等于怎么样,具体就跟上文一样。这道题目中背包容量就是时间总数,花费时间数就是重量,草药的价值当然就是价值。

接下来就是代码的呈现咯!

AC代码

二维方法

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
	int n, m;
	cin >> n >> m;
	vector<int> t(m + 1, 0);
	vector<int> v(m + 1, 0);
	vector< vector<int> > bag(m + 1, vector<int>(n + 1, 0));
	for (int i = 1; i <= m; i++) {
		cin >> t[i] >> v[i];
	}
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= n; j++) {//双重循环
			if (j >= t[i]) {//如果质量大于等于的情况
				bag[i][j] = max(bag[i - 1][j], bag[i - 1][j - t[i]] + v[i]);//取更大的价值
			}
			else {//如果小于的情况
				bag[i][j] = bag[i - 1][j];
			}
		}
	}
	cout << bag[m][n];
	return 0;
}

这里需要注意的就是在大于等于情况下取最大价值的办法, bag[i][j] = max(bag[i - 1][j], bag[i - 1][j - t[i]] + v[i]);用了max函数,比较不要:bag[i-1][j] 与 要:bag[i-1][j-t[i]] + v[i]的大小。

这里我们还可以发现,新的状态是允许边读边计算的,所以我们不必把t,m保存下来。

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
	int n, m;
	cin >> n >> m;
	vector< vector<int> > bag(m + 1, vector<int>(n + 1, 0));
    int t , v;
	for (int i = 1; i <= m; i++) {
		cin >> t >> v;
        for (int j = 1; j <= n; j++) {
			if (j >= t) {
				bag[i][j] = max(bag[i - 1][j], bag[i - 1][j - t] + v);
			}
			else {
				bag[i][j] = bag[i - 1][j];
			}
		}
	}
	cout << bag[m][n];
	return 0;
}

优化:滚动数组

我们还可以将数组变成一维的。怎么实现的呢,数组是从上到下,从左到右计算的,bag[i][j]只会由上一层的价值来变化,其他层的与其无关,在j小于时间t的时候,bag[i][j] 都是等于 bag[i-1][j],变化的只有大于等于的时候,当计算完第 i 层的时候,第i+1层就已经不会再用到了,所以我们是可以直接将数组变成一层来看待,依次覆盖上一层的数值,一直到最后一层为止。

所以我们将其转变成一个一维数组bag[j],对于循环我们做一点点小调整,将 j 从n开始,依次减1到v,这样就只需变化大于等于的情况了,这里 j 不可以从v递增到n,因为在bag[i-1][j-t[i]] + v[i]的时候前面的价值会影响后面的价值的,现在我们是一维数组,不像二维数组一样分开记录,故从v到n进行计算是有问题的,而从n到v递减是没错的,bag[i][j]会依次将原来bag[i-1][j]覆盖掉。

那么我们就来看看代码吧!

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
	int n, m;
	cin >> n >> m;
	vector<int> bag(n + 1, 0);//n代表总共能够用来采药的时间,m代表山洞里的草药的数目。
	int t, v;
	for (int i = 1; i <= m; i++) {
		cin >> t >> v;
		for (int j = n; j >= t; j--) {
			bag[j] = max(bag[j], bag[j - t] + v);
		}
	}
	cout << bag[n];
	return 0;
}

最后总结一下,在递推中,如果计算顺序比较特殊,而且计算新状态所用到的原状态不多,可以尝试使用滚动数组来减少内存开销。

当然滚动数组也有不足之处,就是打印很难,只能打印最后一个阶段的状态。所以在需要打印方案的时候,滚动数组要慎用。

大概就说这些,01背包问题有很多变化,需要大家慢慢琢磨,之后我也会发相关的题解,把它们整合在一起供大家理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值