图的连通性学习笔记

定义

强联通分量:在有向图中任意两个节点连通的有向图(SCC)

点双连通分量:删除任意节点仍然连通(v-DCC)

边双连通分量:删除任意边仍然连通(e-DCC)

割点:删除这个点以后图的连通性会发生改变的点

:删除这条边后图的连通性会发生改变的边

Tarjan

Tarjan 算法可以求出上面的所有东西。

主要思路

将图转变为 dfs 树,然后再通过记录数组 low 来判定是否拥有不需要经过其祖先就能够到达祖先上方的图。

Tarjan求强联通分量

众所周知强联通分量内的点连通性都是一样的

因此在部分问题中可以用这种方式来将环缩成一个节点,方便处理

那么就用 Tarjan 达到求出图中每一个是强联通分量的子图

先建树,然后用dfs序的特点去判断

强联通分量中,如果所连接的点是祖先,那么又因为祖先一定连向这个点,所以这个点到祖先的这一段一定会形成强联通。

那么我们记录最接近祖先的点,如果能连接到的最大祖先就是这个点本身,那么就将这些点搞成一个强联通分量即可。

我们用栈记录即可

代码:

void dfs(int p){
	low[p]=dfn[p]=++cnt;
	st[++top]=p;
	in[p]=1;
	for(int i:e[p]){
		if(!dfn[i]){
			dfs(i);
			low[p]=min(low[p],low[i]);
		}
		else if(in[i])low[p]=min(low[p],dfn[i]);
	}
	if(dfn[p]==low[p]){
		id++;
		do{
			in[st[top]]=0;
            g[st[top]]=id;
            scc[id].push_back(st[top]);
			top--;
		}while(st[top+1]!=p);
	}
}		

Tarjan求割点

无向图建树只会有连向祖先的边和树边

对于一个割点,判断是否下面的子树在我这个点删除后会被孤立

我们还是记录下可以连接到的最大祖先在哪里,如果可以连接到的最大祖先比我们此时遍历到的祖先要深,那么这个点就可以成为割点

但是还有特例,就是根的情况。

如果根被删除,它的上面是没有其它的树去连接的,那么就不一定形成割点

所以根的情况必须有两个子树才满足

代码:

void tarjan(int p){
	dfn[p]=low[p]=++cnt;
	vis[p]=1;
	int sum=0;
	for(int i:e[p]){
		if(!dfn[i]){
			tarjan(i);
			low[p]=min(low[p],low[i]);
			if(low[i]>=dfn[p])sum++;
		}
		else low[p]=min(low[p],dfn[i]);
	}
	if(sum>=2||sum==1&&p!=rt)f[p]=1;
}

Tarjan求割边

首先所有的非树边一定不会是割边

所以只考虑树边

如果既然都只有树边了,那么和割点在辨析上没有太大区别

只需要在上一步的基础上只考虑边的情况即可

代码就不放了

Tarjan求点双连通分量

点双需要这个点删了以后仍然连通

因为无向图,所以点双一定还是在子树上

如果这个点子树最浅位置不在父亲祖先,那么此时的连通已经无法扩展,只要这个父亲不选择就会直接拆开,所以此时就可以直接建立强联通分量

需要注意的是要保留这个父节点在栈里

一个节点也是点双,如果根不在任何点双里,自己建一个点双就可以了

代码:

void tarjan(int p,int fa){
	vis[p]=1;
	dfn[p]=low[p]=++cnt;
	st[++top]=p;
	int son=0;
	for(int i:e[p]){
		if(!dfn[i]){
			tarjan(i,p);
			son++;
			low[p]=min(low[p],low[i]);
			if(low[i]>=dfn[p]){
				id++;
				do{
					ans[id].push_back(st[top]);
					top--;
				}while(st[top+1]!=i);
				ans[id].push_back(p);
			}
		}
		else if(i!=fa)low[p]=min(low[p],dfn[i]);
	}
	if(!son&&!fa)ans[++id].push_back(p);
}

Tarjan求边双

边双连通分量可能会有重边,对最后的结果产生影响

还是求最浅祖先,如果删除了这一条边不产生影响,那么就是边双了

还是看什么时候最浅等于dfs序

代码:

void tarjan(int p,int fa){
	vis[p]=1;
	dfn[p]=low[p]=++cnt;
	st[++top]=p;
	for(auto tmp:e[p]){
		int v=tmp.first,i=tmp.second;
		if(i==fa)continue;
		if(!dfn[v]){
			tarjan(v,i);
			low[p]=min(low[p],low[v]);
		}
		else low[p]=min(low[p],dfn[v]);
	}
	if(dfn[p]==low[p]){
		id++;
		do{
			ans[id].push_back(st[top]);
			top--;
		}while(st[top+1]!=p);
	}
}

