无向图的双连通分量
无向图的双连通分量
标签(空格分隔): 学术
艹!我一直以为我写了关于割边和边双联通分量的笔记,结果没写。
一、 有关割点即桥的相关概念:
割边(桥):
割点:

很多人认为二者所具有的关系:
有割点并不一定有桥,有桥一定有割点,割点一定是桥所依附的边
但是显然这是个 假理论
如下图所示,$ (1 ,2)$ 是一条割边,但是图中并不存在割点。

两者之间没有任何关系!!!
二、
求割点:
一点 \(u\) 满足 \(dfn_u \leq low_v\) 当且仅当满足以下条件之一 时,该点为割点
- 当前点 \(u\) 不是根节点
- 当前点 \(u\) 是根节点,并且至少有两个子节点
code
void tarjan(int u )
{
dfn[u] = low[u] = ++bian ;
int CCnt = 0 ; //该点属于几个点联通分量
for(int i = head[u] ; i ; i = ed[i].nxt )
{
int v = ed[i].to ;
if(! dfn[v] )
{
tarjan(v ) ;
low[u] = min(low[u] , low[v]) ;
if(dfn[u] <= low[v] )
{
CCnt++ ;
if(u != root || CCnt > 1 ) pd[u] = true ; // 判断是否是割点
}
}
else low[u] = min(low[u] , dfn[v]) ;
}
}
求割边:
“无向边 \((x , y )\) 是桥,当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\) ,满足 \(dfn_x < low_y\) ”
——李煜东《算法进阶指南》
注意:在进行 \(Tarjan\) 算法时需要传入当前边的编号,以解决重边问题
code
void tarjan(int u , int b)
{
dfn[u] = low[u] = ++tot ;
q.push_back(u) ;
for(int i = head[u] ; i ; i = ed[i].nxt )
{
int v = ed[i].to ;
if(! dfn[v] )
{
tarjan(v , i) ;
low[u] = min(low[u] , low[v]) ;
if(dfn[u] < low[v])
pd[i] = pd[i ^ 1] = true ; //下文有解释
}
else if(i != (b ^ 1 ))
low[u] = min(dfn[v] , low[u]) ;
}
}
可能同学们已经发现,这里我在标记割边时,利用了pd[i] = pd[i ^ 1] = true 的方式来同时标记反向边。这点在我的另一篇博客中,有详细解释。 这里简单引用一句话来解释
“当边的编号从 0 开始时,我们的编号 [0 , 1] , [2 , 3] , [4 , 5] …… 是一条边的正反向路径,显然在已知一边的编号为 \(k\) 那反向边即为 \(k \bigoplus 1\)。并且奇数为反向边,偶数为正向边。”
这种方法,常用于图论题中,可以当作小技巧来记住 ^ _ ^
三、点联通分量(V-DCC)
注意:必须保证图连通
孤立的点自成一个点双联通点,在除了孤立的点外的点双联通分量里一定存在割点,统计点双联通分量是基于求割点的。
用队列存储该点联通分量中的点,找到一个割点后,进行弹栈储存操作,特别注意,在弹栈的时候的终止条件是 \(q.top() != v\) ,最后将 \(v\) 和 $ u$ 加入队列中
code
void tarjan(int u )
{
dfn[u] = low[u] = ++bian ;
q.push_back(u) ;
if(root == u && head[u] == 0) // 孤立的点
{
++tot_d ;
dis[tot_d].push_back(u) ;
return ;
}
int CCnt = 0 ; //该点属于几个点联通分量
for(int i = head[u] ; i ; i = ed[i].nxt )
{
int v = ed[i].to ;
if(! dfn[v] )
{
tarjan(v ) ;
low[u] = min(low[u] , low[v]) ;
if(dfn[u] <= low[v] )
{
CCnt++ ;
if(u != root || CCnt > 1 ) pd[u] = true ; // 判断是否是割点
dis[++tot_d].push_back(v) ;
while(q.back() != v )
{
int now = q.back() ; q.pop_back() ;
dis[tot_d].push_back(now ) ;
}
q.pop_back() ;
dis[tot_d].push_back(u ) ;
}
}
else low[u] = min(low[u] , dfn[v]) ;
}
}
关于点双联通分量的缩点
不同于同边连通分量和普通的缩点操作,点联通分量缩点过程中割点是单独作为一个节点的。
总体思想是将一个点联通分量里除割点外的其他点集缩成一个点,用一条无向边与割点相连。
由此可见,一个割点的度数至少是 2。 除孤立点外的v-DCC大小至少是 \(2\)。
code
//写在主函数里
num_d = tot_d ;
for(int i = 1 ; i <= n ; i++ )
if(pd[i] ) id[i] = ++num_d ;
for(int i = 1 ; i <= tot_d ; i++ )
{
for(aotu j : dis[i] )
{
if(pd[j] ) add_c(i , id[j]) , add(id[j] , i) ;
else id_d[j] = i ;//该点所在的缩点后的“点”的编号
}
}
四、边双联通分量 (e-DCC)
根据桥的定义,每条桥边连接两个边双联通分量。求解边双联通分量是以求桥边为基础的。
步骤如下,先用 \(Tarjan\) 算法求出所有的桥边,打上标记。通过 \(dfs\) 遍历整张图,划分出每个边双联通分量。值得注意的是,在遍历的过程中不访问桥边。
code
void dfs(int u )
{
id_d[u] = ++tot_b ;
for(int i = head[u] ; i ; i = ed[i].nxt )
{
int v = ed[i].to ;
if(id_b[i] || pd[i]) continue ;
dfs(v ) ;
}
}
for(int i = 1 ; i <= n ; i++ )
{
if(! id_b[i] ) dfs(i ) ;
}
关于边双联通分量的缩点
不同于 v-DCC 繁琐的缩点方式,e-DCC 缩点方式类似有向图缩点。即将每个 e-DCC 收缩成一个点,每个点之间以桥边连接。不难发现一张无向连通图缩点会变成一颗树,非连通图则会变成森林。这样就从图上问题转化成了树上问题。
code
for(int i = 1 ; i <= n ; i++ )
{
if(! id_b[i] ) dfs(i ) ;
}
for(int i = 0 ; i < cnt ; i++ )
{
int u = ed[i ^ 1].to , v = ed[i].to ;
if(id_b[u] == id_b[v] ) continue ;
add(id_b[u] , id_b[v] ) ;
}
参考文献:
《算法进阶指南》李煜东著
OI Wiki 《图论》部分
\(AcWing\) 算法提高课

浙公网安备 33010602011771号