Tarjan(原理、应用)

Tarjan

首先记录说明一下图论中的常用的概念

  • 无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图
  • 有向图中,若任意两个顶点 V i V_i Vi V j V_j Vj,满足从 $V_i $到 V j V_j Vj 以及从 V j V_j Vj V i V_i Vi 都连通,也就是都含有至少一条通路,则称此有向图为强连通图
  • 若无向图不是连通图,但图中存储某个子图符合连通图的性质,则称该子图为连通分量
  • 非强连通图有向图的极大强连通子图(最大的子图),称为强连通分量
  • 若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图
  • 无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通或者连通分枝数增加,那么这个点就叫做割点
  • 桥(割边)——指的是一条边,就是如果没有这条边,图的连通分量就会增加
  • 没有圈的连通图叫,树的边数恰好是顶点数减1,没有圈的非连通图叫做森林

一、算法介绍

T a r j a n Tarjan Tarjan是一种由 R o b e r t T a r j a n Robert Tarjan RobertTarjan提出的求解有向图强连通分量的线性时间的算法。通常可以用来求强连通分量、双连通分量、缩点、割点、割边等问题。

在这里插入图片描述

例如上图中顶点 1 、 2 、 3 1、2、3 123组成的子图就是这个有向图的强连通分量

T a r j a n Tarjan Tarjan算法可以找出这样的强连通分量, T a r j a n Tarjan Tarjan算法是基于对整个有向图的 d f s dfs dfs进行的,将整个图作为一棵搜索树,图中的强连通分量作为搜索树的子树。

对于上图,很容易看出强连通分量,但是要让计算机找出来,那么肯定要做好相应的标记,如果对于这张图进行 d f s dfs dfs遍历,假设从1开始搜索(用链式前向星存),搜索到的边如下

1 − > 2 − > 4 − > 5 1->2->4->5 1>2>4>5 2 − > 3 − > 1 2- >3->1 2>3>1

可以发现,只有 3 3 3 1 1 1这条边搜索到了已经走过的顶点 1 1 1,如果按照走过的顶点的先后顺序来表示时间,那么关系如下

顶点访问时间(第几个访问到的)
11
22
35
43
54

很明显,如果从一个点出发不断遍历,发现有一个能够走回之前已经走过的点,说明形成了一个环,环上的点能够互相访问,环上的所有点都是强连通

二、原理

上面举了一个例子,基于这一点,很容易想到需要维护一个访问时间顺序的数组,知道了访问顺序,那么怎么找出其中和这个点构成的所有强连通分量呢?同样需要一个数组来维护,在搜索每一个点的时候,如果发现这个点已经被访问过,说明形成了环,这时候就可以将当前的点进行更新,更新成已经访问过的点的顺序。因此 T a r j a n Tarjan Tarjan中重要的两个数组需要维护好

low[N];//low[i]的值表示i能够回溯的最小的祖先
dfn[N];//表示时间戳,dfn[i]的值表示第几个访问到i节点的

由于是递归实现的,明显数组 l o w low low是靠着回溯的时候更新的,而 d f n dfn dfn是靠着递进去的时候更新的

struct Edge {
	int to, next;
}edge[M * 2];
void add_Edge(int u, int v) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
}
void Tarjan(int x) { //表示当前的顶点x
	dfn[x] = low[x] = ++c_time; //更新时间戳
	for(int i = head[x]; i != -1; i = edge[i].next) { //访问所有x能够一步到达的顶点,用v表示
		int v = edge[i].to;
		if(!dfn[v]) { //如果顶点v没有访问过,就继续找下去
			Tarjan(v);
			low[x] = min(low[x], low[v]);//v是由x走过去的,因此x是v的祖先,如果有环,则可能low[v]小于low[x],因此要更新low[x]
		}
		low[x] = min(low[x], dfn[v]);//当v已经被访问过,出现了环,当前x则更新到小的那个节点
	}
}

上图中更新结果

