图论 —— 图的连通性 —— Tarjan 求割点与桥
【概念】
1.割点
1)割点:删除某点后,整个图变为不连通的两个部分的点
2)割点集合:在一个无向图中删除该集合中的所有点,能使原图变成互不相连的连通块的点的集合
3)点连通度:最小割点集合点数
如上图,若去掉 0,则图被分成 12 和 34 两个连通分量;若去掉 3,则图被分成 012 和 4 两个连通分量。
故:0、3 是割点,两者是一个割点集,点连通度是 2
2.桥
1)桥(割边):删除某边后,使得连通图变为不连通的图,一图中可能有多条割边
2)割边集合:所有割边的集合
3)边连通度:最小割边集合边数
如上图,去掉 5-6,2-5 后不再连通,图被分为 1234、5、6 三个连通分量
故:5-6、2-5 是割边,两者组成的集是割边集,边连通度是 2
【原理】
1.求割点
对于一棵 DFS 搜索树:
1)根结点:当且仅当根节点至少有两个儿子时,其是割点
2)其他点:对于其他点 v,当仅有一个儿子 u 时,从 u 或 u 的后代出发,没有指向 v 祖先(不含 v)的边,则删除 v后,u 和 v 的父亲不连通时,v 是割点
2.求桥
对于边 (u,v),若发现 v 和它的后代不存在一条连接 u 或其祖先的边,则删除 (u,v) 后 u 和 v 不连通,因此边 (u,v) 为桥
【过程】
1.求割点
1)对图进行 Tarjan 算法,得到 dfn 和 low 两个数组
2)每遍历一个新的 u 的儿子 v 都记录个数
3)low[u] 值更新后进行以下判断(前提:v 未被遍历):
① u 为树根,且儿子个数大于 1
② u 不为树根,但 low[v]>=dfn[u]
满足以上任意条件 u 便为割点,记录在一数组里,Tarjan 完成后再输出即可(中途输出会重复)
2.求桥
判断原则:若 low[v]>dfn[u],则 (u,v) 为割边。
由于有的图有重边,因此不好通过上述判断原则来处理,故使用以下方法:
1)对图进行 Tarjan 算法,得到 dfn 和 low 两个数组
2)形参加上 father,作用为记录 u 的父亲节点,避免在遍历 v 时遇到重边重新更新 low[u]
3)在 v 已被遍历的位置加上判断:如果 v 点等于 father,那么就不更新 low[u] 值
4)Tarjan算法结束后,遍历所有节点u及其子节点v,如果 low[u]==low[v],那么这条边就为割边
【实现】
1.先将数据保存下来,再求割点与桥
int n,m;
int low[N],dfn[N];
vector<int>G[N];
bool is_cut[N];//记录是否为割点
int father[N];
int tim;//时间戳
void Tarjan(int x,int pre){
father[x]=pre;//记录每一个点的父亲
dfn[x]=low[x]=tim++;//每找到一个新点,纪录当前节点的时间戳
for(int i=0;i<G[x].size();i++){
int y=G[x][i];//当前结点的下一结点
if(dfn[y]==-1){//若未被访问过
Tarjan(y,x);
low[x]=min(low[x],low[y]);
}
else if(pre!=y)//假如k是i的父亲的话,那么这就是无向边中的重边,有重边那么一定不是桥
low[x]=min(low[x],dfn[y]);//可能dfn[k]!=low[k],所以不能用low[k]代替dfn[k],否则会上翻过头
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i){
int x,y;
scanf("%d%d",&x,&y);
G[x].push_back(y);
G[y].push_back(x);
}
tim=0;
memset(low,-1,sizeof(low));
memset(dfn,-1,sizeof(dfn));
memset(father,0,sizeof(father));
memset(is_cut,false,sizeof(is_cut));
Tarjan(1,0);//先Tarjan求出dfn和low数组
int root=0;
for(int i=2;i<=n;i++){//统计根节点子树的个数,根节点的子树个数>=2,即为割点
int temp=father[i];
if(temp==1)
root++;
else{
if(low[i]>=dfn[temp])//割点的条件
is_cut[temp]=true;
}
}
if(root>1)
is_cut[1]=true;
//打印割点
for(int i=1;i<=n;++i)
if(is_cut[i])
printf("%d ",i);
printf("\n");
for(int i=1;i<=n;++i){
int temp=father[i];
if(temp>0&&low[i]>dfn[temp])//桥的条件
printf("%d,%d\n",temp,i);
}
return 0;
}
2.在 Tarjan 的过程中求桥
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<map>
#define EPS 1e-9
#define PI acos(-1.0)
#define INF 0x3f3f3f3f
#define LL long long
const int MOD = 1E9+7;
const int N = 1000000+5;
const int dx[] = {-1,1,0,0};
const int dy[] = {0,0,-1,1};
using namespace std;
struct Edge{
int from,to,next;
int cut;//记录该边是否为桥
Edge(){}
Edge(int from,int to,int next,int cut):from(from),to(to),next(next),cut(cut){}
}edge[N];
int n,m;
int head[N],edgeNum;
int dfn[N],low[N];
int block_cnt;
void addEdge(int from,int to){//添边
edge[edgeNum]=Edge(from,to,head[from],false);
head[from]=edgeNum++;
}
void tarjin(int x,int father){
low[x]=dfn[x]=++block_cnt;
for(int i=head[x];i!=-1;i=edge[i].next){
int y=edge[i].to;
if(!dfn[y]){
tarjin(y,x);
if(low[x]>low[y])
low[x]=low[y];
if(low[y]>dfn[x])
edge[i].cut=true;
}
else if(y!=father)
low[x]=min(low[x],dfn[y]);
}
}
int main(){
scanf("%d%d",&n,&m);
memset(dfn,0,sizeof(dfn));
memset(head,-1,sizeof(head));
edgeNum=0;
while(m--){
int x,y;
scanf("%d%d",&x,&y);
addEdge(x,y);
addEdge(y,x);
}
tarjin(1,1);
for(int i=0;i<edgeNum;i++)//打印桥
if(edge[i].cut)
printf("%d %d\n",edge[i].from,edge[i].to);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效