连通分量tarjan学习笔记

强连通分量

定义

有向图 \(G\) 强连通是指,在 \(G\) 内任意两点都可以互相到达。
强连通分量是指极大的强连通子图。

tarjan求强连通分量

dfs生成树


如上图,主要有 \(4\) 种边:

  1. 树边(图中的黑色边):从一个已访问节点遍历到一个未访问节点时就形成了一个树边。
  2. 返祖边(图中的红色边):也被叫做回边,指向祖先。
  3. 横插边(图中的蓝色色边):搜索时遇到一个一访问的节点,但这个节点并不是当前结点的祖先。
  4. 前向边(图中的绿色边):搜索时遇到子树中的节点形成的。

tarjan

考虑强连通分量与dfs生成树的关系:如果 \(u\) 是一个强连通分量在dfs生成树上的根节点,那么这个强连通分量的其他节点一定在它的子树内。
反证法:如果节点 \(v\) 不在 \(u\) 的子树内(二者在同一强连通分量内),那么必定有一条离开 \(u\) 的子树的边指向 \(v\),而这条边一定会被遍历到,这与定义不服,证毕。
tarjan维护了以下几个值:

  1. \(dfn_u\):深度优先遍历时节点 \(u\) 的次序。
  2. \(low_u\)\(u\) 的子树和 \(u\) 的子树通过一条不在dfs生成树上的边能到达的结点的 \(dfn\) 的最小值。

一个结点的子树内结点的 dfn 都大于该结点的 \(dfn\)
从根开始的一条路径上的 \(dfn\) 严格递增,\(low\) 严格非降。
考虑 \(low\) 的更新方式:

  1. \(v\) 未被访问过:先深度优先遍历,然后直接拿 \(low_v\) 去更新就好了(\(v\) 的子树在计算 \(low_v\) 时已经算好了)。
  2. \(v\) 被访问过,还在栈里:拿 \(dfn_v\) 去更新。
  3. \(v\) 被访问过,不在栈里:说明已经操作完了,不需要用 \(v\) 去更新。

很容易想到,如果 \(dfn_u = low_u\),那么 \(u\) 就是这个强连通分量内的第一个节点。

点击查看代码
void tarjan(int u){
	low[u] = dfn[u] = ++dfncnt , s[++tp] = u , in_stack[u] = 1;
	for(int i=0;i<g[u].size();i++){
	    int v = g[u][i];
	    if(!dfn[v]){
	    	tarjan(v);
	    	low[u] = min(low[u],low[v]);
	    }else if(in_stack[v]){
	    	low[u] = min(low[u],dfn[v]);
	    }
	}
	if(dfn[u]==low[u]){
		++sc;
		do{
			color[s[tp]] = sc;
			sz[sc]++;
			in_stack[s[tp]] = 0;
		}while(s[tp--]!=u);
	}
}

双连通分量

这里只讨论无向图。

边双连通分量

在一张无向的连通图中,无论删去哪条边(只能删一条)都不能使图不联通,那么这张图就是一个边双联通。极大的边双连通子图被称为边双连通分量。
边双联通具有传递性,即 \(x\)\(y\) 边双联通,\(y\)\(z\) 边双联通,则 \(x\)\(z\) 边双联通。

tarjan1

我们先总结出一个重要的性质,在无向图中,dfs生成树上的边不是树边就只有非树边。
我们联系一下求强连通分量的方法,在无向图中只要一个分量没有桥,那么在dfs生成树上,它的所有点都在同一个强连通分量中。反过来,在dfs生成树上的一个强连通分量,在原无向图中是边双连通分量。
可以发现,求边双连通分量的过程实际上就是求强连通分量的过程。
时间复杂度 \(𝑂(𝑛+𝑚)\)
桥的定义下面会讲到。

点击查看代码
//稍有变动,可判重边
void tarjan(int u , int lst){
	low[u] = dfn[u] = ++dfncnt , s[++tp] = u , in_stack[u] = 1;
	for(pair<int,int> v:vec[u]){
	    if(v.second!=(lst^1)&&!dfn[v.first]){
	    	tarjan(v.first,v.second);
	    	low[u] = min(low[u],low[v.first]);
	    }else if(v.second!=(lst^1)){
	    	low[u] = min(low[u],dfn[v.first]);
	    }
	}
	if(dfn[u]==low[u]){
		++sc;
		do{
			sz[sc].push_back(s[tp]);
			in_stack[s[tp]] = 0;
		}while(s[tp--]!=u);
	}
}
//加边
for(int i=1;i<=m;i++){
	int u , v;cin >> u >> v;
	vec[u].push_back({v,i<<1}) , vec[v].push_back({u,i<<1|1});
}