d f n [ 1 ] = 1 、 d f n [ 2 ] = 2 、 d f n [ 3 ] = 5 、 d f n [ 4 ] = 3 、 d f n [ 5 ] = 4 dfn[1] = 1、dfn[2] = 2、dfn[3] = 5、 dfn[4] = 3、 dfn[5] = 4 dfn[1]=1dfn[2]=2dfn[3]=5dfn[4]=3dfn[5]=4

l o w [ 1 ] = 1 、 l o w [ 2 ] = 1 、 l o w [ 3 ] = 1 、 l o w [ 4 ] = 3 、 l o w [ 5 ] = 4 low[1] = 1、low[2] = 1、low[3] = 1、low[4] = 3、low[5] = 4 low[1]=1low[2]=1low[3]=1low[4]=3low[5]=4

使用Tarjan的时候,如果不能保证可以一遍搜完整个图,那么使用方式如下

for(int i = 1; i <= n; i++)
    if(!dfn[i])
        Tarjan(i);

三、应用

1、求强连通分量

强连通分量需要引入栈来进行记录,每次进入递归的时候都进栈。考虑这样一种情况,当前的节点 x x x在更新完之后,如果 d f n [ x ] = l o w [ x ] dfn[x] = low[x] dfn[x]=low[x]说明,x以及它的所有子节点构成强连通分量,因此在这个时候需要把 x x x节点后面进栈的节点和 x x x全部弹出,这些节点都是和 x x x强连通的

void Tarjan(int x) {
	dfn[x] = low[x] = ++c_time;
	stack[++t] = x; //进栈
	vst[x] = 1; //表示顶点x在栈中
	for(int i = head[x]; i != -1; i = edge[i].next) {
		int v = edge[i].to;
		if(!dfn[v]) {
			Tarjan(v);
			low[x] = min(low[x], low[v]);
		}
		else if(vst[v]) //如果v在栈中,并且已经访问或,则肯定要更新low[x]
			low[x] = min(low[x], dfn[v]);
	}
	if(dfn[x] == low[x]) { //出现强连通分量子树的最小根
		int cur;
		do {
			cur = stack[t--]; //弹栈
			vst[cur] = 0; //标记出栈
			cout << cur << " ";
		}while(x != cur);
		cout << "\n";
	}
}  
例1 [POJ 3180] The Cow Prom

题意:给出 n n n个点 m m m条边,求出有向图中所有大于 1 1 1的强连通分量个数

输入样例

5 4
2 4
3 5
1 2
4 1

输出样例

1

模板题

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cctype> 
#define N 10005
#define M 50010
using namespace std;
inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) {x = (x << 3) + (x << 1) + c - 48; c = getchar();}
	return f * x;
}
struct Edge{
	int to, next;
}edge[M * 2];

int n, m, cnt, c_time, t, ans;
int head[N], dfn[N], low[N], stack[N];
bool vst[N];
inline void addEdge(int u, int v) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
}

void Tarjan(int x) {
	dfn[x] = low[x] = ++ c_time;
	stack[++t] = x;
	vst[x] = 1;
	for(int i = head[x]; i != -1; i = edge[i].next) {
		int v = edge[i].to;
		if(!dfn[v]) {
			Tarjan(v);
			low[x] = min(low[x], low[v]);
		}else if(vst[v]) {
			low[x] = min(low[x], dfn[v]);
		}
	}
	int now = 0;
	if(dfn[x] == low[x]) { //找到所有以x为根的强连通分量
		int cur;
		do{
			cur = stack[t--];
			vst[cur] = 0;
			now ++;
		}while(cur != x);
	}
	if(now > 1) ans ++;
}

int main() {
	int u, v;
	memset(head, -1, sizeof(head));
	n = read(), m = read();
	for(int i = 1; i <= m; i++) {
		u = read(), v = read();
		addEdge(u, v);
	}
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) Tarjan(i);
	cout << ans;
	return 0;
}
例2 [POJ 2186]受欢迎的牛

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

输入

