题解 P1270 【“访问”美术馆】
0.说在前面
仔细检查了一下自己的做法和前面的题解们都有一些或大或小的差异,决定详细地分享给大家。
如果是因为莫名WA掉了某些点而点进来的,那么请检查一下
- 偷了画之后必须从入口逃走,所以每条走廊必须走两次
- 要在警察来之前逃走,因此离开的时间必须严格小于警察来的时间
1.思路分析 和 代码实现
每条走廊分叉为两条走廊? ——树形结构
在规定的时间(价值)内拿走多少画(物品)? ——背包
一道非常好的树形背包上手题。当然也可以用爆搜实现,但是其复杂度更高,并不推荐用在这里。
Part 1 .读入
很新颖的读入方式,先序遍历读入。我们可以使用指针,每条走廊开两个指针分别指向儿子们,但这样略显麻烦。
更好的方法是像线段树一样,把二叉树改为线性存储,节点x<<1和x<<1|1。此方法的优点在于,无需再存储儿子编号,而且时间效率极高。为避免下标越界记得开两倍空间。
另外,刚刚也已经提到每条走廊必须走两次,我们不妨在读入的时候就直接把时间乘2,省去后顾之忧。
void build(int x){
scanf("%d%d",&t[x],&a[x]);
t[x]<<=1; //非常重要,每条走廊必须走两次
if(!a[x])build(x<<1),build(x<<1|1); //如果尽头没有画,读左右儿子走廊
}
Part 2. 背包
节点分为两类,一类是没画的走廊,一类是有画的展厅。不妨设
在以下的代码中,tim 指到达m指警察到的时间减1(因为你只能偷这么久)
我们先讨论展厅。很显然每幅画只能偷一次,因此最多偷
for(int i=1;i<=a[x];++i){ //枚举偷1副到全部偷完
int now=tim+(i*5);
if(now>m)break; //你必须在警察来之前逃走
f[x][now]=f[x][now-5]+1;
}
我们再讨论更复杂的走廊。由于先偷左儿子和先偷右儿子没有本质区别,所以我们一律视为先偷左儿子。假设此人花了
int l=x<<1,r=x<<1|1;
dfs(l,tim+t[l]); //先递归儿子,再回溯
dfs(r,tim+t[r]);
for(int i=0;i<=m-tim;++i) //左儿子偷i秒
for(int j=0;tim+i+j<=m;++j) //右儿子偷j秒
tomax(f[x][tim+i+j],f[l][tim+i]+f[r][tim+j]);
最后的答案应该是
2.完整代码
#include<cstdio>
int t[405],a[405],f[405][605],ans,m;
void build(int x){ //读入
scanf("%d%d",&t[x],&a[x]);
t[x]<<=1;
if(!a[x])build(x<<1),build(x<<1|1);
}
inline void tomax(int &a,int b){if(a<b)a=b;}
void dfs(int x,int tim){ //树形DP主体
if(!a[x]){
int l=x<<1,r=x<<1|1;
dfs(l,tim+t[l]);
dfs(r,tim+t[r]);
for(int i=0;i<=m-tim;++i)
for(int j=0;tim+i+j<=m;++j)
tomax(f[x][tim+i+j],f[l][tim+i]+f[r][tim+j]);
}else{
for(int i=1;i<=a[x];++i){
int now=tim+(i*5);
if(now>m)break;
f[x][now]=f[x][now-5]+1;
}
}
}
int main(){
scanf("%d",&m);--m; //读入与预处理
build(1);
dfs(1,t[1]);
printf("%d",f[1][m]);
return 0;
}
3.写在后面
做完此题可以继续尝试很类似的 P3360 偷天换日
DP不易,多加练习