连通性相关
基础部分
DFS生成树
在有向图中,DFS生成树有
-
树边:每次搜索找到一个还未访问过的节点时就形成了一条树边。
-
返祖边:搜索时遇到在树上的祖先节点,指向祖先的边。
-
横叉边:搜索时遇到已访问过的节点,但该节点不是当前节点的祖先,就形成了一条横叉边。
-
前向边:搜索时遇到了子树内的点,形成一条前向边。
无向图中除了树边就是非树边(同时也是返祖边)。
性质
显然每条返祖边对应着一个环,但注意不是一一对应
极其有用的一个性质是:无向图中,DFS生成树上的返祖边构成的环是原图的环空间的一组基。这意味着DFS生成树上的返祖边构成的环异或起来属于原图中的环的集合的子集构成的簇。
很多神秘的东西发现对应不到其它的算法时,就上最纯粹的DFS生成树的力量吧。
强连通分量
这是定义在有向图上的。
强连通分量(Strongly Connected Components,SCC):极大的强连通子图。
求强连通分量,常用Tarjan算法。(求各种连通分量都常用Tarjan算法)
在DFS生成树上
如果一个节点
证:
反证法。若一个点
Tarjan算法求强连通分量
用一个栈保存当前已经搜到过并且还未处理所在强连通分量的节点。
对每个节点
-
:表示 的DFS序。 -
:表示 的子树内能够回溯到的最早的在栈中的节点。 的值定义为以下节点的 的最小值: 中的节点,从 中通过一条非树边可以到达的节点。
显然有些性质:
-
的子树内的节点的 都大于 。 -
从根开始的一条路径上,
单调递增, 单调不降。
在搜索过程中,对于
-
未被访问过,则继续搜索 ,并用 更新 。这是因为存在从 到 的直接路径,从 的子树内能回溯到的节点,在 的子树内也能回溯到。 -
被访问过,在栈中。根据 的定义,用 更新 。 -
被访问过,不在栈中。说明 所在强连通分量已经处理好了, 已搜索完毕。不用操作。
在一个强连通分量中,有且仅有一个节点的
因此,在回溯时,如果对于当前节点
板子
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--;
}
}
时间复杂度
遍历每个点,如果当前点
SCC缩点
强连通分量可以进行缩点,缩点后形成一张DAG。在这张DAG上可以进行更多操作。
双连通分量
板子多,尽量理解记忆。
前置
有向图中的割点与桥
校测里出现的神秘题目。
先强连通分量缩点转DAG,然后就是DAG上的割点和桥。
还是用DFS生成树分析。咕咕咕。
感觉是现在的自己不可做的。
也有可能本身不可做(?)
割点
定义在无向图上。
简化的定义:一张图
求割点,常用Tarjan算法。
-
: 的时间戳。 -
: 不经过其父亲能到达的最小时间戳。但是要用其父亲的时间戳更新 。
若
若
板子
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;
}
桥
定义在无向图上。
简化的定义:一张图
求桥,常用Tarjan算法。
其实与割点差不多,改动一点即可:
-
桥与是否为根节点无关,所以不用特别记录。
-
割点中
,桥中 。
代码放到边双了。
点双
无向图中的极大点双连通子图。
就是极大的没有割点的子图。
注意判自环,孤立点加自环会出错。
为形式统一,这里不判fa
,并且将条件改成low[v]==dfn[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;
}
圆方树
由于点双不能直接缩点(割点在多个点双里),我们使用圆方树。
原图中的点对应圆点,对每个点双新建一个方点并对这个点双中的圆点连边,最后得到圆方树。
边双
无向图中的极大边双连通子图。
就是极大的没有桥的子图。
Tarjan 1
可以把所有桥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
。注意判自环。
直接用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;
}
边双缩点
直接缩就好,因为一个点只会在一个点双中,缩完后会形成一棵树,树边对应原图中的割边。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App