今天早上算是勉强学了Tarjan算法求无向图中的割点,虽然还不是特别透彻,但结合一点理解再背板子会比较容易一些,下面我简单讲讲我的理解。
首先是割点的定义,就是在无向图中去掉后,使得图会变成两个部分的点,可以理解为把图连接在一起的关键的点。那么对于割点的判定,就有以下两种情况:
一,一个点是根的时候。因为Tarjan算法需要对图进行dfs深度搜索,所以对于第一次dfs的点,就把它视作是根。那么这样的根是割点,需要它至少有2个子树,也就是至少有两个顶点与它相连,那么如果这个点去掉,它的两个子树就分开了,所以是割点。
二,一个点不是根的时候,如果这个点是割点,那么会有这样的性质:它的子树只能通过这个点这一条路径来访问到图的其他部分。
至于证明的话我也不是很清楚,我是根据Tarjan算法的代码得出上面的两个判定,就稍微背一下。
下面来讲一下Tarjan算法是如何利用这两个判定条件来找到割点的。
Tarjan算法利用了两个数组,dfn[i]和low[i],分别表示第i个点的dfn遍历的顺序和第i个 点能够访问到的最老的祖先。因为第二个判定条件需要用到子树访问到的祖先,如果一个点的子树能够访问到的最上面的点就是这个点,就说明这个点是割点。每次dfs时,传递的参数是两个变量,u和anc,u表示当前dfs访问到的点,anc代表这一次dfs的根。对u相邻的结点进行遍历,设这个点为v,如果没有访问过v,就对它dfs,dfs(v,anc),因为v是u出发到达的点,所以它们的根相同,都是anc。算了还是直接看代码吧,具体的解释在代码注释里,结合代码看得更容易一些。(代码我是背的洛谷用户wind_seeker的板子)
void tarjan(int u, int anc) {
dfn[u] = low[u] = ++t;//更新时间戳
int child = 0;//记录u结点的孩子个数
for (auto v : G[u]) {//访问顶点u相邻的顶点v
if (!dfn[v]) {//如果该顶点未访问过
tarjan(v, anc);//因为是从u继续访问的,所以追溯到的祖先相同
low[u] = min(low[u], low[v]);//u能访问到的最上面的是u和v能访问到的中偏上的那个
if (low[v] >= dfn[u] && u != anc) cut[u] = 1;//u的孩子最多访问到u且u不是根结点
if (u == anc) child++;//如果u是根结点,且有孩子v,则u的孩子数量增加
}
else
low[u] = min(low[u], dfn[v]);//已访问过的顶点v,有可能dfs序更小
}
if (child >= 2 && u == anc) cut[u] = 1;//有2个孩子的根是割点
}
代码里的t代表时间戳,从0开始,用来记录dfs遍历的顺序,cut数组记录点是否为割点,整个算法总体还是比较短的,直接背就完了,下面放一道洛谷P3388的求割点模板题。
题目描述:
给出一个 n 个点,m 条边的无向图,求图的割点。
输入格式:
第一行输入两个正整数 n,m。
下面 m 行每行输入两个正整数 ,x,y 表示 x 到 y 有一条边。
对于全部数据,1<=n<=2e4,1<=m<=1e5
输出格式:
第一行输出割点个数。
第二行按照节点编号从小到大输出节点,用空格隔开。
输入样例:
6 7 1 2 1 3 1 4 2 5 3 5 4 5 5 6输出样例:
1 5
AC代码:
#include<bits/stdc++.h>
#define ll long long
#define pii pair<int,int>
using namespace std;
const int maxn = 2e5 + 10;
vector<int>G[maxn];
int dfn[maxn], low[maxn],ge[maxn],t=0,cut[maxn];
int read() {
int x = 0, f = 0, ch = getchar();
while (!isdigit(ch)) { if (ch == '-')f = 1;ch = getchar(); }
while (isdigit(ch)) { x = x * 10 + ch - '0';ch = getchar(); }
return f ? -x : x;
}
void tarjan(int u, int anc) {
dfn[u] = low[u] = ++t;//更新时间戳
int child = 0;//记录u结点的孩子个数
for (auto v : G[u]) {//访问顶点u相邻的顶点v
if (!dfn[v]) {//如果该顶点未访问过
tarjan(v, anc);//因为是从u继续访问的,所以追溯到的祖先相同
low[u] = min(low[u], low[v]);//u能访问到的最上面的是u和v能访问到的中偏上的那个
if (low[v] >= dfn[u] && u != anc) cut[u] = 1;//u的孩子最多访问到u且u不是根结点
if (u == anc) child++;//如果u是根结点,且有孩子v,则u的孩子数量增加
}
else
low[u] = min(low[u], dfn[v]);//已访问过的顶点v,有可能dfs序更小
}
if (child >= 2 && u == anc) cut[u] = 1;//有2个孩子的根是割点
}
int main() {
int n = read(), m = read();
while (m--) {
int u = read(), v = read();
G[u].push_back(v);G[v].push_back(u);
}
for (int i = 1;i <= n;i++)
if (!dfn[i]) tarjan(i, i);
int ans = 0;
for (int i = 1;i <= n;i++)
if (cut[i]) ans++;
cout << ans << endl;
for (int i = 1;i <= n;i++)
if (cut[i])
cout << i << " ";
return 0;
}
以上。