Codeforces 855G Harry Vs Voldemort(边双+缩点+并查集)

题意
众所周知,小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.csdn.net/qq_33229466/article/details/80670669

题意非常明确, w w 其实就是从u v v 路径的一个必经中间点,

首先考虑原始的是一棵树的情况:显然,我们可以自底向上维护每个点作为中间点的贡献。这里我们选择将不合法的贡献从总数中减去以得到结果。
因为我们还需要得到关于点的更多信息(诸如它的父节点、它的深度等),所以我们将第一次执行的过程放在一个dfs里。

接下来考虑加边的情况。通过观察可以发现:若两端点和中间点均处于一个边双内,则一定存在合法路径;若一个端点与中间点处于同一边双中,而另一端点处于另一边双,亦存在合法路径;若两个端点位于同一边双,中间点处于另一边双,则必然不存在合法路径。证明相对简单,因为不同边双之间必然通过割边相连。故第三种情况下,两个端点必然要经过同一条割边以到达中间点。特别地,当三个点均不位于同一边双内时,我们发现这种情况与树的情况是非常相似的。反过来思考为什么树上易于统计:因为每条边都是割边,每个点相当于一个边双。
换句话说,当两个端点与中间点分属一条割边两侧时,是不存在合法路径的。

这个结果引导我们想到缩点。每次加边(x,y)后,若 x x y位于同一边双内,则贡献是不改变的(没有新的不合法状况产生);若 x x y不位于同一边双内,则这条边使两个边双合并为一。将每个边双都作为树上的一个节点,仍然维护树上每个节点的非法贡献(这里的非法情况实际上只有上段所说的那一种。另,从语文的角度讲,非法也许就不能称为贡献了。由于暂时想不到更好的表达方式,姑且这么将就一下),即维护每个边双的贡献。因为边双里的点是等价的。
边双的合并可以用并查集完成。

值得注意的是,在缩完点后的树上,边双之间仍是以部分原树边相连的(新加的边不可能是割边)。

整体的思路已经阐述完毕。至于如何统计的问题,根据我个人阅读 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的提交记录里可以找到。
在程序上这题并不能看出缩点的痕迹,事实上很多题都是在意念里用到这种思想,并不需要我们真的缩点建图。当然,这题也是因为其原始图是树的特殊性,其实并查集+树边也可以理解为新图的一种存在形式。
例行总结:图论学习,从入门到入土。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值