题解 P1616 【疯狂的采药】

Mutsumi_0114

2018-01-26 13:09:00

Solution

来做这题的人应该都是看过 [【P1048】采药](https://www.luogu.org/problemnew/show/P1048) 的,在 <采药> 中,每种草药只允许 采一次 。 以下一大段为<采药>思路回顾,可适当跳过 ------------ 我们将采 第$i$种草药 所需的 时间 设为 $t[i]$ , 价值 设为 $p[i]$ 。 如果有一个数组 $f[i][j]$ 来表示 从前往后 到第$i$种草药 (当然前面可能有草药不采) ,花费了 最多 $j$时间 (意思是可能花费了少于$j$时间) 时,能采到草药的 最大 价值,那它的状态转移方程是怎样的呢? 到第$i$种草药时有两种情况:采与不采。 若不采这种草药,则 时间花费没有增多 ,经过的 草药种数增加了$1$ , 采到草药价格不变 ,所以 $$f[i][j]=f[i-1][j]$$ 若采这种草药,则 时间花费增加了$t[i]$ , 种数增加$1$ , 采到草药价格增加了$p[i]$ ,所以 $$f[i][j]=f[i-1][j-t[i]]+p[i]$$ 我们当然要使$f[i][j]$ 尽可能大 ,即有 $$f[i][j]=max(f[i-1][j],f[i-1][j-t[i]]+p[i])$$ ``` ----------- |C| | | |B| ----------- | | | | |A| ----------- ``` 通过状态转移方程可以发现若 要求上图$f$数组中$A$的值,必须先求出$A$上方的$B$,以及$A$上方左边某处的$C$ 。 所以可确定递推顺序为 从左到右,从上到下 <采药> $C++$ 代码: ```cpp #include <cstdio> #include <algorithm> using namespace std; int i,j,T,M,t[101],p[101],f[101][1001]; int main() { scanf("%d%d",&T,&M); for(i=1;i<=M;i++) scanf("%d%d",&t[i],&p[i]); for(i=1;i<=M;i++) { for(j=0;j<t[i];j++) f[i][j]=f[i-1][j];//针对上面的左边为数组越界的位置 for(j=t[i];j<=T;j++) f[i][j]=max(f[i-1][j],f[i-1][j-t[i]]+p[i]);//递推 } printf("%d",f[M][T]); return 0; } ``` 这显然是一个 二维 的$dp$,其实我们可以将它 压缩至一维 。 若要压缩至一维,我们可以 将药的种类$i$省略 ,只开一个一维数组, 用后面算的值覆盖掉前面算的值 。 已经说过在二维$dp$中计算$A$所需要的数, 转到一维后,其实就是: ``` ----------- |C| | | |A| ----------- ``` 计算$A$的值需要上一轮计算的(还未更新的)$A$和$C$的值。 便可以确定推进顺序为 从右到左 $Code$: ```cpp #include <cstdio> #include <algorithm> using namespace std; int i,j,T,M,t[101],p[101],f[1001]; int main() { scanf("%d%d",&T,&M); for(i=1;i<=M;i++) scanf("%d%d",&t[i],&p[i]); for(i=1;i<=M;i++) for(j=T;j>=t[i];j--)//从右到左计算 f[j]=max(f[j],f[j-t[i]]+p[i]);//递推 //当需要的左边的值为数组越界时,值不变 printf("%d",f[T]); return 0; } ``` ------------ 现在我们再来看看这题 <疯狂的采药> (终于进入正题): 其实题目其他地方都没变,只是同一种草药可以反复采摘,而且数据范围增大,如此一来我们无法使用01背包的思路 可以反复采摘同种草药只对我们之前的状态转移方程有一个影响:当 第$i$种草药采了$n$次时,草药种类增加$1$,时间增加$n\times t[i]$,价值增加$n\times p[i]$ ,所以 $$f[i][j]=f[i-1][j-n\times t[i]]+n\times p[i]$$ 整理: $$f[i][j]=max(f[i-1][j],f[i-1][j-n\times t[i]]+n\times p[i])$$ 但这样一来时间复杂度就变成了$O(M\times T^T)$,面对$M\le10000,T\le100000$的数据量肯定是要炸的. ``` --------------- |F| |E| |D| |C| --------------- | | | | |B| |A| --------------- ``` 于是将状态转移方程化为上图我们可以注意到: 要求$A$的值需要$C,D,E,F$的值,而求$B$的值需要$D,E,F$的值 ,于是,合并一下,可以得出求$A$的值就需要$B,C$的值,即: $$f[i][j]=max(f[i-1][j],f[i][j-t[i]]+p[i])$$ 又观察可得递推顺序为 从左到右,从上到下 $C++$ 初始代码: ```cpp #include <cstdio> #include <algorithm> using namespace std; int i,j,T,M,t[10001],p[10001],f[10001][10000001]; int main() { scanf("%d%d",&T,&M); for(i=1;i<=M;i++) scanf("%d%d",&t[i],&p[i]); for(i=1;i<=M;i++) { //从左到右 for(j=0;j<t[i];j++) f[i][j]=f[i-1][j];//针对左边为数组越界的状态 for(j=t[i];j<=T;j++) f[i][j]=max(f[i-1][j],f[i][j-t[i]]+p[i]);//递推 } printf("%d",f[M][T]); return 0; } ``` 其中f[10001][10000001]超过题目所要求的空间上限,所以它也需要压缩 道理一样的: $$f[j]=max(f[j],f[j-t[i]]+p[i])$$ 注意到 $f[j-t[i]]$需要的是这种草药更新后的状态,所以递推顺序应为 从左到右 这和“采药”是不一样的。 另外,题目并没有说明最终结果的范围,需要开longlong以保证数据不溢出 本题 <疯狂的采药> $AC\;C++$ 代码: ```cpp #include <cstdio> #include <algorithm> using namespace std; int i,j,T,M; long long t[10001],p[10001],f[10000001]; int main() { scanf("%d%d",&T,&M); for(i=1;i<=M;i++) scanf("%lld%lld",&t[i],&p[i]); for(i=1;i<=M;i++) for(j=t[i];j<=T;j++)//当需要的左边的值为数组越界时,值不变 f[j]=max(f[j],f[j-t[i]]+p[i]);//递推 printf("%lld",f[T]); return 0; } ``` 那么,本篇题解到此结束 第一次编辑:$20180126$ 初稿 第二次编辑:$20180816$ 在文章中加入$Markdown$和$Latex$ 第三次编辑:$20180818$ 简化部分代码 第四次编辑:$20181002$ 修改少数错误 第五次编辑:$20190227$ 移除$maxf$的修改 第六次编辑:$20190508$ 移除数组初始化 第七次编辑:$20190607$ 还有人觉得看不见递推式? 第八次编辑:$20200727$ 根据反馈,文中对于数组的图例有会影响理解的错误,已更正 第九次编辑:$20200926$ 数据更新