Tarjan 学习笔记
这里讲一下
有向图的强连通分量
首先我们需要了解几个定义。(以下说法均针对有向图)
连通分量:在一个块中,任意两个点之间能够互相到达。即
强连通分量(简称
知道这两个概念之后,我们就来考虑如何求一张有向图中的所有强连通分量。这就需要
首先,
-
树枝边:
u 是v 的父亲节点。 -
前向边:
u 是v 的祖先节点。 -
后向边:
v 是u 的祖先节点。 -
横叉边:
v 是已经搜索过的子树上的一个点。
这四种边本质上没有什么用,有助于理解。
接下来定义两个时间戳(记录
根据时间戳的定义,我们可以发现这四种边的一些性质,证明显然:
-
树枝边:
dfn_{u}<dfn_{v} -
前向边:
dfn_{u}<dfn_{v} -
后向边:
dfn_{u}>dfn_{v} -
横叉边:
dfn_{u}>dfn_{v}
然后考虑如何判断强连通分量。假设我们要找的都是强连通分量的最上面的那一个点,那么当
为什么?大概感性理解一下。在有向图中当
如何维护两个时间戳呢,其实很简单。
最后的问题就在于如何将每一个强连通分量全部找出来。我们可以维护一个栈来实现。遍历每个点的时候立即进栈,当点
这样我们就找到了图中的每一个强连通分量。
而这种算法的最终目的一般是缩点,即把每一个强连通分量变成一个点(不一定需要重新建图)。这样原来的有向图就变成了一个有向无环图。而且还有一个很重要的性质,缩点后的点的编号的倒序一定是它的拓扑序,就不需要再写一遍拓扑排序了。这使我们做题方便了很多,比如所缩点后可以
Code
inline void Tarjan(int u)
{
dfn[u] = low[u] = ++timestamp;
stk.push(u),in_stack[u] = true;
for(int i = head[u]; ~ i;i = e[i].nxt)
{
int now = e[i].v;
if(!dfn[now]){Tarjan(now);low[u] = min(low[u],low[now]);}
else if(in_stack[now]) low[u] = min(low[u],dfn[now]);
}
if(dfn[u] == low[u])
{
++scc_cnt;int y;
do
{
y = stk.top();stk.pop();
id[y] = scc_cnt;
in_stack[y] = false;
}while(y != u);
}
}
练习题:
P3387 【模板】缩点
P1262 间谍网络
P4742 [Wind Festival] Running In The Sky
P2002 消息扩散
无向图的边双连通分量
在了解边双连通分量之前,先要引入一个桥(割边)的概念。
首先,桥指的是一条边。在一个无向图中,桥的定义是:如果删去该条边,则整个图会变得不连通。
边双连通分量(简称DCC):一个极大地不包含桥的连通块。这里极大是指,再添加图中的任意一个节点,该连通块中就会包含桥。
边双连通分量的
首先考虑如何判断桥,时间戳的定义不变。对于一条边
那如何找边双连通分量呢?当
用栈来维护,方法与有向图的强连通分量相似。
Code
inline void tarjan1(int u,int from)
{
dfn[u] = low[u] = ++timestamp;
s.push(u);
for(int i = head[u]; ~ i;i = e[i].nxt)
{
int now = e[i].v;
if(!dfn[now])
{
tarjan1(now,i),low[u] = min(low[u],low[now]);
if(dfn[u] < low[now]) is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if(i != (from ^ 1)) low[u] = min(low[u],low[now]);
}
if(dfn[u] == low[u])
{
++dcc_cnt;int y;
do
{
y = s.top();s.pop();
id[y] = dcc_cnt;
}while(y != u);
}
}
练习题:P8436 【模板】边双连通分量
无向图的点双连通分量
我感觉点双连通分量是这三个中间最难得一个了,中间还需要分类讨论,显得很繁琐。而且很不符合直觉。
首先引入一个割点的概念。割点表示的是一个点,对于一个无向图来说,割点指:如果删去该点,整个图会变得不连通。
点双连通分量(简称
先来考虑如何找割点。这里需要用到分类讨论:如果点
证明:如果点
接下来考虑如何找点双连通分量,对于一条边
最后有一点需要注意,点双连通分量需要在循环内维护,因为每个点可能对应多个点双连通分量。
和点双联通分量相关的知识叫圆方树,具体内容看我这篇博客。
还有一个很重要的性质要讲:即同一个点双中的两不同点
对割点的一个很神奇的理解:两个点双连通分量中唯一的交点,如果想从一个点双连通分量出去(到另外一个点双连通分量),必须通过割点
Code
inline void Tarjan(int u,int father)
{
dfn[u] = low[u] = ++timestamp;
s.push(u);int son = 0;
for(int i = head[u]; ~ i;i = e[i].nxt)
{
int now = e[i].v;
if(!dfn[now])
{
Tarjan(now,u);low[u] = min(low[u],low[now]);
son++;
if(dfn[u] <= low[now])
{
dcc_cnt++;int y;
do
{
y = s.top();s.pop();
dcc[dcc_cnt].push_back(y);
}while(y != now);
dcc[dcc_cnt].push_back(u);
}
}
else if(now != father) low[u] = min(low[u],dfn[now]);
}
if(u == root && son == 0) dcc[++dcc_cnt].push_back(u);
}
练习题:
P8435 【模板】点双连通分量
P3388 【模板】割点(割顶)