tarjan-------一个神奇的算法

强连通分量

给定一张有向图. 若对于图中任意两个节点 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
3
4
5
6
7
8
9

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值