tarjan2

因为边双连通分量内不能有桥,所以直接忽视掉桥,剩下的一个连通块就是一个边双连通分量。
所以我们可以先求出所有的桥再dfs一下。

对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。严谨来说,就是:假设有连通图 \(𝐺 ={𝑉,𝐸}\)\(e\) 是其中一条边(即 \(e\in E\)),如果 \(G-e\) 是不连通的,则边 \(e\) 是图 \(𝐺\) 的一条割边(桥)。

如果 \(low_v=dfn_u\) 表示还可以回到父节点,如果顶点 \(v\) 不能回到祖先也没有另外一条回到父亲的路,即 \(low_v>dfn_u\),那么 \(u-v\) 这条边就是割边。
如果有重边的话,那么这两条边都不是桥,改为记录哪些边走过而不是哪些点走过就可以了。过程和tarjan求强连通分量类似。

点双连通分量

在一张无向的连通图中,无论删去哪个点(只能删一个)都不能使图不联通,那么这张图就是一个点双联通。极大的点双连通子图被称为点双连通分量。
点双联通具有传递性,如下图,\(A,B\) 点双联通,\(B,c\) 点双联通,但是 \(A,C\) 点双联通。

tarjan

需要先学割点,但是不会并不影响理解。
先给出两个性质:

  1. 两个点双最多只有一个公共点,且一定是割点。
  2. 对于一个点双,它在dfs搜索树中 \(dfn\) 值最小的点一定是割点或者树根。

我们根据第二个性质分类讨论:

  1. 当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。
  2. 当这个点为树根时:
    a. 有两个及以上子树,它是一个割点。
    b. 只有一个子树,它是一个点双连通分量的根。
    c. 它没有子树,视作一个点双。
点击查看代码
void tarjan(int u , int fa){
	low[u] = dfn[u] = ++idx , s[++tp] = u;
	int child = 0;
	for(int v:vec[u]){
		if(v==fa)continue;
		if(!dfn[v]){
			child++;
			tarjan(v,u) , low[u] = min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				sc++;
				do{
					ans[sc].push_back(s[tp--]);
				}while(s[tp+1]!=v);
				ans[sc].push_back(u);
			}
		}else low[u] = min(low[u],dfn[v]);
	}
	if(child==0&&fa==0)ans[++sc].push_back(u);
}

割点

对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。

对于一个顶点 \(u\),如果存在一个顶点 \(v\) 使得 \(low_v \ge dfn_u\),即不能回到祖先,那么 \(u\) 即为割点。
这种情况不适用于根节点,如果是根节点必须有两个以上的儿子才能算作割点。

点击查看代码
void tarjan(int u , int fa){
	dfn[u] = low[u] = ++idx , vis[u] = true;
	int child = 0;
	for(int v:edge[u]){
		if(!vis[v]){
			child++;
			tarjan(v,u);
			low[u] = min(low[u],low[v]);
			if(fa!=u&&low[v]>=dfn[u]&&!flag[u]){
				flag[u] = true , res++;
			}
		}else if(v!=fa){
			low[u] = min(low[u],dfn[v]);
		}
	}
	if(fa==u&&child>=2&&!flag[u]){
		flag[u] = true , res++;
	}
}

缩点

缩点指的是,将处于一个连通分量(强连通分量,双连通分量都可以)内的所有节点看成一个点。
具体操作很简单,对于任意一条边,如果这条边的端点处于同一个连通分量内就无视这条边,否则将两个端点的编号改为两个端点所在的连通分量的编号就好了。

点击查看代码
for(int i=1;i<=m;i++){
	int u = edge[i].u , v = edge[i].v;
	if(color[u]!=color[v]){
		vec[color[u]].push_back(color[v]) , d[color[v]]++;
	}
}
posted @ 2025-10-08 17:08  虚空远行者  阅读(9)  评论(0)    收藏  举报