连通分量tarjan学习笔记
强连通分量
定义
有向图 \(G\) 强连通是指,在 \(G\) 内任意两点都可以互相到达。
强连通分量是指极大的强连通子图。
tarjan求强连通分量
dfs生成树
如上图,主要有 \(4\) 种边:
- 树边(图中的黑色边):从一个已访问节点遍历到一个未访问节点时就形成了一个树边。
- 返祖边(图中的红色边):也被叫做回边,指向祖先。
- 横插边(图中的蓝色色边):搜索时遇到一个一访问的节点,但这个节点并不是当前结点的祖先。
- 前向边(图中的绿色边):搜索时遇到子树中的节点形成的。
tarjan
考虑强连通分量与dfs生成树的关系:如果 \(u\) 是一个强连通分量在dfs生成树上的根节点,那么这个强连通分量的其他节点一定在它的子树内。
反证法:如果节点 \(v\) 不在 \(u\) 的子树内(二者在同一强连通分量内),那么必定有一条离开 \(u\) 的子树的边指向 \(v\),而这条边一定会被遍历到,这与定义不服,证毕。
tarjan维护了以下几个值:
- \(dfn_u\):深度优先遍历时节点 \(u\) 的次序。
- \(low_u\):\(u\) 的子树和 \(u\) 的子树通过一条不在dfs生成树上的边能到达的结点的 \(dfn\) 的最小值。
一个结点的子树内结点的 dfn 都大于该结点的 \(dfn\)。
从根开始的一条路径上的 \(dfn\) 严格递增,\(low\) 严格非降。
考虑 \(low\) 的更新方式:
- \(v\) 未被访问过:先深度优先遍历,然后直接拿 \(low_v\) 去更新就好了(\(v\) 的子树在计算 \(low_v\) 时已经算好了)。
- \(v\) 被访问过,还在栈里:拿 \(dfn_v\) 去更新。
- \(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
需要先学割点,但是不会并不影响理解。
先给出两个性质:
- 两个点双最多只有一个公共点,且一定是割点。
- 对于一个点双,它在dfs搜索树中 \(dfn\) 值最小的点一定是割点或者树根。
我们根据第二个性质分类讨论:
- 当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。
- 当这个点为树根时:
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]]++;
}
}

浙公网安备 33010602011771号