Tarjan 求割点

· · 个人记录

割点:删掉一个点后,整个图不再连通,称此点为割点。

我们想要求出一张图内所有割点,有一个很好的算法:Tarjan 算法。

【Tarjan 算法的预备】

Tarjan 算法主要依靠两个重要概念:

$low_i$,表示 $i$ 号结点通过 以 $i$ 为根的子树(以下子树均指搜索树中的子树)可以走到的 $dfn$ 最小的结点的 $dfn$ ,并且离开子树后就不许动了。 这两个概念比较难以理解,解释一番。 ------------ ![](https://cdn.luogu.com.cn/upload/image_hosting/2e09m6x9.png) 这是一张无向图,其中黑色的边是深搜树里的边,蓝色的边是其余没用的边。 那么,我们可以写出每个点的 $dfn$ 。 ![](https://cdn.luogu.com.cn/upload/image_hosting/piqroik5.png) 图中的红色数字即 $dfn$,比较好理解。 我们再写出每个点的 $low$ 。 ![](https://cdn.luogu.com.cn/upload/image_hosting/ybwkzwhd.png) 图中的黄色数字即 $low$ 。 下文 $n$ 号点 指 $dfn=n$ 的点 这里选几个典型: 1号点,它通过子树没法到达比它的 $dfn$ 更小的结点,因此它的 $low=1$。 这也很好理解,因为根结点不可能可以再往上走。 2号点,它的 $low=2$,因为它通过子树的边最多到达自己,即便是用了蓝色的非树边也只能到自己。 3号点,因为它可以先走到4号点,再通过一条蓝边到达2号点,因此 $low_3=2$。 **注意,3号点从蓝边走上去之后,就离开了3号点的子树,按照定义,不准动了,于是停在2号点。** 4号点,因为它可以直接通过蓝边走到2号点,因此 $low_4=2$。 **注意,与自己连的非树边也可以走。** ## 【Tarjan 算法】 那么,这两个东西有什么用? 主要是 $low$ 发挥了作用。 我们考虑一个割点。 如果删掉这个点,它一定把至少一棵子树 “切开” 了。 而可以把一棵子树 “切开” 的点,一定是割点。 ### 什么叫把子树 “切开”? 我们发现,被切开的子树无法独立连接到 整张图 去掉 以割点为根的子树 剩下来的图 上面。 **又因为没有横向边**,一棵子树的 $low$ 所代表的结点,一定是它们的**直接祖先**;而且,一个点的直接祖先在深搜中一定更先访问到。 所以,如果一棵子树根的 $low$ 如果比这个点的 $dfn$ 小,删掉这个点之后,这一棵子树依然可以连着; 反之,如果一棵子树根的 $low$ 大于等于这个点的 $dfn$,删掉这个点之后,这一棵子树就被 “切开” 了。 ### 如果一个点所有子树都不会被这个点 “切开”,这个点不是割点 ### 如果一个点有子树会被这个点 “切开”,这个点是割点 ------------ ## 【实现的细节】 现在,如果有了 $dfn$ 和 $low$,我们就可以知道每个点是不是割点。 但是,我们要怎么求这两个东西呢? $dfn$:全局变量记录当前已经搜了几个结点,**进入搜索的时候**更新。 $low$: 我们发现,$low$ 可以分两类。 1. 通过自己的非树边直接上去; 2. 通过子树的非树边间接上去。 这两者是不重不漏的,我们在两者之间取 **$min$** 即可。 第一类很好说,遍历自己所有的边,只要是非树边,就用对面的点更新一下。 第二类,还是遍历自己所有的边,只要通向子树,就先走下去搜,用返回的答案更新。 **如何转换成代码** 因为我们搜索的时候,有一个对点的 $vis$ 标记,如果一条边指向一个 $vis$ 标记为 $true$ 的点,说明这是一条往回走的边。 如果 $vis$ 标记为 $false$ 的点,说明这是一条通向子树的边。 **为什么会遍历到走向父结点的边但是不会出错???** **因为本身不会用来更新本身,本身用来更新父结点。** ## 【Code】 ``` #include <iostream> #include <cstring> #include <vector> #include <algorithm> using namespace std; const long long inf = 0x3f3f3f3f3f3f3f3fll; int n, m; vector<int> e[100005]; //dfn[i]为访问顺序,low[i]为i的子树结点能直接到达的最小dfn值 int dfn[100005] = {0}, low[100005], cur = 0, cntP = 0;//割点个数cntP bool ans[100005] = {0};//ans[i]=true表示i为割点 int srh(int x, int rt) {//访问x并返回low[x],搜索树根为rt low[x] = dfn[x] = ++cur; for (int i = 0, cnt = 0; i < e[x].size(); i++)//cnt为子节点个数 if (dfn[e[x][i]] == 0) {//未访问过,去访问,形成树边 low[x] = min(low[x], srh(e[x][i], rt)); cnt++; if (x != rt && low[e[x][i]] >= dfn[x])//非跟通过存在子树low值>=dfn[x]判断 ans[x] = true; if (x == rt && cnt >= 2)//根通过多于一个子节点判断 ans[x] = true; } else //非树边,则用来更新自己的low值 low[x] = min(low[x], dfn[e[x][i]]); cntP += ans[x]; return low[x]; } int main() { cin >> n >> m; for (int i = 1, u, v; i <= m; i++) { cin >> u >> v; e[u].push_back(v), e[v].push_back(u); } //找到一个dfn[i] == 0 则是一个新的连通分支,重新处理一遍 for (int i = 1; i <= n; i++) if (dfn[i] == 0) srh(i, i); cout << cntP << endl; for (int i = 1; i <= n; i++) if (ans[i]) cout << i << " "; cout << endl; return 0; } ```