在介绍算法之前先引入几个概念:
时间戳: 在深度优先搜索时,每个点 x x x第一次被访问的顺序为时间戳,记作 d f n [ x ] dfn[x] dfn[x](从 1 1 1开始)
追溯值: 从 x x x开始走,所能遍历到的最小时间戳,记作 l o w [ x ] low[x] low[x]
1. tarjan 1.\text {tarjan} 1.tarjan求 SCC \text{SCC} SCC
给定一张有向图 G G G, ∀ u , v \forall u,v ∀u,v,即存在从 u u u到 v v v的路径又存在从 v v v到 u u u的路径,那么该图 G G G是强连通图。
强连通分量 (SCC) \text{(SCC)} (SCC) 就是有向图的极大强连通子图
为了方便,我们定义有向图中的四种边:
( 1 ) (1) (1) 树枝边:搜索树中的边
( 2 ) (2) (2) 前向边:搜索树中 x x x是 y y y的祖先节点
( 3 ) (3) (3) 后向边:搜索树中 y y y是 x x x的祖先节点
( 4 ) (4) (4) 横叉边:以上三种情况之外的边: x x x到 y y y的边,并且满足 d f n [ y ] < d f n [ x ] dfn[y]<dfn[x] dfn[y]<dfn[x]
一定是往左边连的边,如果往右,那么根据深度优先遍历会走到右边。
其中后向边和横叉边是构成环的重要的边,所以为了找到这两条边, tarjan \text{tarjan} tarjan算法在深度优先搜索时维护一个栈来记录节点 x x x的祖先节点。如果正在访问的点恰好是栈中的祖先节点,那么就可以形成环,如果正在访问的点可以到栈中的祖先节点,那么他们三个以及搜索路径就可以构成环。
tarjan \text{tarjan} tarjan算法的步骤:
记全局时间戳为 t i m e s t a m p timestamp timestamp
( 1 ) (1) (1) 当 x x x第一次被遍历时,将 x x x入栈,初始化 d f n [ x ] = l o w [ x ] = + + t i m e s t a m p dfn[x]=low[x]=++timestamp dfn[x]=low[x]=++timestamp
( 2 ) (2) (2) 遍历 x x x的出边 j j j:
① ① ①:若 j j j未被访问 ( d f n [ j ] = 0 ) (dfn[j]=0) (dfn[j]=0),那就访问 j j j,回溯之后更新追溯值 l o w [ x ] = min ( l o w [ x ] , d f n [ j ] ) low[x]=\min(low[x], dfn[j]) low[x]=min(low[x],dfn[j])
② ② ②:若 j j j被访问过且 j j j在栈中,更新追溯值 l o w [ x ] = min ( l o w [ x ] , d f n [ j ] ) low[x]=\min(low[x],dfn[j]) low[x]=min(low[x],dfn[j])
( 3 ) (3) (3) 从 x x x回溯之前,判断是否 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],如果有那么说明是环,则不断将栈中节点出栈,直到 x x x出栈为止。
SCC \text{SCC} SCC的判定:
如果 x x x是强连通分量的最高点,等价于 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],那么就说明找到了整个强连通分量,则栈中从 x x x到栈顶的所有节点构成强连通分量。
SCC \text{SCC} SCC缩点:
重新建图的时候只需要判定 i d [ i ] id[i] id[i]是否等于 i d [ j ] id[j] id[j],
如果不相同就建新边。
代码:
void tarjan(int x) {
dfn[x] = low[x] = ++ timestamp;
s.push(x), st[x] = 1;
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j);
low[x] = min(low[x], low[j]);
} else if (st[j]) {
low[x] = min(low[x], dfn[j]);
}
}
if (dfn[x] == low[x]) {
scc_cnt ++;
int y;
do {
y = s.top();
s.pop();
st[y] = 0;
id[y] = scc_cnt;
Size[scc_cnt] ++;
} while (y != x);
}
}
s c c c n t scccnt scccnt表示图中强连通分量的个数, i d [ i ] id[i] id[i]数组表示 x x x节点属于编号为 i i i的强连通分量, S i z e [ i ] Size[i] Size[i]数组表示编号为 i i i的强连通分量里点的个数。
2. tarjan 2.\text {tarjan} 2.tarjan求 e-DCC \text{e-DCC} e-DCC
给定一张无向图 G G G,如果不存在割边,则称为边双连通图,无向图的极大边双连通分量被称为边双连通分量 ( e-DCC ) (\text{e-DCC}) (e-DCC)
求割边:
找到一个条边,满足 d f n [ x ] < l o w [ y ] ( y 是 x 子 节 点 ) dfn[x] <low[y] (y是x子节点) dfn[x]<low[y](y是x子节点)
因为从 y y y出发,无论走到哪条边,都不可能到达 ≥ d f n [ x ] \ge dfn[x] ≥dfn[x]的点,所以删除这条边之后就将图分成了两部分。在算法中,可以巧妙地运用异或运算来标记割边。
e-DCC \text{e-DCC} e-DCC的判定:
只需要求出无向图中的所有割边,把割边删除,无向图会分成若干连通块,每一个连通块就是一个边双连通分量。
e-DCC \text{e-DCC} e-DCC缩点:
把每个 e-DCC \text{e-DCC} e-DCC看成点,每条割边看成新边。
代码:
void tarjan(int x, int fa) {
dfn[x] = low[x] = ++ timestamp;
s.push(x);
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j, i);
low[x] = min(low[x], low[j]);
if (dfn[x] < low[j]) {
st[i] = st[i ^ 1] = 1;
}
} else if (i != (fa ^ 1)) {
low[x] = min(low[x], dfn[j]);
}
}
if (low[x] == dfn[x]) {
dcc_cnt ++;
int y;
do {
y = s.top();
s.pop();
id[y] = dcc_cnt;
} while (y != x);
}
}
i d [ i ] id[i] id[i]表示点 y y y属于编号为i的双连通分量, s t [ i ] , s t [ i ∧ 1 ] st[i],st[i \wedge 1] st[i],st[i∧1]标记每条割边, d c c c n t dcccnt dcccnt表示边双连通分量的个数。
3. tarjan 3.\text {tarjan} 3.tarjan求 v-DCC \text{v-DCC} v-DCC
给定一张无向图 G G G,如果不存在割点,则称为点双连通图,无向图的极大点双连通分量被称为<stron> ( v-DCC ) (\text{v-DCC}) (v-DCC)</stron>
求割点:
如果 x x x不是搜索树的根节点,则 x x x是割点当且仅当搜索树上存在一个子节点 y y y使得 d f n [ x ] ≤ l o w [ y ] dfn[x] \le low[y] dfn[x]≤low[y]
v-DCC \text{v-DCC} v-DCC的判定:
特判孤立点为单独的点双连通分量,除此之外,其他的点双连通分量大小至少为 2 2 2。
做法:
( 1 ) (1) (1) 当节点第一次被访问,将节点入栈。
( 2 ) (2) (2) 当点 x x x,出边 y y y满足 d f n [ x ] ≤ l o w [ y ] dfn[x] \le low[y] dfn[x]≤low[y], 从栈顶不断弹出节点,直到 y y y被弹出,那么所有弹出的节点与点 x x x构成一个点双连通分量。
v-DCC \text{v-DCC} v-DCC缩点:
由于一个割点可能属于多个 v-DCC \text{v-DCC} v-DCC,所以建图时必须将割点单独拿出来建,即每个割点向该割点所在的 v-DCC \text{v-DCC} v-DCC连一条新边。设无向图 G G G有 p p p个割点和 t t t个 v-DCC \text{v-DCC} v-DCC,那么新图的节点个数为 p + t p + t p+t
代码:
void tarjan(int x) {
dfn[x] = low[x] = ++ timestamp;
s.push(x);
if (x == root && h[x] == -1) {
dcc_cnt ++;
dcc[dcc_cnt].push_back(x);
return ;
}
int cnt = 0;
for (int i = h[x]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j);
low[x] = min(low[x], low[j]);
if (dfn[x] <= low[j]) {
cnt ++;
if (x != root || cnt > 1) st[x] = 1;
dcc_cnt ++;
int y;
do {
y = s.top();
s.pop();
dcc[dcc_cnt].push_back(y);
} while (y != j);
dcc[dcc_cnt].push_back(x);
}
} else {
low[x] = min(low[x], dfn[j]);
}
}
}
d c c c n t dcccnt dcccnt表示点双连通分量的个数, d c c [ i ] [ j ] dcc[i][j] dcc[i][j]数组表示新图中割点编号为 i i i,对应 v-DCC \text{v-DCC} v-DCC中编号为 j j j的邻接矩阵。