图的连通性相关(Tarjan算法)
(大抄蓝书)
Part 1:无向图连通性
无向图的割点与桥
给定无向图
-
若对于
,从图中删去节点 以及所有与 关联的边之后, 分裂成两个或两个以上不相连的子图,则称 为 的割点 -
若对于
,从图中删去边 之后, 分裂成两个不相连的子图,则称 为 的割边或桥
时间戳
在图的深度优先遍历中,按照每个节点第一次被访问的时间顺序,给予
搜索树
在无向联通图中任选一个节点进行深度优先遍历,每个点只访问一次,所以发生递归的边
若无向图不连通,则各个连通块的搜索树构成无向图的“搜索森林”
追溯值
设
中的节点- 通过
条不在搜索树上的边,能够达到 的节点
感性理解为不经过其父亲能到达的最小的时间戳。
割边判定法则
无向图
同时可以得出一些结论:
-
桥一定是搜索树中的边
-
一个简单环中的边一定都不是桥
一道割边模板题 P1656 炸铁路
考虑一个细节:
对于边
#include<bits/stdc++.h>
using namespace std;
const int N=250,M=5010;
int n,m,dfn[N],low[N],num,cnt;
int head[N],ver[2*M],nxt[2*M],tot=1;
pair <int,int> ans[M];
bool bri[2*M];
bool cmp(pair<int,int> a,pair<int,int> b)
{
if(a.first==b.first)
return a.second<b.second;
return a.first<b.first;
}
void add(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void tarjan(int x,int last)
{
low[x]=dfn[x]=++num;
for(int i=head[x]; i; i=nxt[i])
{
int y=ver[i];
if(!dfn[y])
{
tarjan(y,i);
low[x]=min(low[x],low[y]);
if(dfn[x]<low[y]) //割边判定法则
bri[i]=bri[i^1]=1;
}
else if(i!=(last^1))
low[x]=min(low[x],dfn[y]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
for(int i=1; i<=n; i++)
if(!dfn[i])
tarjan(i,0);
for(int i=2; i<=tot; i+=2)
{
if(bri[i])
{
int x=ver[i],y=ver[i^1];
if(x>y)
swap(x,y);
ans[++cnt]=make_pair(x,y);
}
}
sort(ans+1,ans+1+cnt,cmp);
for(int i=1; i<=cnt; i++)
printf("%d %d\n",ans[i].first,ans[i].second);
return 0;
}
割点判定法则
若
特别地,若
一道割点模板题【模板】割点(割顶)
因为割点判定法则是小于等于号,所以在求割点时,不必考虑父节点和重边的问题,从
#include<bits/stdc++.h>
using namespace std;
const int N=20010,M=100010;
int n,m,dfn[N],low[N],num,rt;
int head[N],ver[2*M],nxt[2*M],tot;
bool cut[N];
void add(int x,int y)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void tarjan(int x)
{
dfn[x]=low[x]=++num;
int t=0;
for(int i=head[x]; i; i=nxt[i])
{
int y=ver[i];
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
if(dfn[x]<=low[y]) //割点判定法则
{
t++;
if(x!=rt || t>1)
cut[x]=1;
}
}
else
low[x]=min(low[x],dfn[y]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
for(int i=1; i<=n; i++)
if(!dfn[i])
rt=i,tarjan(i);
int cnt=0;
for(int i=1; i<=n; i++)
if(cut[i])
cnt++;
printf("%d\n",cnt);
for(int i=1; i<=n; i++)
if(cut[i])
printf("%d ",i);
return 0;
}
无向图的双联通分量
定义
-
若一张无向联通图不存在割点,则称它为“点双连通图”。若一张无向联通图不存在桥,则称它为“边双联通图”
-
无向图的极大点双联通子图被称为“点双联通分量”,简记为“
”。无向图的极大边双联通子图被称为“边双连通分量”,简记为“ ”。二者统称为“双连通分量”,简记为“ ”
定理
一张无向联通图是“点双联通图”,当且仅当满足下列两个条件之一:
-
图中的顶点数不超过
-
图中任意两点都同时包含在至少一个简单环中。其中“简单环”指的是不自交的环
定理
一张无向联通图是“边双连通图”,当且仅当任意一条边都包含在至少一个简单环中
边双(e-DCC)的求法
边双连通分量的计算非常简单,只需求出无向图中所有的桥,把桥都删除后,剩余的若干个连通块就是若干个“边双连通分量”
实现时,可以先用
int c[N],dcc;
void dfs(int x)
{
c[x]=dcc;
for(int i=head[x]; i; i=nxt[i])
{
int y=ver[i];
if(c[y] || bri[i])
continue;
dfs(y);
}
}
//在main函数中
for(int i=1; i<=n; i++)
if(!c[i])
dcc++,dfs(i);
边双(e-DCC)的缩点
把每个
int hc[N],vc[2*N],nc[2*N],tc=1;
void add_c(int x,int y)
{
vc[++tc]=y;
nc[tc]=x;
hc[x]=tc;
}
//在main函数中
for(int i=2; i<=tot; i+=2)
{
int x=ver[i^1],y=ver[i];
if(c[x]==c[y])
continue;
add_c(c[x],c[y]);
}
点双(v-DCC)
求解点双联通分量一般使用圆方树算法,它基于
在圆方树算法中,每次会找到一个点双在搜索树上的根节点(也是割点),此时有
在这棵树内,有原点(
cnt=n;
void tarjan(int x)
{
dfn[x]=low[x]=++dfn[0];
sta[++top]=x;
for(int i=head[x]; i; i=nxt[i])
{
int y=ver[i];
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
if(dfn[x]==low[y])
{
cnt++;
int z;
do
{
z=sta[top--];
g[cnt].push_back(z);
g[z].push_back(cnt);
}while(z!=y);
g[cnt].push_back(x);
g[x].push_back(cnt);
}
}
else
low[x]=min(low[x],dfn[y]);
}
}
Part 2:有向图连通性
前置知识
给定一个有向图
在一个流图
同时,类似无向图,在流图的深度优先遍历中,按照每个节点第一次被访问的时间顺序,给予
流图中的每条有向边
- 树边:指搜索树中的边,即
是 的父亲 - 前向边:指搜索树中
是 的祖先 - 返祖边:指搜索树中
是 的祖先 - 横叉边:除了上述三种情况以外的边,它一定满足
有向图的强联通分量
-
在有向图中,若两个节点
能够相互到达,则称这两个点是强连通的,它们之间具有强连通性。 -
给定一张有向图,若对于图中任意两个节点
都是强联通的,则称该有向图是一张强连通图 -
有向图的极大强联通子图被称为“强连通分量”,简记为“
”
一个“环”一定是强联通图。如果节点
容易发现,前向边没有什么用处,而返祖边非常有用,横叉边可能有用。为了找到通过“返祖边”和“横叉边”构成的环,
-
搜索树上
的祖先节点,记为集合设
。若存在返祖边 ,则 与 到 的路径一起构成环 -
已经访问过,并且存在一条路径到达
的节点设
是这样的一个点,从 出发存在一条路径到 。若存在横叉边 ,则 、 到 的路径, 到 的路径构成一个环
综上,栈中的节点就是能与
追溯值
设
- 该点在栈中
- 存在一条从
出发的有向边,以该点为终点
根据定义,可按照以下步骤计算“追溯值”
-
当节点
第一次被访问时,把 入栈,初始化 -
扫描从
出发的每条边- 若
没被访问过,说明 是树边,递归访问 ,从 回溯以后,令 - 若
被访问过且 在栈中,则令
- 若
-
从
回溯之前,判断是否有 。若成立,则不断地从栈中弹出节点,直至 出栈
强连通分量判定法则
在追溯值的计算过程中,若在
SCC的缩点
与
#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=100010;
int n,m,a[N],mx;
int dfn[N],low[N],num;
int sta[N],top,ins[N],c[N],cnt,val[N];
int ans[N],p,in_deg[N],f[N];
int head[N],ver[M],from[M],nxt[M],tot;
vector <int> scc[N],g[N],gg[N];
queue <int> q;
void add(int x,int y)
{
ver[++tot]=y;
from[tot]=x;
nxt[tot]=head[x];
head[x]=tot;
}
void tarjan(int x)
{
dfn[x]=low[x]=++num;
sta[++top]=x; ins[x]=1;
for(int i=head[x]; i; i=nxt[i])
{
int y=ver[i];
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(ins[y])
low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x])
{
cnt++; int y;
do
{
y=sta[top--]; ins[y]=0;
c[y]=cnt;
scc[cnt].push_back(y);
val[cnt]+=a[y];
}while(x!=y);
}
}
void topsort()
{
for(int i=1; i<=cnt; i++)
if(!in_deg[i])
q.push(i);
while(q.size())
{
int x=q.front(); q.pop();
ans[++p]=x;
for(int i=0; i<g[x].size(); i++)
{
int y=g[x][i];
in_deg[y]--;
if(in_deg[y]==0)
q.push(y);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
for(int i=1; i<=m; i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1; i<=n; i++)
if(!dfn[i])
tarjan(i);
for(int i=1; i<=tot; i++)
{
int x=from[i],y=ver[i];
if(c[x]==c[y])
continue;
g[c[x]].push_back(c[y]);
gg[c[y]].push_back(c[x]);
in_deg[c[y]]++;
}
topsort();
for(int i=1; i<=cnt; i++)
{
int x=ans[i];
f[x]=val[x];
for(int j=0; j<gg[x].size(); j++)
f[x]=max(f[x],f[gg[x][j]]+val[x]);
mx=max(mx,f[x]);
}
printf("%d",mx);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】