Loading

7376. 【2021.11.11NOIP提高组联考】超级加倍

Description

给定一棵树。

我们认为一条从 \(x \rightarrow y\) 的简单路径是好的,当且仅当路径上的点中编号最小的是 \(x\) ,最大的是 \(y\)

请求出好的简单路径条数。

\(n\le 2\times 10^6\)

Solution

编号最小和编号最大,是本题的关键。

考虑能不能构造出这样的树,使得新的树中任意两点 \((u,v)\)\(lca\) 就是原来的树中 \((u,v)\) 之间的最小/大值。

这就令人想起了 kruskal 重构树,似乎跟这个性质十分类似。

从大到小枚举编号( \(x\)),同时枚举该点连出去的边,若某条边连向的点(\(y\))已经被加入新树中,那么就将新树中 \(y\) 的祖先的父亲记作 \(x\)。这可以通过并查集来实现。

通过以上的操作,我们成功达成了我们的目的,现在想一下怎么用这个造出来的树。

注意到我们建了两棵树,那么就可以发现,若原树中 \((x,y)\) 是符合要求的,当且仅当在一棵树中 \(x\)\(y\) 的祖先,并且在另一棵树中,\(y\)\(x\) 的祖先。

统计答案呢,我们可以先求出一棵树的 dfs 序。然后遍历另一棵树,回溯某点的时候将他的子树全部打上标记。那么一个点的答案就是打了多少个标记。

注意到标记可能会由父亲那一边打过来,因此我们可以在遍历到这个点的时候记录值,回溯的时候再记录一次值,两次值之差才是真正的,由儿子产生的贡献。

打标记显然是可以通过数据结构来完成的,我一开始打了线段树,但被卡常了,后来调成了树状数组才把这题切掉。

Code

#include<cstdio>
#define N 2000005
#define ll long long
using namespace std;
struct node
{
	int to,next,head;
}a[N<<1],tree1[N<<1],tree2[N<<1];
int n,x,tot,num1,num2,dnum,f[N],dfn[N],size[N],c[N];
ll ans;
int lowbit(int x) {return x&(-x);}
void add(int x,int y)
{
	a[++tot].to=y;
	a[tot].next=a[x].head;
	a[x].head=tot;
}
void add1(int x,int y)
{
	tree1[++num1].to=y;
	tree1[num1].next=tree1[x].head;
	tree1[x].head=num1;
}
void add2(int x,int y)
{
	tree2[++num2].to=y;
	tree2[num2].next=tree2[x].head;
	tree2[x].head=num2;
}
int find(int x)
{
	if (f[x]!=x) f[x]=find(f[x]);
	return f[x];
}
void modify(int x,int v)
{
	for (int i=x;i<=n;i+=lowbit(i))
		c[i]+=v;	
}
ll query(int x)
{
	ll res=0;
	for(int i=x;i;i-=lowbit(i))
		res+=c[i];
	return res;
}
void dfs1(int now,int fa)
{
	dfn[now]=++dnum;
	size[now]=1;
	for (int i=tree1[now].head;i;i=tree1[i].next)
	{
		int v=tree1[i].to;
		if (v==fa) continue;
		dfs1(v,now);
		size[now]+=size[v];
	}
}
void dfs2(int now,int fa)
{
	ll sum1=query(dfn[now]);
	for (int i=tree2[now].head;i;i=tree2[i].next)
	{
		int v=tree2[i].to;
		if (v==fa) continue;
		dfs2(v,now);
	}
	ll sum2=query(dfn[now]);
	ans+=sum2-sum1;
	modify(dfn[now],1);modify(dfn[now]+size[now],-1);
}
int main()
{
	freopen("charity.in","r",stdin);
	freopen("charity.out","w",stdout);
	scanf("%d",&n);
	for (int i=1;i<=n;++i)
	{
		scanf("%d",&x);
		if (i!=1) add(x,i),add(i,x);
	}
	for (int i=1;i<=n;++i)
		f[i]=i;
	for (int i=n;i>=1;--i)
	{
		for (int j=a[i].head;j;j=a[j].next)
		{
			int v=a[j].to;
			if (v>i)
			{
				int xf=find(v),yf=find(i);
				f[xf]=yf;
				add1(yf,xf);add1(xf,yf);
			}
		}
	}
	for (int i=1;i<=n;++i)
		f[i]=i;
	for (int i=1;i<=n;++i)
	{
		for (int j=a[i].head;j;j=a[j].next)
		{
			int v=a[j].to;
			if (v<i)
			{
				int xf=find(v),yf=find(i);
				f[xf]=yf;
				add2(yf,xf);add2(xf,yf);
			}
		}
	}
	dfs1(1,0);
	dfs2(n,0);
	printf("%lld\n",ans);
	return 0;
} 

如果想看线段树代码的:
传送门

posted @ 2021-11-11 22:17  Thunder_S  阅读(151)  评论(0编辑  收藏  举报