📒 关于 dfs 树的笔记、用 tarjan 求割点、割边、双连通分量、强连通分量、缩点……

边的分类

对于有向图

树边、前向边、后向边(返祖边)、横叉边
P1
树边dfs 形成的边
前向边指向子孙方向的边
后向边(返祖边) 指向祖先方向的边
横叉边指向另一子树的边

对于无向图

树边,后向边(返祖边),前向边
P2
树边dfs 形成的边
后向边(返祖边) 指向祖先方向的边


无向图的双连通分量

双连通分量又分点双连通分量和边双连通分量两种。若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图。一个无向图中的每一个极大点(边)双连通子图称作此无向图的点(边)双连通分量。求双连通分量可用 Tarjan 算法。 ————百度百科

其中边双连通分量一定是点双连通分量,而点双连通分量不一定是边双连通分量(反例:两点一边)

割点

对于一个无向联通图,去掉一个点,这个图不联通,则该点为割点,下图中 u,v 即为割点

P3

做法

后文会用到)考虑对图进行 dfs,记录时间戳dfn[u]dfn[u],还有low[u]low[u]表示uu点通过返祖边能回到的最远的父亲。

如果当前uv(u \to v)访问到树边则显然low[u]=min(low[u],low[v])low[u]=min(low[u],low[v]),若当前访问到的是返祖边,则low[u]=min(low[u],dfn[v])low[u]=min(low[u],dfn[v])。判断树边和非树边只要看看dfndfn是否等于零就行了。
如何判断割点?假如low[v]>=dfn[u]low[v]>=dfn[u]是不是就可以判断点uu是割点,因为点vv能到达的最远的父亲在uu之下,那么删去uuvv子树就没有连着上面的边了(无向图中无)。但是,注意跟节点例外,反例就是根节点只要一个孩子。
所有对于任意非根节点,找到一个孩子使low[v]>=dfn[u]low[v]>=dfn[u],即为割点,对于根节点,只要有不小于两个孩子就是割点

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void dfs(int u,int f) {
dfn[u]=low[u]=++num;
int child=0;
for(int i=head[u];i;i=nxt[i]) {
int v=pnt[i];
if(!dfn[v]) {
child++;
dfs(v,u);
low[u]=min(low[u],low[v]);//对于割点不用判断重边,因为两点多条边和一条边是一样的
if((u==root&&child>1)||(u!=root&&dfn[u]<=low[v])) {
//u是割点
}
}
else if(v!=f) {
low[u]=min(low[u],dfn[v]);
}
}
}

割边(桥)

对于一个无向联通图,去掉一条边,这个图不联通,则该点为割边(桥),上图中 x 即为桥

做法

若边uvu \to v是桥,当且仅当uvu \to v是树边,且满足dfn[u]<low[v]dfn[u]<low[v],因为 v 想到达 u 必须经过边uvu \to v,删除这条边,图就不连通了。
注意:实现时要考虑重边的问题,故要标记边而不是标记点,边在标记时把反向边(^1)标记了。

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void dfs(int u) {
dfn[u]=low[u]=++num;
int child=0;
for(int i=head[u];i;i=nxt[i]) {
if(vis[i]) continue;
vis[i]=vis[i^1]=true; //标记边与反向边
int v=pnt[i];
if(!dfn[v]) {
dfs(v);
low[u]=min(low[u],low[v]);
if(dfn[u]<low[v]) {
//i是割边
}
}
else {
low[u]=min(low[u],dfn[v]);
}
}
}

求点双连通分量

😅 未完待续。。。