连通性相关

强连通

强连通:有向\(G\) 中每个点中可互相到达。

强连通分量:极大的强连通。(最大可能的)

求强连通分量

先跑出图的 DFS 搜索树(黑边)。

一个结论:一个强连通分量 一定在该强连通分量中的第一个被访问的点 的子树内。

设根为 \(u\),考虑若存在一个点 \(v\) 不在 \(u\) 子树中则一定存在一条边离开 \(u\) 子树,只可能是返祖/横叉边,显然指向的点已经被标记过了。

Tarjan

一般都用这个。

\(dfn(u),low(u)\),后者表示不经过其父亲能到达的最小的 \(dfn\)

记一个栈 \(stk\) 表示当前搜索到的节点,对于一个节点 \(v\)

  • \(v\) 未被访问:先算 \(v\) 的答案,再用 \(low(v)\) 更新 \(low(u)\)
  • \(v\) 访问过,且在栈中:用 \(dfn(v)\) 更新 \(low(u)\)
  • \(v\) 访问过,不在栈中:\(v\) 所在连通分量已被处理完。不做操作。

最后只有满足 \(dfn(u)=low(u)\)\(u\) 才能作为结论中的点,据此确定一个强连通分量 \(G=\braket{stk,\cdot}\)

时间复杂度 \(O(n+m)\)。非常优秀。

int stk[maxn],top;
int dfn[maxn],dfncnt;
int low[maxn];
bool ins[maxn];
int scc[maxn],scccnt;

void tarjan(int u){
    dfn[u]=low[u]=++dfncnt;
    stk[++top]=u; ins[u]=1;
    for(int v:e[u]){
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }else if(ins[v]){
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u]){
        scccnt++;
        while(stk[top]!=u){
            scc[stk[top]]=scccnt;
            ins[stk[top]]=0;
            top--;
        }
        scc[stk[top]]=scccnt;
        ins[stk[top]]=0;
        top--;
    }
}

Kosaraju

黑科技吧。实现非常简单:DFS,以后序遍历(即回溯时编号,相当于反 \(dfn\) 满足 \(rdfn(u)>rdfn(v)(v\in subtree(u))\))标号,在反图上从编号最大的点 DFS,所有遍历到的点即为一个强连通分量。???有点玄幻但的确是对的,马良极小。同样时间复杂度 \(O(n+m)\)

int scc[maxn],scccnt;
int rdfn[maxn],dfncnt;
bool vis[maxn];
void dfs1(int u){
    vis[u]=1;
    for(int v:e[u]) 
        if(!vis[v]) dfs1(v);
    rdfn[++dfncnt]=u;
}
void dfs2(int u,int col){
    scc[u]=col;
    for(int v:re[u]) 
        if(!scc[v]) dfs2(v);
}
void Kosaraju(){
    for(int i=1;i<=n;i++)
        if(!vis[i]) dfs1(i);
    for(int i=n;i>=1;i--)
        if(!scc[i]) dfs2(rdfn[i],++scccnt);
}

例题:[USACO03FALL / HAOI2006] 受欢迎的牛 G

每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N 头牛,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。

先缩点,然后找 DAG 上出边为 0 的点,如果不唯一就无解,否则即为那个点包含的连通分量大小。

code
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+3;
int n,m;
vector<int>e[maxn];
int stk[maxn],top;
int dfn[maxn],dfncnt,low[maxn];
int scc[maxn],scccnt;
bool ins[maxn];
int out[maxn],siz[maxn];
void tarjan(int u){
    dfn[u]=low[u]=++dfncnt;
    stk[++top]=u; ins[u]=1;
    for(int v:e[u]){
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }else if(ins[v]){
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u]){
        scccnt++;
        while(stk[top]!=u){
            scc[stk[top]]=scccnt;
            ins[stk[top]]=0;
            top--;
            siz[scccnt]++;
        }
        scc[stk[top]]=scccnt;
        ins[stk[top]]=0;
        top--;
        siz[scccnt]++;
    }
}
signed main(){
    cin>>n>>m;
    for(int i=1,u,v;i<=m;i++){
        cin>>u>>v;
        e[u].emplace_back(v);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i);
    for(int i=1;i<=n;i++){
        for(int j:e[i]){
            if(scc[i]!=scc[j]){
                out[scc[i]]++;
            }
        }
    }
    int oucnt=0,id=0;
    for(int i=1;i<=scccnt;i++){
        if(out[i]==0) oucnt++,id=i;
    }
    if(oucnt>1){
        cout<<0;
    }else{
        cout<<siz[id];
    }
    return 0;
}