常见解法

强连通分量缩点

由于强连通分量每一个点都可以互相到达,具有相同连通性,我们可以将所有强连通分量缩成一个点。

此时我们就得到了一个 有向无环图,可以直接用 拓扑排序 解决很多问题,并且时间复杂度仅仅是\(O(n)\) 非常高效

点/边 双连通分量缩点

解决一些删除某点或者边是否能够正常连通的问题

配合差分约束

如果形成一个环,那么就必定相等,直接 tarjan 缩点即可

配合染色

二分染色图中,若在一个强连通分量中存在一个奇数环,那么整个强联通分量都可以形成一个奇数环

统计不互相连通的数量

直接跑连通,得到的强连通分量数量即为答案

选择单向边方向

先跑边双,每一个边双都可以转化为一个强连通分量,在保证强连通分量越多越好的情况下,然后可以对有向无环图进行分析。

其它

欧拉回路

欧拉回路在无向图中需要满足每个点的边数为偶数且连通,有向图需要满足连通且出度入度相等。

然后直接dfs即可,每个dfs结束后加入即可,每次要删边

代码:

void dfs(int p){
	for(int &i=now[p];i<e[p].size();){
		i++;
		dfs(e[p][i-1]);
	}
	st[++top]=p;
}

仙人掌

任意一条边至多只出现在一条简单回路的无向连通图称为仙人掌。

问题求两个点之间距离

我们可以使用圆方树来解决

我们将一个环上的点连向一个方点,此时的距离就是根可到达的点到这个点的距离

维护完成,跑最近公共祖先,前缀和求距离

由于祖先可能是方点,需要得到进入方点的前一个圆点,并得到两个圆点的距离,通过环上前缀和。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=2e4+5;
int n,m,q,id,dfn[N],low[N],cnt,fa[N],faw[N],pre[N],sum[N],st[N][16],dep[N],dis[N],sa,sb;
vector<pair<int,int>> e[N],e2[N];
void build(int x,int y,int w){
	int s=w;
	for(int i=y;i!=x;i=fa[i]){
		pre[i]=s;
		s+=faw[i];
	}
	sum[x]=pre[x]=s;
	e2[x].push_back({++id,0});
	for(int i=y;i!=x;i=fa[i]){
		sum[i]=s;
		e2[id].push_back({i,min(pre[i],s-pre[i])});
	}
}
void tarjan(int p,int f){
	dfn[p]=low[p]=++cnt;
	for(auto tmp:e[p]){
		int i=tmp.first,w=tmp.second;
		if(!dfn[i]){
			fa[i]=p,faw[i]=w;
			tarjan(i,p);
			low[p]=min(low[p],low[i]);
			if(low[i]>dfn[p])e2[p].push_back({i,w});
		}
		else if(i!=f)low[p]=min(low[p],dfn[i]);
	}
	for(auto tmp:e[p]){
		int i=tmp.first,w=tmp.second;
		if(fa[i]!=p&&dfn[i]>dfn[p])build(p,i,w);
	}
}
void dfs(int p,int f){
	dep[p]=dep[f]+1;
	st[p][0]=f;
	for(int i=1;i<=15;i++)st[p][i]=st[st[p][i-1]][i-1];
	for(auto tmp:e2[p]){
		int v=tmp.first,w=tmp.second;
		if(v==f)continue;
		dis[v]=dis[p]+w;
		dfs(v,p);
	}
}
int LCA(int l,int r){
	if(dep[l]<dep[r])swap(l,r);
	for(int i=15;i>=0;i--)if(dep[st[l][i]]>=dep[r])l=st[l][i];
	if(l==r)return l;
	for(int i=15;i>=0;i--){
		if(st[l][i]!=st[r][i]){
			l=st[l][i];
			r=st[r][i];
		}
	}
	sa=l,sb=r;
	return st[l][0];
}
signed main(){
	cin>>n>>m>>q;
	for(int i=1,u,v,w;i<=m;i++){
		cin>>u>>v>>w;
		e[u].push_back({v,w});
		e[v].push_back({u,w});
	}
	id=n;
	tarjan(1,0);
	dfs(1,0);
	for(int i=1,a,b;i<=q;i++){
		cin>>a>>b;
		int lca=LCA(a,b);
		if(lca<=n)cout<<dis[a]+dis[b]-2*dis[lca]<<endl;
		else cout<<dis[a]-dis[sa]+dis[b]-dis[sb]+min(abs(pre[sa]-pre[sb]),sum[sa]-abs(pre[sa]-pre[sb]))<<endl;
	}
	return 0;
}
posted @ 2025-11-28 09:51  huhangqi  阅读(0)  评论(0)    收藏  举报
/*
*/