2021-07-13

Tarjan算法与无向图连通性

tarjan算法与无向图连通性

无向图的割点与桥

定义:

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

若对于 e ∈ E e\in E eE ,从图中删去边 e e e 之后, G G G 分裂成两个不相连的子图,则称 e e e G G G割边

一些概念

  1. 在dfs中,按照每个节点第一次被访问的时间顺序,一次给N个节点1-N的整数标记,这种标记称为时间戳,记为 d f n [ x ] dfn[x] dfn[x]
  2. 搜索树(dfs树)从一个节点出发进行dfs,每个点指访问一次。所有发生递归的边(x,y)构成一颗树。

下图左侧展示了一张无向连通图,灰色节点是深度优先遍历的起点,加粗的边是“发生递归”的边,右侧展示了深度优先遍历的搜索树,并标注了时间戳
在这里插入图片描述
3:追溯值: l o w [ x ] low[x] low[x],设 s u b t r e e ( x ) subtree(x) subtree(x) 表示搜索树种以 x x x 为根的子树。 l o w [ x ] low[x] low[x] 定义为一下节点的时间戳最小值。
1: s u b t r e e ( x ) subtree(x) subtree(x) 中的节点。
2:通过一条不在搜索树上的边,能到达的 s u b t r e e ( x ) subtree(x) subtree(x) 的节点。

所以对于点 x x x 来说,
首先有 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x] ;
y y y 没遍历过,则先 d f s ( y ) dfs(y) dfs(y) ,再有 l o w [ x ] = m i n ( l o w [ x ] . l o w [ y ] ) low[x]=min(low[x].low[y]) low[x]=min(low[x].low[y]);
y y y 已经遍历过了,则 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])

下图的中括号[]里的数值标注了每个节点的“追溯值” l o w low low
在这里插入图片描述

割边判定法则

定义:

无向边 ( x , y ) (x,y) (x,y) 是桥,当且仅当搜索树上存在x的一个子节点y,满足: d f n [ x ] < l o w [ y ] dfn[x]<low[y] dfn[x]<low[y]

即去掉边(x,y)图就断开成了两部分

下面的程序求出一张无向图中所有的桥。特别需要注意,因为我们遍历的是无向图,所以从每个点 x x x 出发,总能访问到它的父节点 f a fa fa 。根据 l o w low low 的计算方法, ( x , f a ) (x,fa) (x,fa) 属于搜索树上的边,且 f a fa fa 不是 x x x 的子节点,故不能用 f a fa fa 的时间戳来更新 l o w [ x ] low[x] low[x]

但是,如果仅记录每个节点的父节点,会无法处理重边的情况——当 x x x f a fa fa 之间有多条边时, ( x , f a ) (x,fa) (x,fa) 一定不是桥,在这些重复的边中,只有一条算是“搜索树上的边”,其他的几条都不算。故有重边的时候, d f n [ f a ] dfn[fa] dfn[fa] 能用来更新 l o w [ x ] low[x] low[x]

一个好的解决方案是:改为记录“递归进入每个节点的边的编号”。编号可认为是边在邻接表中存储的下标位置。然后我们运用成对变换的方法

对于任意一个非负整数 n n n,如果 n n n 为正数那么 n n n x o r xor xor 1 = n − 1 1 = n − 1 1=n1 ,否则如果为负数则 n n n x o r xor xor 1 = n + 1 1 = n+1 1=n+1

把无向图的每一条边看作双向边,成对存储在下标“2和3” , “4和5” , “6和7” … 处。若沿着编号为 i i i 的边递归进入了节点 x x x ,则忽略从 x x x 出发的编号为 i i i x o r xor xor 1 1 1 的边,通过其他边计算 l o w [ x ] low[x] low[x] 即可

code:

const int SIZE = 100010;
int head[SIZE], ver[SIZE * 2], Next[SIZE * 2];
int dfn[SIZE], low[SIZE], n, m, tot, num;
bool bridge[SIZE * 2];
void add(int x,int y) {
	ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}

void tarjan(int x, int in_edge) {
	dfn[x] = low[x] = ++num;
	for (int i = head[x]; i; i = Next[i]) {
		int y = ver[i];
		if (!dfn[y]){
			tarjan(y, i);
			low[x] = min(low[x], low[y]);
			
			if (low[y] > dfn[x])
				bridge[i] = bridge[i ^ 1] = true;
		}
		else	if (i != (in_edge ^ 1))
			low[x] = min(low[x], dfn[y]);
	}
}