割点:如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点。
割边:如果把一条边删除后这个图的极大连通分量数增加了,那么这条边就是这个图的割边。(也叫桥)

求割点

Tarjan

还是它。

如果 \(\exists v\in son(u)\)\(low(v)\ge dfn(u)\),即点 \(v\) 不能通过除 \(u\) 以外的边回到祖先,则说明 \(u\) 是一个割点。

而注意到起始点的 \(low(v)\) 一定大于等于 \(dfn(u)\),却不能确定 \(u\) 是割点,如图点 1 并不是割点。

所以我们对于这种点需要特殊判断。在搜索树上,若它有大于一个儿子,则说明它是一个割点。

int rcnt;
void tarjan(int u,int rt){
    dfn[u]=low[u]=++dfncnt;
    for(int v:e[u]){
        if(!dfn[v]){
            if(u==rt) rcnt++;
            tarjan(v,rt);
            low[u]=min(low[u],low[v]);
            if(low[v]>=dfn[u]) ge[u]=1;
        }else{
            low[u]=min(low[u],dfn[v]);  
        }
    }
}

for(int i=1;i<=n;i++){
    if(!dfn[i]){
        rcnt=0;
        tarjan(i,0,i);
        ge[i]=rcnt>1;
    }
}

求割边

Tarjan

依然是它。

如果 \(\exists v\in son(u)\)\(low(v){\color{red}{\text{ > }}} dfn(u)\),即点 \(v\) 不能通过除 \(u\) 以外的边回到祖先,则说明 \((u,fa(u))\) 是一条割边。

为什么是大于呢,见图。

显然 \(low(v)=dfn(u)\) 并不能说明什么。图上 \(low(2)>dfn(1)\),即 2 只能通过走边 \((2,1)\) 才能到 1,即 \((2,1)\) 是割边。

并没有对应板子喵。

int fa[maxn],br[maxn];
void tarjan(int u,int Fa){
    fa[u]=Fa;
    dfn[u]=low[u]=++dfncnt;
    for(int v:e[u]){
        if(!dfn[v]){
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u]) br[v]=1; // 割边为边 (v,fa[v])
        }else{
            low[u]=min(low[u],dfn[v]);  
        }
    }
}

双连通

点双连通:删除任意一个非 \(u,v\) 的点 \(u,v\) 仍然连通。称 \(u,v\) 点双连通。
边双连通:删除任意一条边 \(u,v\) 仍然连通。称 \(u,v\) 边双连通。
点双连通:极大的点双连通。
边双连通:极大的边双连通。

求边双连通分量

Tarjan

仍然可以用 tarjan,无向图上的边双等价于有向图上的强连通,由于是无向图注意不要走反边(可以直接判 fa,但是如果碰到大小为 2 的边双就只判第一次不走 fa 即可,也可以去重)。时间复杂度 \(O(n+m)\)

void tarjan(int u,int fa){
    dfn[u]=low[u]=++dfncnt;
    stk[++top]=u; ins[u]=1;
    int pppp=0;
    for(int v:e[u]){
        if(v==fa&&!pppp){pppp=1; continue;}
        if(!dfn[v]){
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
        }else if(ins[v]){
            low[u]=min(low[u],dfn[v]);
        }
    }   
    if(dfn[u]==low[u]){
        scccnt++;
        while(stk[top]!=u){
            scc[scccnt].emplace_back(stk[top]);
            ins[stk[top]]=0;
            top--;
        }
        scc[scccnt].emplace_back(stk[top]);
        top--;
    }
}

求点双连通分量

Tarjan

是它。

点双有两个性质:

  • 两个有交的点双的交集大小为 1,即割点
  • 在一个点双中,dfn 最小的点一定是割点或者

对于性质 2 讨论:

  • \(u\) 是割点,则 \(u\) 为当前点双的根;
  • \(u\) 是根,则:
    • \(u\) 有大于 1 个子树,则 \(u\) 是割点;
    • \(u\) 有 1 个子树,则它是当前点双的根;
    • \(u\) 没有子树,则将其看做一个大小为 1 的点双。
void tarjan(int u,int rt){
    dfn[u]=low[u]=++dfncnt;
    stk[++top]=u;
    if(u==rt&&e[u].empty()){
        scccnt++;
        scc[scccnt].emplace_back(stk[top]);
        return;
    }
    for(int v:e[u]){
        if(!dfn[v]){
            tarjan(v,rt);
            if(u==rt) rcnt++;
            if(low[v]>=dfn[u]){
                if(u!=rt||rcnt>1) cut[u]=1; // 是割点
                scccnt++;
                while(stk[top]!=v){
                    scc[scccnt].emplace_back(stk[top]);
                    top--;
                }
                scc[scccnt].emplace_back(v); 
                top--;
                scc[scccnt].emplace_back(u); 
            }
            low[u]=min(low[u],low[v]);
        }else{
            low[u]=min(low[u],dfn[v]);
        }
    }
}

