无向图的双连通分量

无向图的双连通分量

标签(空格分隔): 学术

艹!我一直以为我写了关于割边和边双联通分量的笔记,结果没写。

一、 有关割点即桥的相关概念:

割边(桥):
割点:

很多人认为二者所具有的关系:

有割点并不一定有桥,有桥一定有割点,割点一定是桥所依附的边

但是显然这是个 假理论

如下图所示,$ (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\) 算法提高课

posted @ 2022-06-08 16:02  Simon_...sun  阅读(37)  评论(0)    收藏  举报