强连通分量,割点与割边

割点-割边:

如果将强连通分量的定义转到无向图,如果任意两点连通,我们称其为连通分量

如果存在一个节点 \(x\) ,删除它后使图被分割为了多块连通分量,那么我们称这个点是割点,

同理如果删除了一条边使图被分为了多个连通分量,我们称这条边是割边。

那么,我们怎么求出有哪些割点和割边呢?我们采用一种名为“深度优先搜索生成树”的数据结构。

从任意一个节点开始dfs,将搜索的节点与当前节点连接的边标记,最后这些被标记的节点,组成了一棵生成树。为了求割点割边,我们还要两个定理:

定理1:以 \(u\) 为根节点构成的生成树,如果为割点,那么至少有两条及两条以上个分支。

定理2:如果 \(u\) 不为根节点,那么如果为割点,需满足 \(u\) 的子节点 \(v\) 及后代没有回退边连至 \(u\) 的祖先。

根据定理二,定义 \(num[u]\) 为bfs的访问顺序,\(low[v]\) 记录 \(v\)\(v\) 的后代能回到的祖先的 \(num\)

只要 \(low[v] \ge num[u]\),就说明 \(v\) 在这条支路上,无法通过回退边连回 \(u\) 的祖先,最多只能连回 \(u\)。而如果判断割边,则只需要把大于等于改为大于,即可判断。

双连通分量:

在一个连通图中任选两点,如果两个节点至少存在两条路径不存在经过相同的节点,那么我们称其为点双连通,而一个图中扩展到最大的点双连通子图,我们称其为点双连通分量。同理,如果存在两条点间至少两条路径不存在经过相同的边,我们称其为边双连通,而极大边双连通子图则被称为边双连通分量。

求法:

首先知道,图中如果存在多个点双连通分量,那么之间只存在一个公共节点。如果存在多个,那么意味着可以合并,与定义产生矛盾。而这个公共点,便是割点。之前有提到割点的求法,可以得知在dfs的过程中,在找到了一个割点的情况下,就已经访问过了一块点双连通分量,所以在求的过程中,我们可以把遍历过的点储存起来,可以采用一个stack记录。

但要注意,这里应当存的是边,因为一个点可能属于多个点双连通分量,所以如果把栈中的点排出来,就可能导致其他点双连通分量失去了这些被排出去的点,即使已经被遍历过。

而要求边双连通分量就更简单了,我们只需要求出每个节点的 \(low\) 值,\(low\) 值相同的即为一块边双连通分量。因为不存在相同的边,就意味着不能经过相同的点。因为这个点的出现,导致后面的点的 \(low\) 值都为这个点的 \(num\) 值(相当于被堵住了),所以要寻找 \(low\) 值相同的块。此时,两个连通分量只需要一遍dfs便能求出来。

强连通分量-定义:

对于一个有向图 \(G\) , 存在节点 \(u,v \in G\) ,存在一条路径使得
两个节点能互相到达,我们称其两个节点是强联通的。如果整个图任意两个节点都是强联通的,
我们称这个图是一个强连通图

同理,如果有向图 \(G\) 不是强连通图,则可以将其分成多个子图,并且都是强连通图,
且已扩展到最大,无法子图外的节点强连通,则我们称这些子图为强连通分量(Strongly Connected Component).

什么,你问我为什么没有无向图的强连通分量?无向图只需满足两个节点连通即可,因为没有方向。

强连通分量-求法:

首先,先看几个关于强连通分量的定理:

定理1:对于两块强连通分量 \(A,B\) ,存在两个节点 \(u \in A,v \in B\) ,存在一条 \(u\) 连至 \(v\) 边,那么就不存在一条 \(v\) 连至 \(u\) 的边。

如果存在,则两块强连通分量可以相互连通,即能合并起来,与定义不符。

定理2:与定理1条件相同,从 \(A/B\) 中的任意节点开始dfs,设 \(d(x)\) 表示dfs访问这个节点的时间, \(f(x)\) 为dfs回溯到这个节点的时间,存在一条 \(u\) 连至 \(v\) 的边,那么满足在 \(A\) 中的 \(f(x)\) 最大值大于 \(B\) 中的 \(f(x)\) 最大值。

由于 \(B\) 无法连回 \(A\) ,所以如果先开始遍历 \(B\) ,那么还需要已 \(A\) 中的节点为起点再次便利一遍,时间比 \(B\) 要晚。

