连通性相关

基础部分

DFS生成树

在有向图中,DFS生成树有4种边:

  1. 树边:每次搜索找到一个还未访问过的节点时就形成了一条树边。

  2. 返祖边:搜索时遇到在树上的祖先节点,指向祖先的边。

  3. 横叉边:搜索时遇到已访问过的节点,但该节点不是当前节点的祖先,就形成了一条横叉边。

  4. 前向边:搜索时遇到了子树内的点,形成一条前向边。

无向图中除了树边就是非树边(同时也是返祖边)。

性质

显然每条返祖边对应着一个环,但注意不是一一对应

极其有用的一个性质是:无向图中,DFS生成树上的返祖边构成的环是原图的环空间的一组基。这意味着DFS生成树上的返祖边构成的环异或起来属于原图中的环的集合的子集构成的簇。

很多神秘的东西发现对应不到其它的算法时,就上最纯粹的DFS生成树的力量吧。

强连通分量

这是定义在有向图上的。

强连通分量(Strongly Connected Components,SCC):极大的强连通子图。

求强连通分量,常用Tarjan算法。(求各种连通分量都常用Tarjan算法)

在DFS生成树上

如果一个节点u是它所在强连通分量中在搜索树上被搜到的第一个节点,那么该强连通分量的其他点一定在u在搜索树上的子树内。

证:

反证法。若一个点v属于该强连通分量但不在u的子树内,由于强连通,u一定有到达v的路径。由于v不在u的子树内,uv的路径上一定有离开u的子树的边,即横叉边或返祖边。然而这两种边都要求指向的节点已经被访问过了,这于u最先被访问矛盾。得证。

Tarjan算法求强连通分量

用一个栈保存当前已经搜到过并且还未处理所在强连通分量的节点。

对每个节点u维护:

  • dfnu:表示u的DFS序。

  • lowu:表示u的子树内能够回溯到的最早的在栈中的节点。lowu的值定义为以下节点的dfn的最小值:Subtreeu中的节点,从Subtreeu中通过一条非树边可以到达的节点。

显然有些性质:

  • u的子树内的节点的dfn都大于dfnu

  • 从根开始的一条路径上,dfn单调递增,low单调不降。

在搜索过程中,对于u和相邻的v,考虑三种情况:

  1. v未被访问过,则继续搜索v,并用lowv更新lowu。这是因为存在从uv的直接路径,从v的子树内能回溯到的节点,在u的子树内也能回溯到。

  2. v被访问过,在栈中。根据lowu的定义,用dfnv更新lowu

  3. v被访问过,不在栈中。说明v所在强连通分量已经处理好了,v已搜索完毕。不用操作。

在一个强连通分量中,有且仅有一个节点的dfn=low,这个节点一定是它所在强连通分量中第一个被访问到的,因为其dfnlow最小,不会被同一强连通分量中的其他点影响。

因此,在回溯时,如果对于当前节点udfnu=lowu,则栈中u及其上方的点构成SCC。

板子
void tarjan(int u){
	stk[++tp]=u;
	dfn[u]=low[u]=++dfc;
	instk[u]=true;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(instk[v]) low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		num++;
		while(stk[tp]!=u){
			bel[stk[tp]]=num;
			instk[stk[tp]]=false;
			tp--;
		}
		bel[stk[tp]]=num;
		instk[stk[tp]]=false;
		tp--;
	}
}

时间复杂度O(n+m)

遍历每个点,如果当前点u还未被遍历过,就清空栈,搜索u。这一步是防止有些点不能一次遍历到。

SCC缩点

强连通分量可以进行缩点,缩点后形成一张DAG。在这张DAG上可以进行更多操作。

双连通分量

板子多,尽量理解记忆。

前置

有向图中的割点与桥

校测里出现的神秘题目。

先强连通分量缩点转DAG,然后就是DAG上的割点和桥。

还是用DFS生成树分析。咕咕咕。

感觉是现在的自己不可做的。

