P5384 [Cnoi2019] 雪松果树 题解

传送门

前言

一年一度,生长在高山上的雪松果树又结果了。
第二天,雪松果树长成了一颗参天大树, 上面长满了雪松果。

求雪松果树生长周期

整活向题解。

奋力卡常 3h,纪念一下。

是的,我一个人的提交占了三页。

题意简述

给一棵树,查询某节点的 \(k\)-cousin。

题解

基本思路

虽然有很多更优的做法,但是我们考虑线段树合并。

在每个节点建一棵权值线段树,维护子树内每个深度的节点个数。

把询问离线下来,对整棵树进行 dfs,将每棵子树的线段树合并到该节点的线段树,之后处理该节点的询问。

这里需要将询问转化为求原询问节点的 \(k\)-father 的 \(k\)-son 数量减一。

求 \(k\)-father 可以使用倍增。

于是我们可以写出如下代码:

cpp 复制代码
#include <cstdio>
#include <vector>
#define N 1000005
int n,q,ans[N],fa[N][21],rt[N];
int hed[N],tal[N],nxt[N],cnte;
void adde(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
struct query {int id,k;};
std::vector<query> a[N];
struct sgt
{
	#define mid (lb+rb>>1)
	#define pushup(x) d[x]=d[ls[x]]+d[rs[x]]
	int d[N<<5],ls[N<<5],rs[N<<5],idx;
	void modify(int &x,int t,int lb,int rb)
	{
		if(!x) x=++idx;
		d[x]++;
		if(lb==rb) return;
		if(t<=mid) modify(ls[x],t,lb,mid);
		else modify(rs[x],t,mid+1,rb);
	}
	int query(int x,int t,int lb,int rb)
	{
		if(!x) return 0;
		if(lb==rb) return d[x];
		if(t<=mid) return query(ls[x],t,lb,mid);
		else return query(rs[x],t,mid+1,rb);
	}
	int merge(int x,int y,int lb,int rb)
	{
		if(!x||!y) return x+y;
		if(lb==rb) {d[x]+=d[y];return x;}
		ls[x]=merge(ls[x],ls[y],lb,mid);
		rs[x]=merge(rs[x],rs[y],mid+1,rb);
		pushup(x);return x;
	}
	#undef mid
	#undef pushup
} tr;
void dfs(int x,int f,int dep)
{
	tr.modify(rt[x],dep,1,n);
	for(int i=hed[x];i;i=nxt[i]) if(tal[i]!=f)
		dfs(tal[i],x,dep+1),rt[x]=tr.merge(rt[x],rt[tal[i]],1,n);
	for(int i=0;i<a[x].size();i++)
		ans[a[x][i].id]=tr.query(rt[x],dep+a[x][i].k,1,n)-1;
}
main()
{
	scanf("%d%d",&n,&q);
	for(int i=2;i<=n;i++) scanf("%d",&fa[i][0]),adde(fa[i][0],i);
	for(int i=1;i<=20;i++) for(int j=2;j<=n;j++)
		fa[j][i]=fa[fa[j][i-1]][i-1];
	for(int i=1;i<=q;i++)
	{
		int u,k;
		scanf("%d%d",&u,&k);
		int d=0;
		for(int j=20;j>=0;j--)
			if(d+(1<<j)<=k) u=fa[u][j],d+=1<<j;
		if(u) a[u].push_back({i,k});
	}
	dfs(1,0,1);
	for(int i=1;i<=q;i++) printf("%d ",ans[i]);
}

像这样。

优化

如果你按照我们刚才的思路写出了如上代码,那么恭喜你,你可以获得 \(40\) 分的好成绩。

于是我们需要优化。

由于剩下的点都 MLE 了,所以需要优化空间。

首先,注意到线段树的数组开得很大,这是因为每个节点都有一棵线段树。然而我们线段树合并统计答案时,当一个节点的答案被合并到父节点时,这个节点的线段树就再也用不上了。所以我们回收这棵线段树上的节点。这样,线段树的空间只需开到 \(4e6\)。

于是得到这样一棵线段树:

cpp 复制代码
struct sgt
{
	#define mid (lb+rb>>1)
	int d[N<<2],ls[N<<2],rs[N<<2],idx;
	int st[M],tp;							//回收
	void modify(int &x,int t,int lb,int rb)
	{
		if(!x) x=tp?st[tp--]:++idx;			//使用先前回收的空间
		d[x]++;
		if(lb==rb) return;
		if(t<=mid) modify(ls[x],t,lb,mid);
		else modify(rs[x],t,mid+1,rb);
	}
	int query(int x,int t,int lb,int rb)
	{
		if(!x) return 0;
		if(lb==rb) return d[x];
		if(t<=mid) return query(ls[x],t,lb,mid);
		return query(rs[x],t,mid+1,rb);
	}
	int merge(int x,int y,int lb,int rb)
	{
		if(!x||!y) return x|y;
		d[x]+=d[y];
		if(lb<rb)
		ls[x]=merge(ls[x],ls[y],lb,mid),
		rs[x]=merge(rs[x],rs[y],mid+1,rb);
		d[y]=ls[y]=rs[y]=0,st[++tp]=y;		//回收空间
		return x;
	}
	#undef mid
} tr;

其次,我们开了 \(1e6\) 个 vector 来存储询问,这样消耗的空间是无法接受的,所以需要改变询问的存储方式。

可以使用一种链式前向星式的做法:

cpp 复制代码
int qh[N];		//链头
int qnxt[N];	//下一个询问的编号
struct query
{
	int v,k;	//v:u的k-father
} a[N];
void addq(int x)
{
	qnxt[x]=qh[a[x].v];
	qh[a[x].v]=x;
}

我们看到用来求 k-father 的倍增数组也占了很大的空间,于是改用树剖。

cpp 复制代码
int dep[N],son[N],siz[N],top[N],dfn[N],li[N],id;
void dfs1(int x)						//正常的树剖
{
	siz[x]=1,dep[x]=dep[fa[x]]+1;
	for(int i=hed[x];i;i=nxt[i])
		if(!siz[tal[i]])
		{
			dfs1(tal[i]);
			siz[x]+=siz[tal[i]];
			if(siz[tal[i]]>siz[son[x]])
				son[x]=tal[i];
		}
}
void dfs2(int x,int tp)					//正常的数剖
{
	li[dfn[x]=++id]=x,top[x]=tp;
	if(!son[x]) return;
	dfs2(son[x],tp);
	for(int i=hed[x];i;i=nxt[i])
		if(!top[tal[i]])
			dfs2(tal[i],tal[i]);
}
int anc(int u,int k)
{
	int v=u;
	while(v&&dep[u]-dep[top[v]]<k)
		v=fa[top[v]];
	if(!v) return 0;					//没有k-father
	/*
	例:u=8,k=4,dep[8]=7
	跳到了重链1-6
	1-2-3-4-5-6
	^   ^     v
	top kfa
	
	dep[3]=dep[u]-k
	dfn[1]+dep[3]-dep[1]=dfn[3]
	li[dfn[3]]=3
	*/
	return li[dfn[top[v]]+dep[u]-k-dep[top[v]]];
}

像这样。

终极优化

如果你使用如上方式优化,那么恭喜你,可以不再 MLE 并获得 \(76\) 分至 \(92\) 分不等的好成绩。
当然,如果你的写法常数更小卡过去了也行。

为什么不能 AC 呢?

让我们看看最初的代码中调用线段树的部分:

cpp 复制代码
rt[x]=tr.merge(rt[x],rt[tal[i]],1,n);

线段树的值域是 \(n\)。

然而,我们的线段树维护的是深度。

所以值域应该为深度的最大值。

代码

cpp 复制代码
#include <cstdio>
#define N 1000002

int n,q,md,ans[N],fa[N],rt[N];
int hed[N],tal[N],nxt[N],cnte;
void adde(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
int qh[N],qnxt[N];
int av[N],ak[N];
void addq(int x) {qnxt[x]=qh[av[x]],qh[av[x]]=x;}
int dep[N],son[N],siz[N],top[N],dfn[N],li[N],id;
void dfs1(int x)
{
	siz[x]=1,dep[x]=dep[fa[x]]+1;
	if(dep[x]>md) md=dep[x];
	for(int i=hed[x];i;i=nxt[i]) if(!siz[tal[i]])
	{
		dfs1(tal[i]),siz[x]+=siz[tal[i]];
		if(siz[tal[i]]>siz[son[x]]) son[x]=tal[i];
	}
}
void dfs2(int x,int tp)
{
	li[dfn[x]=++id]=x,top[x]=tp;
	if(!son[x]) return;
	dfs2(son[x],tp);
	for(int i=hed[x];i;i=nxt[i]) if(!top[tal[i]]) dfs2(tal[i],tal[i]);
}
int anc(int u,int k)
{
	int v=u;
	while(v&&dep[u]-dep[top[v]]<k) v=fa[top[v]];
	if(!v) return 0;
	return li[dfn[top[v]]+dep[u]-k-dep[top[v]]];
}
struct sgt
{
	#define mid (lb+rb>>1)
	int d[N<<2],ls[N<<2],rs[N<<2],idx;
	int st[N<<2],tp;
	void modify(int &x,int t,int lb,int rb)
	{
		if(!x) x=tp?st[tp--]:++idx;
		d[x]++;
		if(lb==rb) return;
		if(t<=mid) modify(ls[x],t,lb,mid);
		else modify(rs[x],t,mid+1,rb);
	}
	int query(int x,int t,int lb,int rb)
	{
		if(!x) return 0;
		if(lb==rb) return d[x];
		if(t<=mid) return query(ls[x],t,lb,mid);
		return query(rs[x],t,mid+1,rb);
	}
	int merge(int x,int y,int lb,int rb)
	{
		if(!x||!y) return x|y;
		d[x]+=d[y];
		if(lb<rb)
		ls[x]=merge(ls[x],ls[y],lb,mid),
		rs[x]=merge(rs[x],rs[y],mid+1,rb);
		d[y]=ls[y]=rs[y]=0,st[++tp]=y;
		return x;
	}
	#undef mid
} tr;
void dfs3(int x)
{
	tr.modify(rt[x],dep[x],1,md);
	for(int i=hed[x];i;i=nxt[i]) if(tal[i]^fa[x])
		dfs3(tal[i]),rt[x]=tr.merge(rt[x],rt[tal[i]],1,md);
	for(int i=qh[x];i;i=qnxt[i])
		ans[i]=tr.query(rt[x],dep[x]+ak[i],1,md)-1;
}
main()
{
	scanf("%d%d",&n,&q);
	for(int i=2;i<=n;i++) scanf("%d",&fa[i]),adde(fa[i],i);
	dfs1(1),dfs2(1,1);
	for(int i=1;i<=q;i++)
	{
		int u,k;
		scanf("%d%d",&u,&k);
		av[i]=anc(u,k),ak[i]=k,addq(i);
	}
	dfs3(1);
	for(int i=1;i<=q;i++) printf("%d ",ans[i]);
}

\[\Huge End \]