第一行两个数 N , M N,M N,M($1\leq N\leq 10000,1 \leq M\leq 50000 ) ; 接 下 来 ); 接下来 )M 行 , 每 行 两 个 数 行,每行两个数 A,B$ ,意思是 A A A认为 B B B是受欢迎的

输出

输出被除自己之外的所有牛认为是受欢迎的牛的数量。

样例输入

3 3
1 2
2 1
2 3

样例输出

1

首先把整个图染色, 所有强连通分量为一种颜色,或者说打上相同标记,然后缩点,遍历所有的强连通分量,把整个图当成 D A G DAG DAG(有向无环图)考虑,那么出度为 0 0 0的点如果只有 1 1 1个,这个点一定是被所有牛喜欢的(可多举几个例子证明)。(可看代码注释)

#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstring>
#include <cstdio>
#define M 50050
#define N 10050
using namespace std;

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)){x = (x << 3) + (x << 1) + c - 48; c = getchar();}
	return f * x;
}
struct Edge{
	int to, next;
}edge[M * 2];
//color表示染色标记的数组,sum[i]表示标记为i的图中顶点的数量,r数组表示出度
int head[N], low[N], dfn[N], color[N], sum[N], stack[N], r[N];
//c_time 表示时间戳,t用来记录栈中的元素个数,rs用来记录染色的数量,也就是连通分量的数量
int cnt, n, m, c_time, t, rs;
bool vst[N];

inline void add_Edge(int u, int v) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
}

void Tarjan(int x) {
	dfn[x] = low[x] = ++ c_time;
	stack[++t] = x; 
	vst[x] = 1;
	for(int i = head[x]; i != -1; i = edge[i].next) {
		int u = edge[i].to;
		if(!dfn[u]) {
			Tarjan(u);
			low[x] = min(low[x], low[u]);
		} else if (vst[u]) {
			low[x] = min(low[x], dfn[u]);
		}
	}
	if(dfn[x] == low[x]) {
		int cur;
		rs ++; //连通分量的数量增加
		do{
			cur = stack[t--];
			color[cur] = rs; //染色,标记
			sum[rs] ++; //记录该标记下的点数量
			vst[cur] = 0;
		}while(cur != x);
	}
}

int main () {
	memset(head, -1, sizeof(head));
	int u, v, judge = 0, loc;
	n = read(), m = read();
	for(int i = 1; i <= m; i++) {
		u = read(), v = read();
		add_Edge(u, v);
	}
	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 != -1; j = edge[j].next) {
			int ve = edge[j].to;
			if(color[i] != color[ve]) { //如果顶点i和ve不是同一连通分量,那么顶点i的标记出度+1(因为有缩点)
				r[color[i]] ++;
			}
		}
	}
	for(int i = 1; i <= rs; i++) { //遍历DAG
		if(r[i] == 0) { //出度为0
			judge ++;
			loc = i; //记录标记
		}
	}
	if(judge == 1) 
		cout << sum[loc]; //输出带有这种标记的顶点数量
	else 
		cout << 0;
	return 0;
}

2、求割点

割点:在无向联通图 G = ( V , E ) G=(V,E) G=(V,E)中: 若对于 x ∈ V x∈V xV, 从图中删去节点 x x x以及所有与 x x x关联的边之后, G G G分裂成两个或两个以上不相连的子图, 则称 x x x G G G割点

割边(桥):在一个无向图中,如果有一个边集合,删除这个边集合以后,图的连通分量增多,就称这个边集为割边集合,如果某个割边集合只含有一条边 X(也即{X}是一个边集合),那么X称为一个割边,也叫做

在这里插入图片描述
根据定义来看,割点为 3 、 4 3、4 34,桥为 d 、 e d、e de,有两种情况会出现割点

  • 当对于点 x x x存在儿子节点 y y y,使得 d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]low[y] x x x一定是割点
  • 如果根节点有 2 2 2个及以上的儿子,那么它也是割点(特判)
例题 [洛谷 3388] 割点(割顶)

题目

给出一个 n n n个点, m m m条边的无向图,求图的割点

输入格式

