强连通分量,割点与割边
割点-割边:
如果将强连通分量的定义转到无向图,如果任意两点连通,我们称其为连通分量
如果存在一个节点 \(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\) 算法。
算法流程:
- 从任意节点开始dfs,求出每个节点的 \(num_u,low_u\) .
- 统计不同的 \(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;
}