强连通分量
给定一张有向图. 若对于图中任意两个节点 x, y, 既存在从x到y的路径, 也存在从y到x的路径, 则称该有向图是"强连通图".
有向图的极大强连通子图被称为“强连通分量”, 简记为SCC(Strongly Connected Component).
在上面的定义中, 我们称一个强连通子图G’ = (V’, E’)“极大”(其中V’ ⊆ V, E’ ⊆ E), 是指不存在包含G’的更大的子图G’’ = (V’’, E’’), 满足V’ ⊆ V’’, E’ ⊆ E’’ 并且G’'也是强连通子图.
流图: 给定有向图 G = (V, E), 若存在 r ∈ V, 满足从r出发能够到达V中所有的点, 则称G是一个"流图"(Flow Graph), 记为(G, r), 其中r称为流图的源点
时间戳: 在深度优先遍历的过程中, 按照每个节点第一次被访问的时间顺序, 依次给予流图中N个节点 1~N 的整数标记, 该标记被称为时间戳, 记为 dfn[x].
追溯值: 设 subtree(x) 表示流图的搜索树中以x为根的子树. x的追溯值 low[x] 定义为满足以下条件的节点的最小时间戳:
给个丑陋的样例图:
1.该点在栈中.
2.存在一条从 subtree(x) 出发的有向边, 以该点为终点.
根据定义, Tarjan 算法按照以下步骤计算"追溯值":
1.当节点x第一次被访问时, 把x入栈, 初始化 low[x] = dfn[x].
2.扫描从x出发的每条边 (x, y).
(1)若y没被访问过, 则说明 (x,y) 是树枝边, 递归访问 y, 从y回溯之后, 令 low[x] = min(low[x], low[y]).
(2)若y被访问过并且y在栈中, 则令 low[x] = min(low[x], dfn[y]).
3.从x回溯之前, 判断是否有 low[x] = dfn[x]. 若成立, 则不断从栈中弹出节点, 直至x出栈.
判定强连通分量:
#define maxn 10000 + 5
#define maxm 10000 + 5
struct edge{
int to, next;
edge(){}
edge(int _to, int _next){
to = _to;
next = _next;
}
}e[maxm];
int head[maxn], k;
int dfn[maxn], low[maxn], tot;
int stack[maxn], vis[maxn], top;
int col[maxn], cnt;
int n, m;
void tarjan(int x){
dfn[x] = low[x] = ++tot;
stack[++top] = x;
vis[x] = true;
int y;
for(int i = head[x]; ~i; i = e[i].next){
y = e[i].to;
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
}else if(vis[y]){
low[x] = min(low[x], dfn[y]);
}
}
if(dfn[x] == low[x]){
cnt++;
do{
y = stack[top--];
vis[y] = false;
col[y] = cnt;
}while(x != y);
}
}
int main(){
memset(head, -1, sizeof head);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
add(x, y);
}
for(int i = 1; i <= n; i++) if(!dfn[i]){
tarjan(i);
}
for(int i = 1; i <= n; i++){
cout << i << " belongs to SCC[" << col[i] << "]" << endl;
}
return 0;
}
缩点:
#define maxn 10000 + 5
#define maxm 10000 + 5
using namespace std;
struct edge{
int to, next;
edge(){}
edge(int _to, int _next){
to = _to;
next = _next;
}
}e[maxm], ec[maxm];
int head[maxn], k;
int head_c[maxn], kc;
int dfn[maxn], low[maxn], tot;
int stack[maxn], vis[maxn], top;
int col[maxn], cnt;
int n, m;
void tarjan(int x){
dfn[x] = low[x] = ++tot;
stack[++top] = x;
vis[x] = true;
int y;
for(int i = head[x]; ~i; i = e[i].next){
y = e[i].to;
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
}else if(vis[y]){
low[x] = min(low[x], dfn[y]);
}
}
if(dfn[x] == low[x]){
cnt++;
do{
y = stack[top--];
vis[y] = false;
col[y] = cnt;
}while(x != y);
}
}
void add_c(int x, int y){
ec[kc] = edge(y, head_c[x]);
head_c[x] = kc++;
}
int main(){
memset(head, -1, sizeof head);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
add(x, y);
}
for(int i = 1; i <= n; i++) if(!dfn[i]){
tarjan(i);
}
for(int i = 1; i <= n; i++){
for(int j = head[i]; ~j; j = e[j].next){
int y = e[j].to;
if(col[i] != col[y]) add_c(i, y);
}
}
return 0;
}
割点与桥
给定无向连通图 G = (V, E):
若对于x∈V,从图中删去节点x以及所有与x关联的边之后,G分裂成两个或两个以上不相连的子图,则称x为G的割点.
若对于e∈E,从图中删去边e后,G分裂成两个不相连的子图,则称e为G的桥或割边.
搜索树: 在无向连通图中任选一个节点出发进行深度优先遍历, 每个点只访问一次. 所有发生递归的边 (x, y) (换言之, 从x到y是对y的第一次访问) 构成一棵树, 我们把它称为"无向连通图的搜索树". 当然, 一般无向图(不一定连通)的各个连通块的搜索树构成无向图的"搜索森林".
割边判定法则
无向边 (x, y) 是桥, 当且仅当搜索树上存在x的一个子节点y, 满足:dfn[x] < low[y]
#define maxn 10000 + 5
#define maxm 10000 + 5
using namespace std;
struct edge{
int to, next;
edge(){}
edge(int _to, int _next){
to = _to;
next = _next;
}
}e[maxm << 1];
int head[maxn], k;
int dfn[maxn], low[maxn], tot;
int n, m;
bool bridge[maxm << 1];
void tarjan(int x, int in_edge){
dfn[x] = low[x] = ++tot;
for(int i = head[x]; ~i; i = e[i].next){
int y = e[i].to;
if(!dfn[y]){
tarjan(y, i);
low[x] = min(low[x], low[y]);
if(dfn[x] < low[y]){
bridge[i] = bridge[i ^ 1] = true;
}
}else if(i != (in_edge ^ 1)){
low[x] = min(low[x], dfn[y]);
}
}
}
int main(){
memset(head, -1, sizeof head);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
add(x, y);
add(y, x);
}
for(int i = 1; i <= n; i++) if(!dfn[i]){
tarjan(i, 0);
}
for(int i = 0; i < k; i += 2) if(bridge[i]){
printf("edge(%d, %d) is bridge\n", e[i ^ 1].to, e[i].to);
}
return 0;
}
割点判定法则
若x不是搜索树的根节点(深度优先遍历的起点), 则x是割点当且仅当搜索树上存在x的一个子节点y, 满足: dfn[x] <= low[y]
特别地, 若x是搜索树的根节点, 则x是割点当且仅当搜索树上存在至少两个子节点 y1, y2 满足上述条件.
#define maxn 10000 + 5
#define maxm 10000 + 5
using namespace std;
struct edge{
int to, next;
edge(){}
edge(int _to, int _next){
to = _to;
next = _next;
}
}e[maxm << 1];
int head[maxn], k;
int dfn[maxn], low[maxn], tot;
int cut[maxn], root;
int n, m;
void tarjan(int x){
dfn[x] = low[x] = ++tot;
int flag = 0;
for(int i = head[x]; ~i; i = e[i].next){
int y = e[i].to;
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
if(dfn[x] <= low[y] && (x != root || ++flag > 1)){
cut[x] = true;
}
}else{
low[x] = min(low[x], dfn[y]);
}
}
}
int main(){
memset(head, -1, sizeof head);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
add(x, y);
add(y, x);
}
for(int i = 1; i <= n; i++) if(!dfn[i]){
root = i;
tarjan(i);
}
for(int i = 1; i <= n; i++) if(cut[i]){
printf("%d is cut-vertex\n", i);
}
return 0;
}
双连通分量
若一张无向连通图不存在割点, 则称它为"点双连通图". 若一张无向连通图不存在桥, 则称它为"边双连通图".
无向图的极大点双连通子图被称为"点双连通分量", 简记为"v-DCC"(vertex double connected component). 无向连通图的极大边双连通子图被称为"边双连通分量", 简记为"e-DCC". 二者统称为"双连通分量", 简记为"DCC".
定理
一张无向连通图是"点双连通分量", 当且仅当满足下列两个条件之一:
1.图的顶点数不超过2.
2.图中任意两点都同时包含在至少一个"简单环"指的是不自交的环,也就是我们通常画出的环.
一张无向连通图是"边双连通图", 当且仅当任意一条边都包含在至少一个简单环中.
边双连通分量的求法
求出无向图中所有的桥, 把桥都删除后, 无向图会分成若干个连通块, 每一个连通块就是一个"边双连通分量".
在具体的程序实现中, 一般先用Tarjan算法标记出所有的桥边. 然后, 再对整个无向图执行一次深度优先遍历(遍历过程不访问桥边), 划分出每个连通块.
#define maxn 10000 + 5
#define maxm 10000 + 5
using namespace std;
struct edge{
int to, next;
edge(){}
edge(int _to, int _next){
to = _to;
next = _next;
}
}e[maxm << 1];
int head[maxn], k;
int dfn[maxn], low[maxn], tot;
bool bridge[maxm << 1];
int col[maxn], cnt;
int n, m;
void tarjan(int x, int in_edge){
dfn[x] = low[x] = ++tot;
for(int i = head[x]; ~i; i = e[i].next){
int y = e[i].to;
if(!dfn[y]){
tarjan(y, i);
low[x] = min(low[x], low[y]);
if(dfn[x] < low[y]){
bridge[i] = bridge[i ^ 1] = true;
}
}else if(i != (in_edge ^ 1)){
low[x] = min(low[x], dfn[y]);
}
}
}
void dfs(int x){
col[x] = cnt;
for(int i = head[x]; ~i; i = e[i].next){
int y = e[i].to;
if(!col[y] && !bridge[i]) dfs(y);
}
}
int main(){
memset(head, -1, sizeof head);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
add(x, y);
add(y, x);
}
for(int i = 1; i <= n; i++) if(!dfn[i]){
tarjan(i, 0);
}
for(int i = 1; i <= n; i++) if(!col[i]){
cnt++;
dfs(i);
}
for(int i = 1; i <= n; i++){
printf("%d belongs to DCC[%d]\n", i, col[i]);
}
return 0;
}
点双连通分量的求法
若某个节点为孤立点, 则它自己单独构成一个v-DCC. 除了孤立点以外, 点双连通分量的大小至少为2. 根据v-DCC定义中的"极大"性, 虽然桥不属于任何e-DCC, 但是割点可能属于多个v-DCC.
为了求出"点双连通分量", 需要在Tarjan算法的过程中维护一个栈, 并按照如下方法维护栈中的元素:
1.当一个节点第一次被访问时, 把该节点入栈.
2.当割点判定法则中的条件 dfn[x] <= low[y] 成立时, 无论x是否为根, 都要:
(1)从栈顶不断弹出节点, 直到节点y被弹出.
(2)刚才弹出的所有节点与节点x一起构成一个v-DCC.
#define maxn 10000 + 5
#define maxm 10000 + 5
using namespace std;
struct edge{
int to, next;
edge(){}
edge(int _to, int _next){
to = _to;
next = _next;
}
}e[maxm << 1];
int head[maxn], k;
int dfn[maxn], low[maxn], tot;
int stack[maxn], top;
int cut[maxn], root;
vector<int> dcc[maxn];
int n, m, cnt;
void tarjan(int x){
dfn[x] = low[x] = ++tot;
stack[++top] = x;
if(x == root && head[x] == -1){
dcc[++cnt].push_back(x);
return;
}
int flag = 0;
for(int i = head[x]; ~i; i = e[i].next){
int y = e[i].to;
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
if(dfn[x] <= low[y]){
if(x != root || ++flag > 1) cut[x] = true;
cnt++;
int z;
do{
z = stack[top--];
dcc[cnt].push_back(z);
}while(z != y);
dcc[cnt].push_back(x);
}
}else{
low[x] = min(low[x], dfn[y]);
}
}
}
int main(){
memset(head, -1, sizeof head);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
add(x, y);
add(y, x);
}
for(int i = 1; i <= n; i++) if(!dfn[i]){
root = i;
tarjan(i);
}
for(int i = 1; i <= cnt; i++){
printf("v-DCC #%d:", i);
for(int j = 0; j < dcc[i].size(); j++){
printf(" %d", dcc[i][j]);
}
puts("");
}
return 0;
}
例题:P2341,P2863,P3225,P2746
模板: P3387,P3388