也有可能本身不可做(?)

割点

定义在无向图上。

简化的定义:一张图G若在删去点u后不再连通,则称uG的割点。

求割点,常用Tarjan算法。

  • dfnuu的时间戳。

  • lowuu不经过其父亲能到达的最小时间戳。但是要用其父亲的时间戳更新lowu

u不是搜索的起始点,则若vsonu,lowvdfnuu为割点。

urt,则若儿子个数ch2u为割点。

板子
void tarjan(int u){
	dfn[u]=low[u]=++dfc;
	int ch=0;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(!dfn[v]){
			ch++;
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]&&u!=rt) ans+=!iscut[u],iscut[u]=true;
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(ch>=2&&u==rt) ans+=!iscut[u],iscut[u]=true;
}

定义在无向图上。

简化的定义:一张图G若在删去边e后不再连通,则称eG的桥。

求桥,常用Tarjan算法。

其实与割点差不多,改动一点即可:

  1. 桥与是否为根节点无关,所以不用特别记录。

  2. 割点中lowvdfnu,桥中lowv>dfnu

代码放到边双了。

点双

无向图中的极大点双连通子图。

就是极大的没有割点的子图。

P8435 【模板】点双连通分量

注意判自环,孤立点加自环会出错。

为形式统一,这里不判fa,并且将条件改成low[v]==dfn[u]。弹栈时到v就停下,因为u属于多个点双,不能弹出来。

板子
#include<bits/stdc++.h>

using namespace std;

int rd(){
	int f=1,r=0;
	char ch=getchar();
	while(ch>'9'||ch<'0'){ if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){ r=(r<<3)+(r<<1)+(ch^48);ch=getchar();}
	return r*f;
}

const int maxn=5e5+10,maxm=2e6+10;
int n,m,tot,dfc,vdc,tp,head[maxn],dfn[maxn],low[maxn],stk[maxn];
vector<int> ans[maxn];
struct edge{
	int v,nxt;
}e[maxm<<1];

inline void add(int u,int v){
	e[++tot].v=v;
	e[tot].nxt=head[u];
	head[u]=tot;
}

void tarjan(int u){
	dfn[u]=low[u]=++dfc,stk[++tp]=u;
	if(!head[u]) ans[++vdc].push_back(u);
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(low[v]==dfn[u]){
				vdc++;
				do ans[vdc].push_back(stk[tp]);
				while(stk[tp--]!=v);
				ans[vdc].push_back(u);
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}

int main(){
	n=rd(),m=rd();
	for(int i=1;i<=m;++i){
		int u=rd(),v=rd();
		if(u==v) continue;
		add(u,v),add(v,u);
	}
	for(int i=1;i<=n;++i) if(!dfn[i]) tp=0,tarjan(i);
	printf("%d\n",vdc);
	for(int i=1;i<=vdc;++i){
		printf("%d ",(int)ans[i].size());
		for(int k=0;k<(int)ans[i].size();++k) printf("%d ",ans[i][k]);
		puts("");
	}
	return 0;
}

圆方树

由于点双不能直接缩点(割点在多个点双里),我们使用圆方树。

原图中的点对应圆点,对每个点双新建一个方点并对这个点双中的圆点连边,最后得到圆方树。

P3854 [TJOI2008] 通讯网破坏

边双

无向图中的极大边双连通子图。

就是极大的没有桥的子图。

Tarjan 1

P8436 【模板】边双连通分量

可以把所有桥Tarjan出来,然后求边双。

第一种Tarjan(不推荐用这个求边双)
#include<bits/stdc++.h>

using namespace std;

const int maxn=5e5+5,maxm=4e6+5;
int n,m,tot=1,dfc=0,ans=0,head[maxn],dfn[maxn],low[maxn];
bool bri[maxm],used[maxn];
vector<int> d[maxn];
struct edge{
	int v,nxt;
}e[maxm];

inline void add(int u,int v){
	e[++tot].v=v;
	e[tot].nxt=head[u];
	head[u]=tot;
}

void tarjan(int u,int f){
	dfn[u]=low[u]=++dfc;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(!dfn[v]){
			tarjan(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) bri[i]=bri[i^1]=true;
		}
		else if(v!=f){
			low[u]=min(low[u],dfn[v]);
		}
	}
}

