图的联通性相关

强连通分量

对于有向图 \(G(V,E)\)。如果对于任意 \(\{u,v\}\in V\)\(u\)\(v\) 均可以互相到达。则称这个有向图是强连通。

对于一张有向图。其最大的强连通子图被称为强联通分量。简称 SCC。

算法讲解

强连通分量一般使用 tarjan 算法求解。

对于图上连通性的问题,如果直接在图上做会比较困难,我们可以将其转换为 DFS 树上的问题。

DFS 树

如果我们从一个联通(弱联通)图上的某个点出发,最后必然会访问完所有点,并且任意相邻的点只会通过一条边访问。访问的点和边构成了这个图的 DFS 树。

有向图 DFS 树上有四种边,可以自行在 oi-wiki 上查看。

tarjan 算法

我们另 \(S(x)\) 表示 DFS 树中 \(x\) 子树的部分。

显然 \(x\) 可以到达任意 \(y\in S(x)\),且 \(x\to y\) 的路径上所有的点都可以被 \(x\) 到达,都可以到达 \(y\)。此时,如果存在一条返租边 \((y,x)\)。则 \(x\to y\) 的路径上所有的点及其边构成的子图强连通。

现在的问题是如何保证最大性,即强连通子图最大。我们选取一个基准,即一个强连通分量中一个在 DFS 树内时间戳最小的点的时间戳,为了方便,记节点 \(x\) 的时间戳为 \(dfn_x\)

对于一个点 \(y\)。如果 \(x\)\(y\) 所在强连通分量内时间戳最小的点。必然满足以下条件

  • 在 DFS 树中,\(x\)\(y\) 的祖先。

  • \(y\) 可以到达 \(x\)

  • 不存在 \(z\)\(z\)\(y\) 的祖先,\(y\) 能到达 \(z\)\(dfn_z>dfn_x\)

为了方便,我们定义节点 \(y\)最小追溯值\(dfn_x\),写为 \(low_y=dfn_x\),特殊的,存在 \(low_u=dfn_u\),节点 \(u\) 就是上文所说的 \(x\)

显然,如果求出了 \(low\) 数组,所有 \(low\) 值相等的节点构成一个强连通分量。

代码实现

显然,根据算法思想,可以如下求 \(low\) 值。

\[low_u=\min\limits_{(u,v)\in E}low_v \]

初始定义

\[low_u=dfn_u \]

因为时间戳的先后性,满足 \(low_x=dfn_x\) 中的 \(x\) 所在强连通分量的节点一定在 \(S(x)\) 中,不过 \(S(x)\) 中的节点不一定在强连通分量内,其可能独属于一个其他强连通分量。

我们维护一个栈,一旦遇到满足 \(dfn_u=low_u\)。就把栈中节点不断弹出直到 \(u\) 被弹出。这样在求 \(dfn_x=low_x\) 时,属于其他强连通分量的节点都弹了出去,就可以保证答案的正确性。

如果当前遍历到 \(x\)。显然 \(x\) 的祖先都在栈中,可以同时维护 \(x\) 的祖先。

void tarjan(int u){
	dfn[u]=low[u]=++tot;
	stk[++top]=u,mk[u]=1;
	for(int i=ver[u];i;i=nxt[i]){
		int v=to[i];
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}else if(mk[v])low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		scc++;
		while(stk[top+1]!=u){
			mk[stk[top]]=0;
			f[stk[top]]=scc;
			top--;
		}
	}
	return;
}

注意到这里用到了 low[u]=min(low[u],dfn[v]) 。巧妙的借助了代码的特性,这并不影响算法的正确性。

SCC 缩点

「缩点」。就是把一个强连通分量看成一个点,连接两个联通分量之间的有向边看成一条边,应为强连通分量的极大性,可以得到结论

两个强连通分量不可能存在于一个环中。

换句话说

缩点后的新图一定是 DAG。

来点例题?

例1.1:[USACO03FALL / HAOI2006] 受欢迎的牛 G

如果我们对原图缩点,一个强连通分量内所有的牛都是互相崇拜的。能当明星的牛要么是一头,要么是一个强连通分量内。否则无解。

统计度数,存在出度的点中都不可能成为明星,如果存在多个点没有出度,则无解,因为必须满足所有人都“爱慕”

例1.2:[USACO5.3] 校园网Network of Schools

缩点后一定会形成若干个 DAG。如果一个点有入度,只需满足指向他的点有软件就行。可以如果一个点没有入度,你就必须下载一个,\(0\) 入度点数就是第一问答案。

假设有 \(p\)\(0\) 入度点,\(q\)\(0\) 出度点,第二问要求我们最少加几条边使原图缩成一个 SCC 。我们先把所有点入度和出度变为非零零。

  • \(p\le q\):先将 \(q\)\(0\) 出度点向 \(q\)\(0\) 入度点连线,在将剩下 \(p-q\)\(0\) 出度点向第 \(q\)\(0\) 入度点连线。连了 \(p\) 条边。

  • \(p<q\):先将 \(p\)\(0\) 出度点向 \(p\)\(0\) 入度点连线,在将第 \(p\)\(0\) 出度点向剩下 \(p-q\)\(0\) 入度点连线。连了 \(q\) 条边。

