题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 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
说明/提示
【数据范围】
- 对于 30% 的数据,M≤10;
- 对于全部的数据,M≤100。
【题目来源】
NOIP 2005 普及组第三题
一、做题方法
这道题一看就是一道01背包的问题。我们可以先看看为什么要用01背包。(如果知道的话可以跳过)
1.枚举法
我们用0和1来表示一株草药采或不采(0为不采,1为采)。我们枚举所有的串,对每个串,我们看看它的总时间是不是小于等于T,再从所有总时间小于等于T的方案中,选出总价值最大的就可以啦。
But,太慢了!
1 .贪心?
可能有些人在刚刚看到这道题的时候第一反应觉得这是道贪心题,但是细一看就能找到反例。比如,采药时间是100,有三株草药,时间分别是51,50,50,价值分别是55,50,50。用贪心思想,第1株草药性价比高,先采第1株。可是一旦选了第1株草药,时间就只剩下了45,不够采第2、3株草药了。如果不采第1株,选看起来不如第1株草药的第2、3株,总时间正好100,这时总价值100,比55多多了。贪心策略无效。
2.动态规划
普通写法
如果我们现在有了两种采法,总时间一样,那是不是我们只要价值更大的那组?
那现在我们考虑采前 i 株草药采或不采,如果有两种采法总时间一样,我们就保留价值更大的那组。
把考虑了前 i 株草药以后时间为0,1,2……,m时的最大收益都记下来。
现在,考虑了前 i 株草药,总时间为 j 时,有两种情况:
①第 i 株草药没采,问题变成考虑了前 i 株草药,总时间为 j 时的情况。
②第 i 株草药采了,问题变成考虑了前 i 株草药,总时间为 j-w[i] 时的情况。
思路有了,我们看看它满不满足动态规划的要求:
最优子结构:要计算考虑了前 i 株草药,总时间为 j 时的最大收益,我们可以先计算考虑了前 i-1 株草药,总时间为 j 时的最大收益与考虑了前 i-1 株草药,总时间为 j-w[i] 时的最大收益。
无后效性:我们只关注考虑了前 i 株草药,总时间为 j 时的最大收益,不关注哪些草药采了,哪些草药没采。
状态:f[i][j] 表示考虑了前 i 株草药,总时间为 j 时的最大收益。
转移:f[i][j] = max( f[i-1][j] , f[i-1][j-w[i]] + v[i] ) ,分别对应了第 i 株草药没采和采了的两种情况。
没问题啊,那么我们就可以列出状态转移方程了。
判断 j 是否小于 wi 是因为如果时间不够了,就不能采这株草药。
代码:
#include<bits/stdc++.h>
using namespace std;
int v[1010],w[1010];
int f[1010][1010];
int main()
{
int t,m;
cin>>t>>m;
for(int i=1;i<=m;i++)
{
cin>>w[i]>>v[i];
}
for(int i=1;i<=m;i++)
{
for(int j=0;j<=t;j++)
{
if(j<w[i])
{
f[i][j]=f[i-1][j];
}
else
{
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
}
}
}
cout<<f[m][t];
return 0;
}
空间优化
第一种方法
我们再考虑一下,到底需不需要开这么大的数组?
其实,考虑了前 i 株草药时的状态,只和考虑了前 i-1 株草药时的状态相关,前面的 i-2 行完全不用记的!
那要怎么办呢?
我们可以用 f 数组和 g 数组分别记录第 i-1 株和第 i 株的情况,通过第 i-1 株的情况,去计算第 i 株的情况,最后把 g 数组赋值给 f 数组。
代码:
#include<bits/stdc++.h>
using namespace std;
int v[1010],w[1010];
int f[1010],g[1010];
int main()
{
int t,m;
cin>>t>>m;
for(int i=1;i<=m;i++)
{
cin>>w[i]>>v[i];
}
for(int i=1;i<=m;i++)
{
for(int j=0;j<=t;j++)
{
if(j<w[i])
{
g[j]=f[j];
}
else
{
g[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
memcpy(f,g,sizeof(g));
}
cout<<f[t];
return 0;
}
第二种方法
如果我们继续优化,把两个一维数组改成一个一维数组,可以吗?我们直接用 f[j] 来表示情况,也就是把 g[j] 改成 f[j] 。
我们来试一下:
#include<bits/stdc++.h>
using namespace std;
int v[1010],w[1010];
int f[1010];
int main()
{
int t,m;
cin>>t>>m;
for(int i=1;i<=m;i++)
{
cin>>w[i]>>v[i];
}
for(int i=1;i<=m;i++)
{
for(int j=0;j<=t;j++)
{
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
cout<<f[t];
return 0;
}
输出240。???
为什么呢?不知道大家注意没注意第2层循环的顺序,我们从 w[i] 更新到 t ,那么我们在更新 f[j] 时, f[j-w[i]] 已经更新过了,它并不是原来的 f[j-w[i]] ,可能 f[j-w[i]] 已经采了第 j 种草药,然后我们更新 f[j] 时调用 f[j-w[i]] ,又采了一次这种草药。这已经不符合01背包的要求了。
所以为了防止这种事情发生,我们第2层循环可以反过来遍历,这样就解决了问题。
代码:
#include<bits/stdc++.h>
using namespace std;
int v[1010],w[1010];
int f[1010];
int main()
{
int t,m;
cin>>t>>m;
for(int i=1;i<=m;i++)
{
cin>>w[i]>>v[i];
}
for(int i=1;i<=m;i++)
{
for(int j=t;j>=0;j--)
{
if(j>=w[i])
{
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
}
cout<<f[t];
return 0;
}
常数优化
优化
刚刚的代码判断了 j 是否大于等于 w[i] ,再进行dp。但是我们发现对于j<w[i]的情况,我们完全不需要遍历。可以直接从t到w[i]。
for(int i=1;i<=m;i++)
{
for(int j=t;j>=w[i];j--)
{
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
二、AC代码
#include<bits/stdc++.h>
using namespace std;
int v[1010],w[1010],f[1010];
int main() {
int t,m;
cin>>t>>m;
for(int i=1; i<=m; i++)
{
cin>>w[i]>>v[i];
}
for(int i=1; i<=m; i++)
{
for(int j=t; j>=w[i]; j--)
{
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
cout<<f[t];
return 0;
}