在一幅无向图中,如果删除了一个点,导致图分成了两个或多个联通块(强连通分量),那么这个点就是割点。怎么求这样的点呢?最原始暴力的方法就是每次枚举一个点,删除,跑一遍最短路。今天我们可以用更高级的 Tarjan 算法 求解。
割点与割边
割点的定义
在一个无向图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,图的连通分量增多,就称这个点集为割点集合。
也就是前文所述:如果删除了一个点,导致图分成了两个或多个联通块(强连通分量),那么这个点就是割点(也叫作割顶)。说得再通俗点,去掉割点,图会不连通。
割边的定义
与割点类似:
假设有连通图 G,e 是其中一条边,如果 G-e 是不连通的,则边 e 是图 G 的一条割边。
就是说如果把这条边删除,图分为两个强连通分量,这条边就称为割边(也称为桥)。
暴力求解
以割点为例:前面也提到了,最原始的暴力方法:就是每次枚举一个点,删除,跑一遍最短路,看看结点 1 是否还可以到达除了删除点之外的所有点,如果不能则说明删除点是割点。如果跑最稳的 Dijkstra+Heap,复杂度也要 ……
Tarjan 算法
求解割点和割边,用 Tarjan 算法会十分优秀。在此之前我们要先学习下什么是 DFS 树。
DFS 树
所谓 DFS 树,就是对一幅图进行 DFS 按照 DFS 顺序得到的一棵生成树。可以从任意点开始 DFS(一般从 1 开始),并不影响答案。在这幅图中,在 DFS 树中的边称为树边,不在 DFS 树中的边称为非树边。
返祖边
这里我们只研究无向图。在无向图中,非树边只会以返祖边的形式存在。也就是说非树边会从当前点指向某个祖先。例如下图中,D 指向 A 的边就是返祖边。(这张图其实是无向图,为了便于理解,按照 DFS 的顺序标出方向)
graph TB
A((A))-->B((B))
B((B))-->C((C))
B((B))-->D((D))
D((D))-->A
算法原理
割点的求解
可以把割点分为两种情况:
- 根节点在 DFS 树中有多于一个子节点,那么根节点就是割点;
- 对于非根节点 u,它至少有一棵子树没有返祖边可以到达 u 的祖先。
第一种情况很好理解;对于第二种情况,如果 u 有一个子树中有一个结点 x 有返祖边可以到达 u 的祖先,那么把 u 删除后,由于原来的树是联通的,那么我们找出的 x 仍然有边可以到达 u 以上的所有点,又因为那个点在 u 的一棵子树里,那么这棵子树就可以到达 u 以上的所有点!
割边的求解
割边甚至更加简单,忽略第一种情况,如果一条边是割边,当且仅当其子树均没有跨过这条边。
具体实现
以求割点为例,每个节点维护几个信息:
-
表示点 x 的标号,即它是第几个被遍历到的点。维护这一信息十分有用,因为如果点 a 是点 b 的祖先,那么 。
-
表示 x 点不经过其的父节点,能通过返祖边到达的 最小的祖先的 值。初始值为 。
在 DFS 的同时可以维护这几个信息,回溯时判断如果 (v 是 x 的儿子)说明 x 是割点。
核心代码如下:
inline void DFS(int x){
vis[x]=1;dfn[x]=low[x]=++acnt;
int nowson=0;
for (int i=lnk[x];i;i=nxt[i]) if (!vis[son[i]]){
nowson++;
fa[son[i]]=x;
DFS(son[i]);
low[x]=min(low[x],low[son[i]]);
if (x!=1&&low[son[i]]>dfn[x]) ans[x]=true;
if (x==1&&nowson>=2) ans[x]=true;
} else if (son[i]!=fa[x]) low[x]=min(dfn[son[i]],low[x]);
}
例题
割点模板题
割边模板题
参考代码
割点(POJ1144)
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn=105,maxe=1000005;
int n,fa[maxn];
int tot=0,acnt=0,dfn[maxn],low[maxn];
int lnk[maxn],nxt[maxe],son[maxe];
bool vis[maxn],ans[maxn];
inline void add(int x,int y){
tot++;son[tot]=y;nxt[tot]=lnk[x];lnk[x]=tot;
}
inline void Init(){
tot=0;acnt=0;
memset(fa,0,sizeof(fa));
memset(lnk,0,sizeof(lnk));
memset(nxt,0,sizeof(nxt));
memset(son,0,sizeof(son));
memset(vis,0,sizeof(vis));
memset(dfn,0,sizeof(dfn));
memset(ans,0,sizeof(ans));
}
{
vis[x]=;dfn[x]=low[x]=++acnt;
nowson=;
( i=lnk[x];i;i=nxt[i]) (!vis[son[i]]){
nowson++;
fa[son[i]]=x;
(son[i]);
low[x]=(low[x],low[son[i]]);
(x!=&&low[son[i]]>=dfn[x]) ans[x]=;
(x==&&nowson>=) ans[x]=;
} (son[i]!=fa[x]) low[x]=(dfn[son[i]],low[x]);
}
{
(;;){
(,&n); (n==) ;
(); ans_cnt=;
x;(,&x);
(x!=){
now=; ch=();
(ch!=&&ch!=){
((ch<||ch>)&&ch!=&&ch!=) ch=();
(ch==||ch==) ;
(ch>=&&ch<=) now=now*+ch-,ch=();
(x,now);(now,x);
now=;
}
(,&x);
}
();
( i=;i<=n;i++) ans_cnt+=ans[i];
(,ans_cnt);
}
;
}
割边(ZOJ2588)
#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>
using namespace std;
const int maxn=10005,maxe=200005;
int T,n,m,fa[maxn];
int tot=0,acnt=0,dfn[maxn],low[maxn];
int lnk[maxn],nxt[maxe],son[maxe],id[maxe];
bool vise[maxe],ans[maxe],vis[maxn];
vector<int> que_ans;
inline void add(int x,int y,int z){
tot++;son[tot]=y;id[tot]=z;nxt[tot]=lnk[x];lnk[x]=tot;
}
inline void Init(){
tot=0;acnt=0;que_ans.clear();
memset(fa,0,sizeof(fa));
memset(id,0,sizeof(id));
memset(lnk,0,sizeof(lnk));
memset(nxt,0,sizeof(nxt));
memset(son,0,sizeof(son));
(dfn,,(dfn));
(low,,(low));
(ans,,(ans));
(vis,,(vis));
}
{
dfn[x]=low[x]=++acnt;
( i=lnk[x];i;i=nxt[i]) (!vis[son[i]]){
fa[son[i]]=id[i];vis[son[i]]=;
(son[i]);
(low[son[i]]<=dfn[x]) ans[id[i]]=;
low[x]=(low[x],low[son[i]]);
} (id[i]!=fa[x]) low[x]=(dfn[son[i]],low[x]),ans[id[i]]=;
}
{
(,&T);
(T--){
(,&n,&m);
();
( i=;i<=m;i++){
x,y;(,&x,&y);
(x,y,i);(y,x,i);
}
vis[]=;();
( i=;i<=m;i++) (ans[i]) que_ans.(i);
nn=que_ans.();(,nn);
( i=;i<nn;i++) (,que_ans[i]);
(nn!=) (,que_ans[nn]);
(T>) ();
}
;
}
参考
吐槽一下,POJ1144 调代码一直调不对,然后去 discuss 区搞了组数据,用 Mermaid 生成图,我以为会很清晰,结果是这样的……
graph TB;
17---4
17---18
1---11
7---5
13---1
3---1
14---5
15---20
9---12
6---8
16---14
18---8
8---4
20---18
20---10
2---3
12---5
5---9
5---20
19---20
19---9
19---11
19---2
11---3
4---15
10---3
21---3
吐血……