[FJOI2020]染色图的联通性问题 题解

FJOI2020 D1T2


题目大意

给出一个由 $n$ 个点 $m$ 条边构成的染色无向图,求删去每一个点及与其相连的边后图中不连通的同色点对数量。$n,m\leq 10^5$。

思路分析

可以想到先统计原图的答案,然后对删去每个点后的多出的答案进行计算,输出时加上即可。

原图的答案很容易统计,遍历一遍同时计算即可。

如何统计删去每个点后多出的答案?模拟过后很容易发现,多出的答案就是删去这个点后断开的连通块之间形成的同色点对,只需要知道断开后每个连通块的各色点的数量即可。暴力统计显然复杂度太高,连最低档的分都拿不到。毒瘤FJOI

可以想到用 tarjan 找删去后的各个连通块,统计用启发式合并或线段树合并。线段树合并实现简单但是空间较大,但是还是有神犇卡过去了。这里用的是线段树合并。

合并的时候怎么计算呢?

设当前已合并的连通块该颜色的点数和为 $x$ ,原连通块该颜色的点数为 $y$ ,那么遍历一个新的连通块时,设该连通块该颜色的点数为 $z$ ,则将该连通块合并后与其它部分断开后的贡献为为 $z*(y-x-z)+x*(y-x-z)=(x+z)(y-x-z)$ 。注意,若该连通块与原连通块之间会被断掉产生多出的答案,则需要加上 $x*z$ 。

注意,上面的原连通块断开后即为当前节点的父节点所在的连通块。

这样这道题就很好解了。对于原图中的每个连通块:

1. 先遍历一遍,计算出连通块中每个颜色的点数
2. 跑一遍 tarjan ,同时合并数据,计算断开连通块中的每个点后多出的答案
3. 再遍历一遍,计算连通块与原图的其它连通块贡献的答案,然后将当前连通块的数据清空

#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
const int N=5e5+100;
struct Seg
{
    int lson,rson;
    ll val,sumv;
    #define lson(i) t[i].lson
    #define rson(i) t[i].rson
    #define val(i) t[i].val
    #define sumv(i) t[i].sumv
}t[N*21];
int n,m,tot,cnt,D;
ll now,sum;
int head[N],ver[2*N],Next[2*N];
int rt[N],c[N],nowc[N],sumc[N],dfn[N],low[N];
ll ans[N];
bool vp[N],vq[N];
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void change(int &p,int l,int r,int k)
{
    if(!p)
        p=++cnt;
    if(l==r)
    {
        val(p)++,sumv(p)+=nowc[l]-1;
        return ;
    }
    int mid=l+r>>1;
    if(k<=mid)
        change(lson(p),l,mid,k);
    else
        change(rson(p),mid+1,r,k);
    sumv(p)=sumv(lson(p))+sumv(rson(p));
}
int merge(int x,int y,int l,int r)
{
    if(!x)
        return y;
    if(!y)
        return x;
    if(l==r)
    {
        sumv(x)=(val(x)+val(y))*(nowc[l]-val(x)-val(y));//断开后的答案
        now+=val(x)*val(y);//当前合并的两个连通块断开的贡献
        val(x)+=val(y);
        return x;
    }
    int mid=l+r>>1;
    lson(x)=merge(lson(x),lson(y),l,mid);
    rson(x)=merge(rson(x),rson(y),mid+1,r);
    sumv(x)=sumv(lson(x))+sumv(rson(x));
    return x;
}
void pre(int x)
{
    vp[x]=1;nowc[c[x]]++;
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(!vp[y])
            pre(y);
    }
}//先遍历一遍,计算出连通块中每个颜色的点数
void tarjan(int x)
{
    int nowr=0;//临时根
    low[x]=dfn[x]=++cnt;
    change(rt[x],1,D,c[x]);
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(!dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x])//断开会使连通块断开,类似割点
            {
                now=0;
                rt[x]=merge(rt[x],rt[y],1,D);
                ans[x]+=now;//多出的答案
            }
            else
                nowr=merge(nowr,rt[y],1,D);//不会断开,合并到临时根上,避免多统计答案
        }
        else
            low[x]=min(low[x],dfn[y]);
    }
    ans[x]+=sumv(rt[x]);
    rt[x]=merge(rt[x],nowr,1,D);//合并临时根
}//跑一遍 tarjan ,同时合并数据,计算断开连通块中的每个点后多出的答案
void query(int x)
{
    vq[x]=1;
    sum+=nowc[c[x]]*sumc[c[x]];//计算当前连通块与其它连通块的贡献
    sumc[c[x]]+=nowc[c[x]],nowc[c[x]]=0;
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(!vq[y])
            query(y);
    }
}//再遍历一遍,计算当前连通块与原图的其它连通块贡献的答案,然后将当前连通块的数据清空
int main()
{
    //freopen("pair.in","r",stdin);
    //freopen("pair.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&c[i]),D=max(D,c[i]);
    for(int i=1,x,y;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            pre(i),tarjan(i),query(i);
    for(int i=1;i<=n;i++)
        printf("%lld\n",sum+ans[i]);
    return 0;
}
posted on 2020-07-17 08:20  TEoS  阅读(530)  评论(0编辑  收藏  举报