综上,答案为 \(\max(p,q)\)

例1.3:【模板】缩点

由于缩点后是 DAG。可以跑最长路求解

\[dp_v=\max\limits_{(u,v)\in E}dp_u+w_v \]

\(w_v\) 表示 \(v\) 所在强连通分量内点的权值和。

点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int N=1e4+10,M=1e5+10;
int ver[N],nxt[M],to[M],idx,n,m,w[N],u[M],v[M],ww[N],ans=-1;
int dfn[N],low[N],stk[N],mk[N],from[N],top,d[N],dp[N],scc,tot;
queue<int>q;
void add(int x,int y){
    to[++idx]=y,nxt[idx]=ver[x],ver[x]=idx;
}
void tarjan(int u){
    dfn[u]=low[u]=++tot;
    stk[++top]=u,mk[u]=1;
    for(int i=ver[u];i;i=nxt[i]){
        int tp=to[i];
        if(!dfn[tp]){
            tarjan(tp);
            low[u]=min(low[u],low[tp]);
        }else if(mk[tp])low[u]=min(low[u],dfn[tp]);
    }
    if(low[u]==dfn[u]){
        scc++;int v,cnt=0;
        while(stk[top+1]!=u){
            v=stk[top--];
            mk[v]=0;
            from[v]=scc;
            cnt+=w[v];
        }
        ww[scc]=cnt;
    }
    return;
}
void topsort(){
    for(int i=1;i<=scc;i++){
        if(!d[i])q.push(i);
        dp[i]=ww[i];
    }
    while(q.size()){
        int t=q.front();q.pop();
        for(int i=ver[t];i;i=nxt[i]){
            int tp=to[i];
            d[tp]--;
            dp[tp]=max(dp[tp],dp[t]+ww[tp]);
            if(!d[tp])q.push(tp);
        }
    }
    return;
}
int main(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",w+i);
    for(int i=1;i<=m;i++){
        scanf("%d %d",u+i,v+i);
        add(u[i],v[i]);
    }
    for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
    idx=0;
    memset(ver,0,sizeof(ver));
    memset(nxt,0,sizeof(nxt));
    memset(to,0,sizeof(to));
    for(int i=1;i<=m;i++){
        if(from[u[i]]!=from[v[i]])add(from[u[i]],from[v[i]]),d[from[v[i]]]++;
    }
    topsort();
    for(int i=1;i<=scc;i++){
        ans=max(ans,dp[i]);
    }
    printf("%d\n",ans);
    return 0;
}

双连通分量

割边

无向图的 DFS 树上仅仅存在两种边,分别是树边和非树边。

割边定义:对于无向图 \(G(V,E)\)。如果删掉某一条边后图的连通分量增加,则称该边为割边。

显然,非树边不会是割边,因为任意非树边连接的两点 \(u,v\) 之间必然被若干树边连接。

定义 \(Son(x)\)\(x\) 在 DFS 树中子节点点集。因为时间戳的定义和祖后代的特性。如果 \(y\in Son(x)\),且从 \(y\) 出发无法到达 \(Son(y)\) 以外的部分,删去 \((x,y)\)\(y\) 将“失联”。则 \((x,y)\) 为割边。

换句话说,如果 \(y\) 所能到达的时间戳最小节点为 \(x\)。则记 \(low_y=dfn_x\)。显然,\(low_u\) 的求解方法如下。

\[low_u=\min\limits_{v\in Son(u)}low_v \]

如果边 \((x,y)\) 是割边,显然

\[low_y<dfn_x \]

前提是 \(y\in Son(x)\)

割边还有一个重要的易错点,就是 DFS 树中子节点的 \(low\) 不能从父节点获取,不过有的题目中会出现重边,一个很好的判断方法是记录边的编号,编号为 \(i\) 的边和编号为 \(i\oplus 1\) 的边其实是一条边,注意在建图时边的编号从 \(2\) 开始标记。

边双连通分量

如果一个无向连通图不具备割边,则称该图为边双连通图。一张无向图的最大联通边双连通图成为该图的边双联通分量,简称 e-DCC。

求法很简单,求出割边后标记,DFS 划分联通分量即可。

void tarjan(int u,int edge){
    dfn[u]=low[u]=++tot;
    for(int i=ver[u];i;i=nxt[i]){
        int v=to[i];
        if(!dfn[v]){
            tarjan(v,i);
            low[u]=min(low[u],low[v]);
            if(dfn[u]<low[v])bridge[i]=bridge[i^1]=1;
        }else if(i!=(edge^1))low[u]=min(low[u],dfn[v]);
    }
    return;
}
void dfs(int x){
    v[dcc].push_back(x),mk[x]=1;
    for(int i=ver[x];i;i=nxt[i]){
        if(mk[to[i]]||bridge[i])continue;
        dfs(to[i]);
    }
    return;
}
posted @ 2024-08-19 11:42  zuoqingyuan111  阅读(24)  评论(0编辑  收藏  举报