定义
首先要明确各种定义,非常容易搞混:
- 时间戳:数组一般叫 d f n [ ] dfn[\;] dfn[],记录点第一次入栈的顺序。
- 搜索树:dfs 时生成的树
- 割点:去掉这个点,原无向联通图不再联通。割点集合同理。
- 割边/桥:去掉这条边,原无向联通图不再联通。
- 点双联通图:不存在割点的图。判定:顶点数不超过2的无向联通图是点双。顶点数大于2的无向连通图是点双,当且仅当任意两点至少包含在一个简单环内。
- 边双联通图:不存在割边的图。判定:任意一条边包含在一个环内。
- 点双联通分量:极大点双联通图,即不存在包含这个点双联通子图的更大的双联通子图。
- 边双联通分量:同6,点改为边。
- 边双缩点:去掉割边,一个联通块缩成一个点。
- 点双缩点:点双缩成一个点,与割点连成图。
- 强连通分量的树边、前向边、后向边、横插边:1搜索树上的边,2连到子孙,3连到祖先,4连到不在同一棵子树的点。
Tarjan算法
割点和点双
无向图 的双连通分量(biconnectivity)
- 定义:
- articulation point: 关节点,去掉这个点图变得不连通
- biconnected graph: 不存在关节点
- biconnected component: maximal biconnected subgraph
- tarjan 算法:
- 生成一颗 dfs 树,遍历顺序记为
dfn[v]
- 除了树边之外仅可能存在一种边:连接 u 和 u 子树中的节点 v 的边。记
low[v]
为从 v 和 v 子树中的节点 出发走 1 条 非树边能够到达的最小 dfn。当 u 的某个儿子 vlow[v]>=dfn[u]
时 u 是关节点。 - 想要记录每个双联通分量中的点有哪些:将遍历到的点都入栈,在找到关节点的时候不断出栈直到关节点出栈,然后再把关节点入栈。(因为每个关节点可能会被包含在多个点双中)
- 注意:对于图的所有不连通的分量都要搜索,注意特判孤立节点的情况
- 生成一颗 dfs 树,遍历顺序记为
void Tarjan(int u)
{
dfn[u] = low[u] = ++idx;
if (rt == u && point[u] == -1){ // 孤立的点特判
dccn++;
dcc[dccn].clear();
dcc[dccn].push_back(u);
return;
}
stk[++st] = u;
for (int i = point[u]; i != -1; i = edge[i].nxt){
int v = edge[i].v;
if (!dfn[v]){
Tarjan(v);
low[u] = min(low[u], low[v]);
// 因为v在u的子树内,所以low[v]可以用于更新low[u]
if (low[v] >= dfn[u]){ // 子树中没有可以连到父亲上面的边
cut[u] = 1;
dccn++;
dcc[dccn].clear();
dcc[dccn].push_back(u); // u是割点,可能包含在多个点双中,不能弹出
while (1){
int w = stk[st];
dcc[dccn].push_back(w);
st--;
if (w == v){ // 做到子树全部弹出为止,不然v的兄弟也会被弹出
break;
}
}
}
}
else{
low[u] = min(low[u], dfn[v]);
// low表示u的子树中的非树边能到的最小的dfn值,所以不能和low[v]比较,详见下图
}
}
}
看这样一张图:(图中标出的数字是 dfn[u]
,对于节点 3,他会先找到 1 并更新 low[3]
然后再拓展 4 号节点)
如果
e
l
s
e
else
else时用
l
o
w
[
v
]
low[v]
low[v]更新
l
o
w
[
u
]
low[u]
low[u],这张图就是一整个点双了。然而并不是,3明显是一个割点。
割边和边双
void Tarjan(int u, int in_e)
{
dfn[u] = ++idx;
low[u] = u;
for (int i = point[u]; i != -1; i = edge[i].nxt){
if (i == in_e){
continue;
}
int v = edge[i].v;
if (!dfn[v]){
Tarjan(v, i^1);
low[u] = min(low[u], low[v]);
if (low[v] < dfn[u]){
cut_e[i] = cut_e[i^1] = 1;
}
}
else{
low[u] = min(low[u], dfn[v]);
// 这里写dfn[v]或者low[v]没什么关系,因为v能通过非树边到达的点u肯定也可以
// low[]甚至可以看成一个类似并查基集的结构
}
}
}
求边双只要求出割边之后 d f s dfs dfs一遍就行了。
强联通分量
p.s. 点双和边双是针对无向图而言,强联通分量是针对有向图而言。因为是有向图所以才存在前向边后向边的说法。
强连通分量的算法是找后向边和横插边构成的环。在栈中记录当前节点的祖先,和能到达当前节点祖先的点。如果有边指向栈中节点,那么就可以用栈中节点的 d f n dfn dfn更新当前节点的 l o w low low。
void Tarjan(int u)
{
dfn[u] = low[u] = ++idx;
stk[++st] = u;
instk[u] = 1;
for (int i = point[u]; i != -1; i = edge[i].nxt){
int v = edge[i].v;
if (!dfn[v]){
Tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (instk[v]){
low[u] = min(low[u], dfn[v]);
}
}
if (low[u] == dfn[u]){
// 如果u不能到达他的祖先,那么在回溯之后他就不满足存在栈中的条件了
cnum++;
while (1){
int v = stk[st--];
col[v] = cnum;
instk[v] = 0;
scc[cnum].push_back(v); // scc中记录每一个强联通分量
if (v == u){
break;
}
}
}
}
例题
一、poj 2942
Knights of the Round Table
点双+奇环判定
二、poj 3694
Network
边双,low[]的并查集用法