第一行输入 n , m n,m n,m

下面 m m m行每行输入 x , y x,y x,y表示 x x x y y y有一条边

输出格式

第一行输出割点个数

第二行按照节点编号从小到大输出节点,用空格隔开

样例输入

6 7
1 2
1 3
1 4
2 5
3 5
4 5
5 6

样例输出

1 
5
#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#define N 20005
#define M 100005
using namespace std;

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) {x = x * 10 + c - 48; c = getchar();}
	return f * x; 
}

struct Edge {
	int to, next;
}edge[M * 2];

int s_clock, cnt, n, m;
int dfn[N], low[N], head[N];
//low[i]表示i能回溯到的最小祖先,dfn[i]表示时间戳,也就是第几个访问到的
bool vst[N], cut[N];

inline void add_edge(int u, int v) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
}
void init() {
	memset(head, -1, sizeof(head));
}
void Tarjan(int x, int root) {
	int r = 0; //用来判断根节点是否是割点
	dfn[x] = low[x] = ++s_clock; //更新时间戳
	for(int i = head[x]; i != -1; i = edge[i].next) {
		int u = edge[i].to;
		if(!dfn[u]) {
			dfn[u] = 1;
			Tarjan(u, root);
			low[x] = min(low[x], low[u]);  // ****
			if(dfn[x] <= low[u] && x != root) cut[x] = 1; //判断割点,当前节点x的子节点u能回溯的最小祖先小于当前节点的时间戳,说明一定没有往回的路,那么当前节点一定是一个割点
			if(x == root) r ++; //最终回溯到了根节点
		} 
		low[x] = min(low[x], dfn[u]);   // ****
	}
	if(x == root && r > 1) cut[root] = 1; //如果有2条及以上的路 能回到根节点,那么根节点也是割点
}

int main() {
	init();
	int u, v, ans = 0;
	n = read(), m = read();
	for(int i = 1; i <= m; i++) {
		u = read(), v = read();
		add_edge(u, v);
		add_edge(v, u);
	}
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) Tarjan(i, i);
	for(int i = 1; i <= n; i++) //统计割点的个数
		if(cut[i]) ans ++;
	cout << ans << "\n";
	for(int i = 1; i <= n; i++) //输出割点
		if(cut[i])  cout << i << " ";
	return 0;
}

3、求桥(割边)

割边(桥):在一个无向图中,如果有一个边集合,删除这个边集合以后,图的连通分量增多,就称这个边集为割边集合,如果某个割边集合只含有一条边 X(也即{X}是一个边集合),那么X称为一个割边,也叫做

和求割点的方法类似,桥的判断如下

  • u u u的子节点是 v v v,当且仅当 d f n [ u ] &lt; l o w [ v ] dfn[u] &lt; low[v] dfn[u]<low[v]时, ( u , v ) (u,v) (u,v)是桥

但是由于是无向图,可能会有重边的情况,为了统一处理,可以利用链式前向星存边的特性,同一条边的序号一定是相邻的,因此在更新 l o w [ x ] low[x] low[x]的时候,需要判断当前边是否和上一条边相同

void Tarjan(int x, int fa) {
	low[x] = dfn[x] = ++c_time;
	for(int i = head[x]; i != -1; i = edge[i].next) {
		int u = edge[i].to;
		if(!dfn[u]) {
			Tarjan(u, i);
			low[x] = min(low[x], low[u]);
			if(dfn[x] < low[u]) ans++;
		}
		else if((i + 1) / 2 != (fa + 1) / 2) low[x] = min(low[x], dfn[u]); //不是同一条边
	}
} 
例题 [HDU 4378] Caocao’s Bridges

曹操建立了许多岛屿,同时还有连接岛屿的桥,周瑜有一枚炸弹,只能炸毁一座桥,周瑜想摧毁一座桥使得曹操的一个或者多个岛屿与其他岛屿分开。周瑜必须派人携带炸弹来炸毁桥,桥上有守卫,轰炸桥的士兵人数不能少于桥的守卫人数,请问周瑜至少要多少士兵才能完成分离任务