圆方树

基于点双,将图上问题转化为树上(森林)问题。

对于每个点双,新建一个方点,将每个点双中的点连向方点,构建成一个树(森林)。

构建圆方树

Tarjan

还™是 tarjan。具体而言,每找到一个点双就新建一个方点,再每个圆点向方点连边即可。注意开双倍空间

对于一个点的图,视具体情况处理。

int nodecnt; // 节点数
vector<int>g[maxn<<1]; // 圆方树
void tarjan(int u,int rt){
    dfn[u]=low[u]=++dfncnt;
    stk[++top]=u;
    if(u==rt&&e[u].empty()){
        scccnt++;
        scc[scccnt].emplace_back(stk[top]);
        return;
    }
    for(int v:e[u]){
        if(!dfn[v]){
            tarjan(v,rt);
            if(u==rt) rcnt++;
            if(low[v]==dfn[u]){
                if(u!=rt||rcnt>1) cut[u]=1; // 是割点
                scccnt++;
                while(stk[top]!=v){
                    scc[scccnt].emplace_back(stk[top]);
                    top--;
                }
                scc[scccnt].emplace_back(v); 
                top--;
                scc[scccnt].emplace_back(u); 
            }
            low[u]=min(low[u],low[v]);
        }else{
            low[u]=min(low[u],dfn[v]);
        }
    }
}
void build_sct(){
    nodecnt=n;
    for(int i=1;i<=scccnt;i++){
        if(scc[i].size()==1){
            ......
        }else{
            nodecnt++;
            for(int j:scc[i]){
                g[nodecnt].emplace_back(j);
                g[j].emplace_back(nodecnt);
            }
        }
    }
}

例题:P4630 [APIO2018] 铁人两项

给你一个不一定连通的无向图,求三元组 \((s,c,f)\) 满足存在从 \(s\) 经过 \(c\)\(f\) 的简单路径的数量。

\(n\le 10^5,m\le 2\times 10^5\)

建出圆方树,然后跑树形 DP 即可。

code
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+3;
int n,m;
vector<int>e[maxn];
int dfn[maxn],dfncnt,low[maxn];
int trsiz;
int stk[maxn],top;
int nodecnt; // 节点数
vector<int>g[maxn<<1]; // 圆方树
int siz[maxn<<1],w[maxn<<1];
void tarjan(int u){
    dfn[u]=low[u]=++dfncnt;
    stk[++top]=u;
    trsiz++;
    for(int v:e[u]){
        if(!dfn[v]){
            tarjan(v);
            if(low[v]==dfn[u]){
                nodecnt++;
                w[nodecnt]=0;
                while(stk[top]!=v){
                    w[nodecnt]++;
                    g[nodecnt].emplace_back(stk[top]);
                    g[stk[top]].emplace_back(nodecnt);
                    top--;
                }
                w[nodecnt]++;
                g[nodecnt].emplace_back(stk[top]);
                g[stk[top]].emplace_back(nodecnt);
                top--;
                w[nodecnt]++;
                g[nodecnt].emplace_back(u);
                g[u].emplace_back(nodecnt);
            }
            low[u]=min(low[u],low[v]);
        }else{
            low[u]=min(low[u],dfn[v]);
        }
    }
}
int ans=0;
void dfs1(int u,int fa){
    siz[u]=u<=n;
    for(int v:g[u]){
        if(v!=fa){
            dfs1(v,u);
            ans=ans+2*w[u]*siz[v]*siz[u];
            siz[u]+=siz[v];
        }
    }
    ans=ans+2*w[u]*siz[u]*(trsiz-siz[u]);
}
signed main(){
    cin>>n>>m; nodecnt=n;
    for(int i=1,u,v;i<=m;i++){
        cin>>u>>v;
        e[u].emplace_back(v);
        e[v].emplace_back(u);
    }
    for(int i=1;i<=n;i++) w[i]=-1;
    for(int i=1;i<=n;i++){
        if(!dfn[i]){
            trsiz=0;
            top=0;
            tarjan(i);
            dfs1(i,0);
        }
    }
    cout<<ans;
    return 0;
}
posted @ 2024-10-24 15:26  view3937  阅读(9)  评论(1编辑  收藏  举报
Title