int main() {
	cin >> n >> m;
	tot = 1;
	for (int i = 1; i <= m; i++) {
		int x, y;
		scanf("%d%d", %x, &y);
		add(x, y);	add(y, x);
	}
	for (int i = 1; i <= n; i++)
		if (!dfn[i])	tarjan(i, 0);
	for (int i = 2; i < tot; i += 2)
		if (bridge[i])
			printf("%d %d\n", ver[i ^ 1], ver[i]);
}

割点判定法则

若x不是跟节点,则x是割点当且仅当存在一个x的子节点y,满足: d f n [ x ] ≤ l o w [ y ] dfn[x]\leq low[y] dfn[x]low[y]

证明方法与割边类似,在下图中共有两个割点,分别是时间戳为1和6两个点
在这里插入图片描述
下面的程序求出一张无向图中所有的割点。所以在求割点时,不必考虑父节点和重边的问题,从x出发能访问到的所有点的时间戳都可以用来更新 l o w [ x ] low[x] low[x]

code:

const int SIZE = 100010;
int head[SIZE], ver[SIZE * 2], Next[SIZE * 2];
int dfn[SIZE], low[SIZE], stack[SIZE];
int n, m, tot, num, root;
bool cut[SIZE];
void add(int x,int y) {
	ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}

void tarjan(int x) {
	dfn[x] = low[x] = ++num;
	int flag = 0;
	for (int i = head[x]; i; i = Next[i]) {
		int y = ver[i];
		if (!dfn[y]) {
			tarjan(y);
			//如果没有被遍历,则无法找到回溯点,所以应该先遍历
			low[x] = min(low[x], low[y]);
			//用子节点更新父节点
			if (low[y] >= dfn[x]) {
				flag++;
				if (x != root || flag > 1)	cut[x] = true;
				//根据定义,根节点不是割点
			}
		}
		else	low[x] = min(low[x], dfn[y]);
	}
}

int main() {
	cin >> n >> m;
	tot = 1;
	for (int i = 1; i <= m; i++) {
		int x, y;
		scanf("%d%d", %x, &y);
		if (x == y)	continue;
		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 ", i);
	puts("are cut-vertexes");
}

无向图的双连通分量

  • 若一张图不存在割点,则称他为点双联通分量。
  • 若一张图不存在桥,则称他为边双联通图。

无向图的极大点双联通子图被称为点双联通分量,简记为“v-DCC”。
无向图的极大边双联通子图被称为边双联通分量,简记为“e-DCC”。
二者统称为双联通分量,简记为“DCC”。

边双连通分量(e-DCC)的求法

只需要把求出的无向图中的所有的割边都删掉后,无向图会分成若干个连通块,每一个连通块都是一个边双联通分量。
在这里插入图片描述
一般先用 Tarjan 算法标记出所有的桥边。然后,再对整个无向图执行一次深度优先遍历(遍历的过程中不访问桥边)。划分出每个连通块。下面的代码在 Tarjan 求桥的程序基础上,计算出数组 c , c [ x ] c,c[x] c,c[x] 表示节点 x x x 所属的“边双连通分量”的编号

code:

int c[SIZE], dcc;
void dfs(int x) {
	c[x] = dcc;
	for (int i = head[x]; i; i = Next[x]) {
		int y = ver[i];
		if (c[y] || bridge[i])	continue;
		dfs(y);
	}
}
//以下code片段加在main函数中
for (int i = 1; i <= n; i++)
	if (!c[i]) {
		++dcc;
		dfs(i);
	} 
printf("There are %d e-DCC.\n", dcc);
for (int i = 1; i <= n; i++)
	printf("%d belongs to DCC %d.\n", i, c[i]);
e-DCC 的缩点

把每个 e-DCC 看作一个节点,把桥边 ( x , y ) (x,y) (x,y) 看作连接编号为 c [ x ] c[x] c[x] c [ y ] c[y] c[y] 的 e-DCC 的code基础上,把 e-DCC 收缩为一个节点的方法就称为“缩点”

code:

int hc[SIZE], vc[SIZE * 2], nc[SIZE * 2], tc;
void add_c(int x, int y) {
	vc[++tc] = y, nc[tc] = hc[x], hc[x] = tc;
}
//以下code片段加在main函数中
tc = 1;
for (int i = 2; i <= tot; i++) {
	int x = ver[i ^ 1], y = ver[i];
	if (c[x] == c[y])	continue;
	add_c(c[x], c[y]);
} 
printf("缩点以后的森林, 点数%d, 边数%d(可能有重边)\n", dcc, tc / 2);
for (int i = 2;i <= tc; i += 2)
	printf("%d %d\n", vc[i ^ 1], vc[i]);

点双连通分量(v-DCC)的求法

未完待续……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值