20210713 Tarjan算法总结
Tarjan算法与无向图连通性
tarjan算法与无向图连通性
无向图的割点与桥
定义:
给定无向连通图 G = ( V , E ) : G=(V,E): G=(V,E):
若对于 x ∈ V x\in V x∈V ,从图中删去节点 x x x 以及所有与 x x x 关联的边之后, G G G 分裂成两个或两个以上不相连的子图,则称 x x x 为 G G G 的割点
若对于 e ∈ E e\in E e∈E ,从图中删去边 e e e 之后, G G G 分裂成两个不相连的子图,则称 e e e 为 G G G 的桥或割边
一些概念
- 在dfs中,按照每个节点第一次被访问的时间顺序,一次给N个节点1-N的整数标记,这种标记称为时间戳,记为 d f n [ x ] dfn[x] dfn[x] 。
- 搜索树(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=n−1 ,否则如果为负数则 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)的求法
未完待续……