P1616 疯狂的采药 题解
Dworry
·
·
个人记录
刚学dp不久的小蒟蒻前来报到
看到题解里很多dalao都用了压维来节省空间。作为一个出生牛犊不怕虎的萌新(其实是不会压),一次又一次地尝试如何开二维数组不 MLE,最后终于成功了!!!(不会吧,不会吧,不会真有人做出一道橙题就开心到来发题解吧。)
在做题中,我拿过80分、90分、直到最后AC。接下来,我也会按照80分代码、90分代码、AC代码的顺序进行讲解。
一、80分代码:部分背包问题
当我第一次看到题目的时候:每种药可以无限次采摘。诶!这不就是部分背包嘛(这时候我还没有意识到问题的严重性),贪心算法走起!于是乎有了以下代码:
#include<bits/stdc++.h>
using namespace std;
struct node{
int value;
int cost;
double cp;
} drug[10003];
bool cmp(node x, node y){
return x.cp>y.cp;
}
int main(){
int n,t;
cin>>t>>n;
for(int i=1;i<=n;i++){
cin>>drug[i].cost>>drug[i].value;
drug[i].cp=(double)drug[i].value/drug[i].cost;
}
sort(drug+1,drug+n+1,cmp);
long long ans=0;
for(int i=1;i<=n;i++){
int get=t/drug[i].cost;
t-=(drug[i].cost*get);
ans+=(get*drug[i].value);
}
cout<<ans;
return 0;
}
(关于部分背包问题,不是本题的重点,而且我懒,想少打几个字,可以传送 P2240
。里面有部分背包问题的详细题解,在此不展开讲解。)
提交评测:80分。。。。。。
这时,我还是有点懵。于是我点开了算法标签,恍然大悟——原来这是一道 dp 题啊!
二、90分代码:01背包问题的演变
相信来做这题的小伙伴都是从 P1048 采药问题中过来的,相信大家一定很熟悉 dp 了吧,在采药问题中,每种草药只有选和不选两种情况,很容易能够得出状态转移方程如下:(做过 P1048 的小伙伴应该再熟悉不过了)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-cost[i]]+value[i]);
在 01 背包问题中,我们面临的问题是选还是不选;而到了现在的问题中,我们面临的问题是选多少(如果 0 也算选的话/w)。首先还是跟 01 问题一样,先选择一株草药,之后枚举 [0,t] 的时间来更新最优状态。但是,由于草药可以多次采集,因而前一状态不再是第 i-1 株草药,而是要从第 i 株草药开始。因此,状态转移方程应修正为:
同样,如果枚举到 $j<cost[i]$ 的话,说明当前的剩余时间不足够我们采集第 $i$ 株草药,因此我们不考虑它,有:
$dp[i][j]=dp[i-1][j]$. (这与01问题是一样的qwq.)
我们终于推出了状态转移方程!dp 中最重要的一步工作完成了!
然鹅,看了一眼数据范围:$m≤10^4, t≤10^7$......
如果直接声明二维 dp 数组的话无疑是要 MLE 的!(又因为要用 long long 声明,则需要 $8×10^{11}$B=$745.06$G 的内存!~~好家伙,硬盘给你当内存都不够用。~~)
怎么办呢?~~压维呗,但是我不会呀。。。~~注意到另一个条件: $m×t≤10^7$. 有了!那就先输入 $n$ 和 $t$ ,然后再用这两个变量声明dp数组就行了!
**注意:教科书中说不能用这种方式声明数组。经实测,该方法只有在较高版本的 DEV C++中(我用的是 5.11 )才能编译通过。在 VS、VC6.0 中好像编译过不了,DEV C++ 4.9.2 也是。**
但是在洛谷OJ上编译能过,那就没问题了。 ^____^
附上代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
//map<int,int> dp;
int main(){
int n,t;
int cost[10001],value[10001];
cin>>t>>n;
long long dp[n+1][t+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++){
cin>>cost[i]>>value[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=t;j++){
if(cost[i]>j){
dp[i][j]=dp[i-1][j];
}
else {
dp[i][j]=max(dp[i-1][j],dp[i][j-cost[i]]+value[i]);
}
}
}
// for(int i=1;i<=n;i++){
// for(int j=1;j<=t;j++){
// printf("%d ",dp[i][j]);
// }
// printf("\n\n");
//// system("pause");
// }
cout<<dp[n][t];
return 0;
}
```
当我已经准备好满怀信心迎接AC时,在最后一个测试点 MLE 了!
冷静分析,代码中声明的 $dp[n+1][t+1]$ 出了问题。这么声明的话需要的空间为 $8×(n+1)×(t+1)$字节,即 $8nt+8n+8t+8$ 字节。在极端情况下,$nt=10^7$, 此时若使需要的空间最大(~~假装我是设计数据的人,哈哈哈哈哈~~),则要求 $n$ 和 $t$ 尽可能的大。$t$ 最大能取到 $10^7$. 代入式子中 (此时 $n=1$, 跟常数 8 一样可以忽略。)计算得需要的空间为:
$2*8*10^7$ B = $152.59$ MB......
(可恶,就差那么一点点。。。。。。估计最后一个点就是被这么卡没的)
------------
## 三、AC代码:从“零”开始 ##
~~萌新三连之三:那咋整啊。~~
怎么办呢?再仔细想想会发现,原来 dp 数组中下标为 $0$ 的单元都没被用上。
经过上面分析,当 $n=10^7$ 时 $dp[n][0]$ 的空间全部没有用上,高达$10^7$个 long long 大小的空间。
(~~好家伙,一半的空间都被吃了!!!~~)
那我用上它们不就行了吗?
越是极端的情况,浪费空间就越可耻!
怎么办呢?只需要让草药从$0$开始标号就行啦!
(如果让 $t$ 也从 $0$ 开始的话会出问题。而且 $t$ 在极端情况下也只会浪费 $10^4$ 个long long 类型的空间,问题不大!)
但是这样的话,当 $i=0$ 时 $i-1$ 会小于 $0$. 因此需要对这种情况做一个特判。
完整AC代码如下:
```cpp
#include<bits/stdc++.h>
using namespace std;
//map<int,int> dp;
int main(){
int n,t;
int cost[10001],value[10001];
cin>>t>>n;
long long dp[n][t+1];
memset(dp,0,sizeof(dp));
for(int i=0;i<n;i++){
cin>>cost[i]>>value[i];
}
for(int i=0;i<n;i++){
for(int j=1;j<=t;j++){
if(cost[i]>j){
if(i==0) dp[i][j]=0;
else dp[i][j]=dp[i-1][j];
}
else {
if(i==0) dp[i][j]=dp[i][j-cost[i]]+value[i];
else dp[i][j]=max(dp[i-1][j],dp[i][j-cost[i]]+value[i]);
}
}
}
// for(int i=1;i<=n;i++){
// for(int j=1;j<=t;j++){
// printf("%d ",dp[i][j]);
// }
// printf("\n\n");
//// system("pause");
// }
cout<<dp[n-1][t];
return 0;
}
```
最后终于AC了!太不容易了qwq
正如小标题一样,从“零”开始。做完了这一题,下一题又要从头开始找
思路了。不过无妨,反正最后总能享受到AC的快乐的!!(/w)