[复习] 图连通性

[复习] 图连通性

搜索生成树

定义(无向边方向是边第一次被遍历时所指的方向)

  • 树边,搜索到一个新的点连的边,构成生成树。
  • 返祖边,搜索到一个指向当前点到根的路径上的一个点的边。
  • 前向边,指向生成树子树内一个点的边。
  • 横叉边,其他边,指向兄弟子树。

有向图dfs生成树 以上四种边都有。

无向图dfs生成树 只有树边和返祖边。

无向图bfs生成树 只有树边和横叉边。

Tarjan 算法

SCC 强连通分量

在有向图中,两个点强连通当且仅当两点互相可达。

强连通分量是极大的满足每个点两两强联通的子图。

注意到在 \(dfs\) 树上,一个环只有可能由树边和返祖边构成。

定义 \(dfn\) 表示 \(dfs\) 序,\(low\) 表示经过树边和返祖边能到达的最小的 \(dfn\)

如果一个点的 \(dfn=low\) 那么它就是这个 SCC 中深度最小的点。

我们到达一个新的点,将它加入栈,之后只用栈中的 \(dfn\) 更新 \(low\)

同时当我们找到一个 \(dfn_x=low_x\) 时,此时栈中在 \(x\) 以上的点都和 \(x\) 在用一个 SCC 中。

vector<int> g[N];
int dfn[N],low[N],dfn1;
int stk[N],bz[N],top; // bz:是否在栈中
int scc[N],sc; // 所在 SCC 的标号
void dfs(int x){
	dfn[x]=low[x]=++dfn1,stk[++top]=x,bz[x]=1;
    for(int v:g[x]) 
        if(!dfn[v])dfs(v),low[x]=min(low[x],low[v]);
    	else if(bz[v])low[x]=min(low[x],dfn[v]);
    if(dfn[x]==low[x]){
        ++sc;
        while(bz[x])scc[stk[top]]=sc,bz[stk[top--]]=0;
    }
}

SCC 标号与拓扑序

SCC 的标号是 SCC 缩点后形成的 DAG 的拓扑序的逆序。

边双连通分量和割边

无向图中,分量内,删除任意一条边后,任意两个点可达。

连接分量的边叫做桥,或割边。

点的边双连通性具有传递性。

在无向图中,\(dfn\) 表示 \(dfs\) 序,\(low\) 表示不经过父边能到达的最小 \(dfn\)

因此我们需要在 \(dfs\) 中记录父亲。

如果树边 \((x,v)\) 满足 \(low_v>dfn_x\) 则这条边是桥。

事实上 \(v\) 也满足 \(dfn_v=low_v\),它是边双的根。当然 \(dfs\) 树的根也是边双的根,也满足 \(dfn=low\)

用栈记录分量内的点,发现除了更新 \(low\),其他和 SCC 的过程一样。

vector<int> g[N];
int dfn[N],low[N],dfn1;
int stk[N],top;
int num[N],num1;
//int cut[N]; // (x,fa_x) 是割边
void dfs(int x,int y){
    dfn[x]=low[x]=++dfn1,stk[++top]=x;
    for(int v:g[x]){
		if(!dfn[v]){
			dfs(v),low[x]=min(low[v],low[x]);
//            if(low[v]>dfn[x]) cut[v]=1;
        }
        else if(v!=y)low[x]=min(dfn[v],low[x]);
    }
    if(low[x]==dfn[x]){
        num[x]=++num1;
		while(stk[top]!=x)num[stk[top--]]=num1;
    	--top;
    }
}

点双连通分量和割点

无向图中,删掉一个点后,分量内任意点两点可达。

单独一个点没有边是一个点双,两个点一条边且没有包含两点的环的子图是点双。

割点:删掉后连通块数增加。

一个点可能属于多个点双,这个点是割点。

点的点双连通性不具有传递性,因为一个点可能属于多个点双。

但一条边一定只属于一个点双。

这里 \(dfn\)\(low\) 与边双的定义一样。

如果树边 \((x,v)\) 满足 \(low_v\ge dfn_x\) 那么 \(x\) 是点双的根,此时:

  • 如果 \(x\) 不是 \(dfs\) 树的根,那么 \(x\)割点
  • \(x\)\(dfs\) 树的根:
    1. 有两个及以上子树,它是割点,是每个子树点双的根。
    2. 有一个子树,它不是割点,它是这个子树点双的根。
    3. 它没有子树,它不是割点,自成点双。

即要多判断根的情况。

由于割的是当前点而不是边,所以加入点双应该在遍历一个新子树之前。

由于割点属于多个点双,考虑每个点双建一个新点,把新点连向点双中的所有点,即构建圆方树。

这里我们暂时不讨论单独一个点的情况。

vector<int> g[N]; // 原无向图的边
vector<int> h[N*2] // 圆方树
int dfn[N],low[N],dfn1;
int stk[N],top;
int root,tot; // tot:圆方树新点的编号
//int cut[N]; // 是否是割点
void add(int x,int y){
    h[x].push_back(y);
    h[y].push_back(x);
}
void dfs(int x,int y){
    dfn[x]=low[x]=++dfn1,stk[++top]=x;
    int du=0; // 子树个数
    if(x==root&&g[x].size()==0); // 这里我们不考虑单独一个点的情况
    for(int v:g[x]){
        if(!dfn[v]){
			dfs(v);
            low[x]=min(low[v],low[x]);
            if(low[v]>=dfn[x]){
//                if(++du>1||root!=x)cut[x]=1; // 求割点
				++tot;
                while(stk[top]!=v) add(stk[top--],tot);
                add(stk[top--],tot); // !!!!! 注意这里一定要弹到 v 而不是弹到 x !!!!!
                add(x,tot);
            }
        }
        else if(v!=y)low[x]=min(low[x],dfn[v]);
    }
}
int main(){
    make_praph();
    tot=n;
    for(int i=1;i<=n;i++)if(!dfn[i])root=i,dfs(i,0),top=0; // 栈中剩下树根可能需将 top 清空
}

解释为什么 \((x,v)\) 满足条件时栈要弹到 \(v\) 而不是 \(x\)

因为会出现在栈中 \(x\)\(v\) 之间有一些与 \(x\) 同一个点双的点,因为 \(x\) 点双的根节点比 \(x\) 还要前,于是 \(x\) 的点双还没计算,这些点也没弹出。

posted @ 2024-10-25 07:04  dengchengyu  阅读(5)  评论(0编辑  收藏  举报