题意:
众所周知,小y好奇心极强。
这次,他看到了一个n个节点的树,他突然想知道有多少个三元组(u, v, w)满足其两两不同且存在一条u到w路径和一条v到w路径满足两条路径之间没有公共边。
单单知道这个小y还不满足,他决定增加一些边,每加一条边他就想知道答案。
数据规模:
20%: n <= 10, q <= 10
50%: n <= 500, q <= 500
100%: n <= 100000, q <= 100000
参考题解:https://blog.youkuaiyun.com/qq_33229466/article/details/80670669。
题意非常明确, w w 其实就是从到 v v 路径的一个必经中间点,
首先考虑原始的是一棵树的情况:显然,我们可以自底向上维护每个点作为中间点的贡献。这里我们选择将不合法的贡献从总数中减去以得到结果。
因为我们还需要得到关于点的更多信息(诸如它的父节点、它的深度等),所以我们将第一次执行的过程放在一个dfs里。
接下来考虑加边的情况。通过观察可以发现:若两端点和中间点均处于一个边双内,则一定存在合法路径;若一个端点与中间点处于同一边双中,而另一端点处于另一边双,亦存在合法路径;若两个端点位于同一边双,中间点处于另一边双,则必然不存在合法路径。证明相对简单,因为不同边双之间必然通过割边相连。故第三种情况下,两个端点必然要经过同一条割边以到达中间点。特别地,当三个点均不位于同一边双内时,我们发现这种情况与树的情况是非常相似的。反过来思考为什么树上易于统计:因为每条边都是割边,每个点相当于一个边双。
换句话说,当两个端点与中间点分属一条割边两侧时,是不存在合法路径的。
这个结果引导我们想到缩点。每次加边后,若
x
x
与位于同一边双内,则贡献是不改变的(没有新的不合法状况产生);若
x
x
与不位于同一边双内,则这条边使两个边双合并为一。将每个边双都作为树上的一个节点,仍然维护树上每个节点的非法贡献(这里的非法情况实际上只有上段所说的那一种。另,从语文的角度讲,非法也许就不能称为贡献了。由于暂时想不到更好的表达方式,姑且这么将就一下),即维护每个边双的贡献。因为边双里的点是等价的。
边双的合并可以用并查集完成。
值得注意的是,在缩完点后的树上,边双之间仍是以部分原树边相连的(新加的边不可能是割边)。
整体的思路已经阐述完毕。至于如何统计的问题,根据我个人阅读 std s t d 的体验,并不是非常的友好,所以在 code c o d e 里进行解释。很多抽象的地方通过画图可以较好地解决。如果画图也解决不了,闭眼感受一下就可以。
#include<bits/stdc++.h>
using namespace std;
const int maxn=100010;
int n,q;
int Link[maxn];
struct edge{
int y,next;
}e[maxn<<1];
int tot;
long long sizTree[maxn];//初始子树大小。
int dep[maxn];//深度。
int pre[maxn];//父节点。
long long sizDcc[maxn];//所在边双的大小。
int fa[maxn];//并查集。即该节点所属边双的代表元。
long long Val[maxn];//不合法贡献。
long long ans;//答案。
inline void read(int &x)
{
x=0;int f=1;char s=getchar();
for(;s<'0'||s>'9';s=getchar()) if(s=='-') f=-1;
for(;s>='0'&&s<='9';s=getchar()) x=(x<<3)+(x<<1)+s-48;
x*=f;
}
inline void Restore(int Root,int x)//先清空两个边双对答案的所有贡献。
{
ans-=1LL*(n-sizDcc[Root])*(n-sizDcc[Root])*sizDcc[Root]+1LL*(n-sizDcc[x])*(n-sizDcc[x])*sizDcc[x];//以所在边双中的点作为中间点的贡献。多减的非法的那一部分后面会加回来。
ans-=1LL*((n-sizDcc[Root])*sizDcc[Root]*(sizDcc[Root]-1)<<1)+1LL*((n-sizDcc[x])*sizDcc[x]*(sizDcc[x]-1)<<1);//中间点和一端点在同一边双的合法贡献。乘2是因为(u,v,w)与(v,u,w)不等价。
ans-=1LL*sizDcc[Root]*(sizDcc[Root]-1)*(sizDcc[Root]-2)+1LL*sizDcc[x]*(sizDcc[x]-1)*(sizDcc[x]-2);//三个点都在同一边双的合法贡献。
ans+=1LL*sizDcc[Root]*Val[Root]+1LL*sizDcc[x]*Val[x];//加上非法贡献。
}
inline void Union(int Root,int x)//合并。
{
fa[x]=Root;
sizDcc[Root]+=sizDcc[x];
Val[Root]+=Val[x]-1LL*sizTree[x]*sizTree[x]-1LL*(n-sizTree[x])*(n-sizTree[x]);//第二项:因为x所在边双与其父节点所在边双合并,所以第一次统计时以它原来的父节点作为中间点,两端点位于子树中的情况变为合法。第三项:因为x到父节点之间的边不再是割边,所以第一次统计时以x为中间点,两端点位于x的子树以外的情况也变为合法。
}
inline void Update(int Root)//重新统计。
{
ans+=1LL*(n-sizDcc[Root])*(n-sizDcc[Root])*sizDcc[Root];
ans+=1LL*(n-sizDcc[Root])*sizDcc[Root]*(sizDcc[Root]-1)<<1;
ans+=1LL*sizDcc[Root]*(sizDcc[Root]-1)*(sizDcc[Root]-2);
ans-=1LL*Val[Root]*sizDcc[Root];
}
inline void Insert(int x,int y)
{
e[++tot].y=y;
e[tot].next=Link[x];Link[x]=tot;
}
inline int Find(int x)
{
return fa[x]=(fa[x]==x?x:Find(fa[x]));
}
inline void Merge(int Root,int x)//合并父节点所在边双与当前边双。
{
Restore(Root,x);
Union(Root,x);
Update(Root);
}
void dfs(int x)//维护节点的基本信息,并完成第一次贡献统计。
{
sizTree[x]=1LL;
for(int i=Link[x];i;i=e[i].next)
if(e[i].y^pre[x]){
pre[e[i].y]=x;dep[e[i].y]=dep[x]+1;
dfs(e[i].y);
Val[x]+=1LL*sizTree[e[i].y]*sizTree[e[i].y];//因为x到子节点之间是一条割边,当x为中间点时,以x的同一子节点的子树中的点(可以理解为割边下侧)作为两端点,是不合法的。
sizTree[x]+=sizTree[e[i].y];
}
Val[x]+=1LL*(n-sizTree[x])*(n-sizTree[x]);//x到它的父节点之间也是一条割边,当x为中间点,以x的子树以外的点(可以理解为割边上侧)作为两端点,也是不合法的。
ans-=Val[x];//减去非法贡献。
}
void Init()
{
read(n);
int x,y;
for(int i=1;i<n;++i)
{
read(x);read(y);
Insert(x,y);Insert(y,x);
}
}
void Work()
{
memset(Val,0,sizeof(Val));
memset(sizTree,0,sizeof(sizTree));
ans=1LL*n*(n-1)*(n-1);//三元组的总数。
dep[1]=1;pre[1]=-1;
dfs(1);
printf("%lld\n",ans);
for(int i=1;i<=n;++i) sizDcc[i]=1LL,fa[i]=i;//初始状态下,每个节点自己都是一个边双。
read(q);
int x,y,t;
for(int i=1;i<=q;++i)
{
read(x);read(y);
x=Find(x);y=Find(y);
while(x^y)//若两者不属于同一边双。
{
if(dep[x]<dep[y]) t=x,x=y,y=t;//(x,y)间连边显然会使在缩点后的树上,x(这里指它代表的边双,y同理)与y到LCA路径上的节点被纳入同一边双,即这些边双被合并。所以我们不断重复上跳、合并的过程,直到已在同一边双中(到达LCA)。
Merge(Find(pre[x]),x);x=Find(pre[x]);//因为不同边双之间以原树边相连的。所以我们依赖当前点与其父节点的边进行上跳。
}
printf("%lld\n",ans);
}
}
int main()
{
freopen("triple.in","r",stdin);
freopen("triple.out","w",stdout);
Init();
Work();
fclose(stdin);fclose(stdout);
return 0;
}
写完去看了一下别人的
code
c
o
d
e
,惊觉直接统计合法贡献会清晰直观得多。悔不当初,……就当训练思维了。直接统计的写法在OJ的提交记录里可以找到。
在程序上这题并不能看出缩点的痕迹,事实上很多题都是在意念里用到这种思想,并不需要我们真的缩点建图。当然,这题也是因为其原始图是树的特殊性,其实并查集+树边也可以理解为新图的一种存在形式。
例行总结:图论学习,从入门到入土。