void dfs(int u){
	d[ans].push_back(u);
	used[u]=true;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(bri[i]||used[v]) continue;
		dfs(v);
	}
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<=m;++i){
		scanf("%d%d",&u,&v);
		if(u==v) continue;
		add(u,v);
		add(v,u);
	}
	for(int i=1;i<=n;++i){
		if(!dfn[i]){
			tarjan(i,0);
		}
	}
	for(int i=1;i<=n;++i){
		if(!used[i]){
			ans++;
			dfs(i);
		}
	}
	printf("%d\n",ans);
	for(int i=1;i<=ans;++i){
		int len=d[i].size();
		printf("%d ",len);
		for(int j=0;j<len;++j){
			printf("%d ",d[i][j]);
		}
		printf("\n");
	}
	return 0;
}

Tarjan 2

我们发现无向图中的DFS生成树上不是树边就是非树边。

在无向图中一个分量没有桥,那么它在DFS生成树上在同一个强连通分量中。

反过来,DFS生成树上的一个强连通分量在原图上是边双。

于是类似有向图求强连通分量Tarjan。但是由于是无向图,非树边只会的返祖边,于是不用判instk

重边是有意义的,于是防止回到父亲要记录边而非fa。注意判自环。

CF1000E We Need More Bosses

直接用Tarjan求出了每个边双,同时可以缩点。

第二种Tarjan(推荐)
#include<bits/stdc++.h>

using namespace std;

const int maxn=3e5+10;
int n,m,dfc=0,tp=0,tot=0,ans=0,s,stk[maxn],dfn[maxn],low[maxn],bel[maxn],dis[maxn];
bool vis[maxn];
vector< pair<int,int> > e[maxn];
vector<int> g[maxn];
queue<int> q;

void tarjan(int u,int lst){
	low[u]=dfn[u]=++dfc;
	stk[++tp]=u;
	int len=e[u].size();
	for(int i=0;i<len;++i){
		if(e[u][i].second==lst) continue;
		int v=e[u][i].first;
		if(!dfn[v]){
			tarjan(v,e[u][i].second);
			low[u]=min(low[u],low[v]);
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		tot++;
		while(stk[tp]!=u){
			bel[stk[tp]]=tot;
			tp--;
		}
		bel[u]=tot;
		tp--;
	}
} 

void dfs(int u,int d){
	int len=g[u].size();
	vis[u]=true;
	if(d>ans){
		ans=d,s=u;
	}
	for(int i=0;i<len;++i){
		int v=g[u][i];
		if(vis[v]) continue;
		dfs(v,d+1);
	}
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,x,y;i<=m;++i){
		scanf("%d%d",&x,&y);
		e[x].push_back(make_pair(y,i));
		e[y].push_back(make_pair(x,i));
	}
	for(int i=1;i<=n;++i){
		if(!dfn[i]){
			tp=0;
			tarjan(i,0);
		}
	}
	for(int i=1;i<=n;++i){
		int len=e[i].size();
		for(int j=0;j<len;++j){
			int v=e[i][j].first;
			if(bel[i]!=bel[v]){
				g[bel[i]].push_back(bel[v]);
				g[bel[v]].push_back(bel[i]);
			}
		}
	}
	memset(vis,0,sizeof(vis));
	dfs(1,0);
	memset(vis,0,sizeof(vis));
	ans=0;
	dfs(s,0);
	printf("%d\n",ans);
	return 0;
} 

边双缩点

直接缩就好,因为一个点只会在一个点双中,缩完后会形成一棵树,树边对应原图中的割边。

posted @   RandomShuffle  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示