推广:同理如果存在一条 \(v\) 连至 \(u\) 的边,那么也满足在 B 中的 \(f(x)\) 最大值大于 \(A\) 中的 \(f(x)\) 最大值。

定理3:强连通分量所有边方向相反,点集依然不变,并且依然是强连通分量。

Kosareju算法

那么给出了这些定理,我们该怎么求出强连通分量呢?接下来要介绍一下 \(Kosareju\) 算法:

算法流程:

1.从任意节点开始dfs,计算出每一个节点的 \(d(x),f(x)\) .

2.由定理3得,求出了反向图的强连通分量,相当于求出了原图的强连通分量。

3.在所有未访问的节点中找出 \(f(x)\) 最大的节点,开始dfs,将访问的节点打上标记

4.当步骤3结束时,访问的节点组成了一个强连通分量。

5.重复步骤3,直到找出所有的强连通分量。

HDU 1269:

给定一个由 \(n\) 个节点和 \(m\) 条边组成的有向图,判断是否任意两个节点是强连通的,是则输出YES,否则输出No。

\(\mathbb{The Code:}\)

#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
int n,m,cnt,vis[10005],rvis[10005];//cnt统计强连通分量个数
vector<int> G[10005],rG[10005];
vector<int> S;
void dfs1(int x){
    if(vis[x]) return; //一开始被访问直接回溯
    vis[x] = 1;
    for(int i = 0;i < G[x].size();i++)
        dfs1(G[x][i]);
    S.push_back(x);
    return;
}
void dfs2(int x){
    if(rvis[x]) return;
    rvis[x] = cnt;
    for(int i = 0;i < rG[x].size();i++)
        dfs2(rG[x][i]);
    return;
}
void Kosaraju(){
    for(int i = 1;i <= n;i++) dfs1(i);
    for(int i = n - 1;i >= 1;i--){ //按照f(x)从大到小便利
        if(rvis[S[i]]) continue;
        cnt++;
        dfs2(S[i]);
    }
}
int main(){
    cin >> n >>m;
    while(m--){
        int u , v;
        cin >> u >> v;
        G[u].push_back(v);
        rG[v].push_back(u);
    }
    Kosaraju();
    cout<<(cnt == 1 ? "Yes" : "No")<<'\n';
    return 0;
}

Tarjan算法:

在介绍之前,我们需要在知道一个定理:

在强连通分量里的任意节点,都可以通过至少一条路径绕回自己。

绕回自己?说到绕回,我们可以想到求割点里面的回连边。如果节点 \(u\) 的子节点 \(v\) 及其后代的 \(low\) 值都大于等于 \(num[u]\) ,那么 \(u\) 就为割点。这种 \(low[]\)\(num[]\) 的操作就是 \(Tarjan\) 算法。

算法流程:

  1. 从任意节点开始dfs,求出每个节点的 \(num_u,low_u\) .
  2. 统计不同的 \(low_u\) 相同的值为同一块强连通分量。

根据定理,任意节点出发都有回路回到自己,因此在dfs的过程中,同一个强连通分量内的元素的 \(low_v\) 都为最开始访问的节点 \(u\) 。因此,同一个强连通分量内的节点 \(low_u\) 的值都是相通的。

由于只要遍历一次,所以时间复杂度为 \(O(n + m)\)

\(\mathbb{The Code:}\)

#include<iostream>
#include<vector>
#include<stack>
using namespace std;
int vis[10005];
int n,m,ss,cnt,num[10005],low[10005];
vector<int> G[10005];
stack<int> s;
void dfs(int u){
    s.push(u);
    low[u] = num[u] = ++ss;
    for(auto v : G[u]){
        if(!num[v]){
            dfs(v);
            low[u] = min(low[v],num[u]);
        }else if(!vis[v]) low[u] = min(low[u],num[v]);
    }
    if(num[u] == low[u]){
        cnt++;
        while(!s.empty()){
            vis[s.top()] = cnt;
            if(u == s.top()){
                s.pop();
                break;
            }
            s.pop();
        }
    }
    return;
}
int main(){
    cin >> n >> m;
    while(m--){
        int u , v;
        cin >> u >> v;
        G[u].push_back(v);
    }
    for(int i = 1;i <= n;i++){
        if(!num[i]) continue;
        dfs(i);
    }
    cout<<(cnt == 1 ? "Yes" : "No");
    return 0;
}
posted @ 2025-07-24 10:16  Cai_hy  阅读(31)  评论(0)    收藏  举报