提米树/【BZOJ2031】剪枝-DP
【问题描述】
Temmie 村里有很多提米树,上面长满了 Temmie 薄片。一天,Flowey 来到
Temmie 村做客,Temmie 们想要送给它一棵提米树。
对于这棵提米树,满足这样的性质:
·1.这棵树可以看做一棵 n 个点的无向无环图,每个点上长了一定数量的
Temmie 薄片,薄片数量记为这个点的权值,这些点被标记为 1 到 n 的整数,其
中 1 号点是树的根,没有孩子的点是树上的叶子。
·2.对于这棵树进行深度优先搜索,每个点从左到右按顺序遍历每个孩子,得
到这棵树的 DFS 序。
·3.定义(a,b)是一对相邻的叶子,当且仅当没有其它的叶子节点在 DFS 序上在
a,b 之间。每对相邻的叶子都会产生一个代价,代价为 a 到 b 路径上(不包含 a,b)
的点中,最大点权值。
·4.提米树可以提供决心,一棵提米树能提供的决心的数量是树上所有叶子上
长的 Temmie 薄片数量和,减去所有相邻叶子的代价。
Flowey 虽然不是以 Temmie 薄片为食的生物,但是它需要决心。这棵提米树
能提供的决心太少了,Temmie 们决定对这棵树进行若干次剪枝(可以不剪枝),使
得这棵树能提供的决心最多。
一次剪枝定义为:如果一个点的孩子都是叶子,就可以把它所有的孩子剪掉。
【输入格式】
从 temmie.in 中读入数据。
输入数据的第一行为一个正整数 n,代表提米树上点的数量。
接下来 n 行,对于每行:
第一个数为一个正整数,代表这个点上 Temmie 薄片的数量
第二个数为一个整数 num,代表这个点孩子的数量
接下来 num 个数,依次表示这个点从左到右的孩子。
【输出格式】
输出到 temmie.out 中
输出包含一个数,为这棵树剪枝后最多能提供的决心的数量。
【样例输入 1】
3
1 2 2 3
2 0
2 0
【样例输出 1】
3
【样例输入 2】
5
1 2 2 3
2 1 4
3 1 5
4 0
5 0
【样例输出 2】
6
思路
- 关于这道题的两个关键性质/?:
- 1.注意到这里求的都是每两个相邻叶子节点路径上的极值,用倍增求复杂度是nlogn,但用暴力dfs求刚好遍历整棵树复杂度O(n);
- 2.无论如何剪枝,两个剪枝前不相邻的叶子都不会在剪枝后相邻【关键!!!】
- dp[i]表示到i节点的答案,初步写出dp方程:dp[x]=dp[y]-max(h[x],H[y])+num[x],其中h[x]为x至lca的最大值,H[y]同理
- 发现此dp方程枚举每一对h[x],H[y],时间复杂度为n^2(不是/然而并不会算...)超时100%,想到优化时间
- 想到前缀优化,与前缀和类似,前缀最大值(然而并不能想到)
- 又应为前缀最大值后h数组单调递增,想到双指针[套路]将复杂度降到O(n)
- 思路看起来很复杂,但实现稍简单,具体注释见代码
代码
#include <iostream>
#include <cstdio>
#define inf 0x7f7f7f7f
#define maxn 100005
using namespace std;
int n,fa[maxn],head[maxn],cnt,keep[maxn],deep[maxn],w[maxn],e[maxn],h[maxn],H,num[maxn],dp[maxn],g[maxn],tmax;
struct fdfdfd{int next,to;}a[maxn];
void addedge(int x,int y){a[++cnt].to=y; a[cnt].next=head[x]; head[x]=cnt;}
int max(int a,int b){return a>b?a:b;}
void dfs(int x)
{
deep[x]=deep[fa[x]]+1;
if(!head[x]) keep[++cnt]=x;
else for(int i=head[x];i;i=a[i].next) dfs(a[i].to);
}
int main()
{
scanf("%d",&n);
for(int i=1,tnum;i<=n;++i)
{
scanf("%d%d",&num[i],&tnum);
for(int j=1,ch;j<=tnum;++j) scanf("%d",&ch),addedge(i,ch),fa[ch]=i;
}
cnt=0; dfs(1);//一遍dfs存储树的所有叶子节点
int x=keep[1],y;
while(x) dp[x]=num[x],x=fa[x];//初始化第一条链的值,dp[x]=num[x];
for(int i=2;i<=cnt;++i)
{
w[0]=e[0]=0; x=keep[i]; y=keep[i-1];//w,e暴力爬树分别记录想x,y到lca路径上的点
while(deep[x]>deep[y]) w[++w[0]]=x,x=fa[x];
while(deep[y]>deep[x]) e[++e[0]]=y,y=fa[y];
while(x!=y)
{
w[++w[0]]=x,x=fa[x];
e[++e[0]]=y,y=fa[y];
}//注意从w[0]~1记录从lca到叶节点,e同理
g[0]=h[e[0]+1]=H=tmax=-inf; int r=e[0];
for(int j=e[0];j;--j) h[j]=max(h[j+1],num[fa[e[j]]]);//前缀最大值[补充1]
for(int j=1;j<=e[0];++j) g[j]=max(g[j-1],dp[e[j]]-h[j]);//此处g为for中dp转移的一部分,不懂先跳过
for(int j=w[0];j;--j)
{
H=max(H,num[fa[w[j]]]);//省去H数组,每次直接求
while(r&&H>h[r]) tmax=max(tmax,dp[e[r--]]);//找到e上最后一个小于H的节点
dp[w[j]]=max(tmax-H,g[r])+num[w[j]];//[补充2]
}
}
x=keep[cnt]; int ans=-inf;
while(x) ans=max(dp[x],ans),x=fa[x];
printf("%d\n",ans);
return 0;
}
//[补充1]:在阅读某些题解代码时不理解为什么时fa[e[j]],读题后发现“代价为 a 到 b 路径上(不包含 a,b)”。。。
//[补充2]:max中,tmax-H 是h数组<H时的 dp[y]-max(h[x],H[y]) 最大值,g[r]则是h数组>H时的最大值,
//也就同时解释为什么要提前求出g数组,而不能直接用dp[e[r]]-h[r]
结
- 大概这道题主要的关键点在于性质2,
虽然就算知道这条性质,代码的细节仍然多得令人发指,考试多半只能拿暴力