输入

测试用例不超过 12 12 12个。

在每个测试用例中:第一行包含两个整数 N N N M M M,意味着有 N N N个岛和 M M M个桥。所有岛都从 1 1 1 N N N编号。 ( 2 ≤ N ≤ 1000 , 0 &lt; M ≤ N 2 ) (2 \leq N \leq 1000,0 &lt;M \leq N^2) (2N1000,0<MN2

接下来的 M M M行描述了 M M M个桥。每条线包含三个整数 U , V U,V UV W W W,意味着有一个连接岛 U U U和岛 V V V的桥,并且在该桥上有 W W W守卫。( U ≠ V U≠V U̸=V 0 ≤ W ≤ 10 , 000 0 \leq W\leq 10,000 0W10,000

输入以 N = 0 N = 0 N=0 M = 0 M = 0 M=0结束。

输出

对于每个测试用例,输出周瑜必须发送的最小士兵号码才能完成任务。如果周瑜无法成功,请输出-1代替。

样例输入

3 3
1 2 7
2 3 4
3 1 4
3 2
1 2 7
2 3 4
0 0

样例输出

-1
4

策略

  • 判断图是否连通,不连通输出 − 1 -1 1(并查集)
  • T a r j a n Tarjan Tarjan找权值最小的桥,如果没有桥输出 − 1 -1 1
  • 如果桥上没有人。要输出 1 1 1,要有一个人扛炸药(坑!)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#define N 1005
#define M 1000005
#define INF 0x7fffffff
using namespace std;

inline void read(int &x) {
	x = 0; int f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)){x = (x << 3) + (x << 1) + c - 48; c = getchar();}
	x *= f;
}

struct Edge{
	int to, next, w;
}edge[M * 2];

int head[N], low[N], dfn[N], f[N];
int cnt, c_time, ans, n, m;

inline void init() {
	memset(head, -1, sizeof(head));
	memset(low, 0, sizeof(low));
	memset(dfn, 0, sizeof(dfn));
	for(int i = 1; i <= n; i ++)
		f[i] = i;
	cnt = 0;
	c_time = 0;
	ans = INF;
}

inline void addEdge(int u, int v, int cost) {
	edge[++cnt].to = v;
	edge[cnt].w = cost;
	edge[cnt].next = head[u];
	head[u] = cnt;
}

inline int find(int x) {
	if(x == f[x]) return x;
	else return f[x] = find(f[x]);
}
inline void unite(int x, int y) {
	int u = find(x);
	int v = find(y);
	f[u] = v;
}
inline bool IsSame(int x, int y) {
	int u = find(x);
	int v = find(y);
	if(u == v) return true;
	else return false;	
}
inline void Tarjan(int x, int fa) {
	dfn[x] = low[x] = ++c_time;
	for(int i = head[x]; i != -1; i = edge[i].next) {
		int u = edge[i].to;
		if(!dfn[u]) {
			Tarjan(u, i);
			low[x] = min(low[x], low[u]);
			if(dfn[x] < low[u]) ans = min(ans, edge[i].w);//更新桥的最小权值
		}else if((i + 1) / 2 != (fa + 1) / 2) 
			low[x] = min(low[x], dfn[u]);
	}
}
int main() {
	int u, v, cost, flag;
	while(1) {
		read(n), read(m);
		if(n == 0 && m == 0) break;
		flag = 0;
		init();
		for(int i = 1; i <= m; i ++) {
			read(u), read(v), read(cost);
			unite(u, v);
			addEdge(u, v, cost);
			addEdge(v, u, cost);	
		}
		for(int i = 1; i < n; i++) //判断图是否是连通的
			if(!IsSame(i, i+1)) {
				printf("0\n");
				flag = 1;
				break;
			}
		if(flag) continue;
		Tarjan(1, 0);
		if(ans == INF) ans = -1; //没有桥的情况
		if(ans == 0) ans++; //桥上没有人的情况
		printf("%d\n", ans);
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值