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;
}
如果想看线段树代码的:
传送门。