图论篇6——割点(关节点)
引入
连通图
在一个无向图\(G\)中,若从顶点\(i\) 到顶点\(j\)有路径相连,则称 \(i\)和\(j\)是连通的。如果图中任意两点都是连通的,那么图被称作连通图。如果\(G\)是有向图,则称为强连通图(注意:需要双向都有路径)。如果是单向连通,则称\(G\)为单向连通图。
割点(关节点)
在无向连通图\(G=(V,E)\)中: 若对于\(x\in V\), 从图中删去节点\(x\)以及所有与\(x\)关联的边之后, \(G\)分裂成两个或两个以上不相连的子图, 则称\(x\)为\(G\)的割点。 简而言之, 割点是无向连通图中的一个特殊的点, 删去中这个点后, 此图不再连通, 而所以满足这个条件的点所构成的集合即为割点集合。
割边(桥)
如果删除\(G\)的一条边\(b\),图\(G\)分离成两个非空子图,则称边\(b\)为图\(G\)的桥。如下图中,顶点\(u\)和\(v\)都是割点,其他顶点都不是割点,边\((u,v)\)是桥,其他边都不是桥。
关节点识别
方案一:DFS (O(n^2))
依次去掉每一个点,判断图是否还连通。
方案二: Tarjan 算法
DFS树
首先需要了解一些关于深度优先搜索树(DFS tree)的概念。
以下图为例:
它的深度优先搜索树如下:
其中黑色的边为树边:如果结点\(u\)因算法对边\((u,v)\)的搜索而首次被发现,则\((u,v)\)是一条树边。
简单点说就是,它是正常的一颗树的边,只看\(1,2,3,4\)结点,是一颗树。【箭头只是表示搜索顺序】
其中红色的边为反向边:方向边\((u,v)\)是将结点\(u\)连接到其\(dfs\)树中的一个祖先结点\(v\)的边,环也被认为是反向边。
算法步骤
首先选定一个根节点,从该根节点开始遍历整个图(使用\(DFS\))。
对于根节点,判断是不是割点很简单,计算其子树数量就行,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。
对于非根节点,判断是不是割点就有些麻烦了。我们维护两个数组\(dfn[]\)和\(low[]\)。
\(dfn[u]\):意思就是在\(dfs\)的过程中,当前的\(u\)结点是第几个(首次)被访问的。(之前一直不知道这个\(dfn\)是个什么缩写,豆腐脑?)
\(low[u]\):表示顶点\(u\)及其子树中的点,通过反向边,能够回溯到的最早的点(\(dfn\)最小)的\(dfn\)值。
对于边\((u, v)\),如果\(low[v]>=dfn[u]\),此时\(u\)就是割点。
算法可视为线性时间复杂度,采用邻接表存储的话,应与\(DFS\)相同,为\(O(V+E)\)
例题1:https://www.luogu.org/problem/P3388
#include <iostream>
#include <set>
#include <stdio.h>
using namespace std;
int cnt, Time = 1;
int Low[20005], d[20005];
bool fuck[20005];
struct Node {
int data;
Node* next;
Node() {}
Node(int data) :data(data) {};
void push(int to) {
Node* s = new Node(to);
s->next = next;
next = s;
}
}head[20005];
void DFS(int u, int father) {
int cnt = 0;
Low[u] = d[u] = Time++;
Node* p = head[u].next;
while (p) {
int v = p->data;
if (d[v] == -1) {//如果v尚未访问
DFS(v, u);
if (Low[v] < Low[u])Low[u] = Low[v];
if (father != 0 && Low[v] >= d[u]) fuck[u] = true;
if (father == 0)
cnt++;
}
//如果v已经访问,但不是v的双亲,则v是一条反向边
Low[u] = Low[u] < d[v] ? Low[u] : d[v];
p = p->next;
}
if (father == 0 && cnt >= 2)fuck[u] = true;
}
int main() {
int i, n, m;
cin >> n >> m;
for (i = 1; i <= n; i++) {
d[i] = -1;
head[i].next = NULL;
}
for (i = 0; i < m; i++) {
int c1, c2;
//cin >> c1 >> c2;
scanf("%d%d", &c1, &c2);
head[c1].push(c2);
head[c2].push(c1);
}
for (int i = 1; i <= n; i++) {
if (d[i] == -1) DFS(i, 0);
}
//DFS(1, 0);
int res = 0;
for (int i = 1; i <= n; i++) {
if (fuck[i])res++;
}
cout << res << endl;
for (int i = 1; i <= n; i++) {
if (fuck[i])cout << i << ' ';
}
cout << endl;
return 0;
}