连通性与 Tarjan
强连通分量和 Tarjan
强连通分量(Strongly Connected Components,SCC),指的是极大的连通子图。
tarjan 算法,用于求图的强连通分量。
dfs 生成树
有向图的 dfs 生成树大致分为四种边:
- 树边(tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
- 反祖边(back edge):示意图中以红色边表示(即
),也被叫做回边,即指向祖先结点的边。 - 横叉边(cross edge):示意图中以蓝色边表示(即
),它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。 - 前向边(forward edge):示意图中以绿色边表示(即
),它是在搜索的时候遇到子树中的结点的时候形成的。
如果结点
反证法:假设有个结点
Tarjan 求强连通分量
需要对每个节点
: 在深度优先搜索时是第几个被访问的(人称豆腐脑序)。 :在 的子树中能够回溯到的最早的在栈中的结点。
很明显,对于每个
用 dfs 来求
当节点
未被访问,则对 进行 dfs,回溯时low[u] = min(low[u], low[v]);
因为存在从 到 的直接路径,所以 能够回溯到的已经在栈中的结点, 也一定能够回溯到。 已被访问且仍在栈中,那么low[u] = min(low[u], dfn[v]);
被访问过且已不在栈中,说明 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
处理完以后,我们可以想到在一个强连通分量里有且仅有一个点
所以只要在回溯的过程中判断
const int N = 1e4 + 10; int n, dfncnt, dfn[N], low[N], bel[N], stk[N], top, fstk[N], sc, sz[N], b[N], ans, sum, c[N]; vector<int> g[N]; void tarjan (int x) { dfn[x] = low[x] = ++dfncnt, fstk[x] = 1, stk[++top] = x; for (int i : g[x]) { if (!dfn[i]) { tarjan(i), low[x] = min(low[x], low[i]); } else if (fstk[i]) { low[x] = min(low[x], dfn[i]); } } if (dfn[x] == low[x]) { ++sc; while (1) { sz[sc]++, fstk[stk[top]] = 0, bel[stk[top--]] = sc; if (stk[top + 1] == x) { break; } } } }
应用
我们可以将每个强连通分量都压缩成一个点(缩点),那么原图就会变成 DAG,可以进行拓扑排序等。
例题
P2746 [USACO5.3] 校园网Network of Schools | #10091. 「一本通 3.5 例 1」受欢迎的牛
割点和割边
割点
对于一个无向图,如果把这个点删除后这个图的极大连通分量数增加了,那么这个点就是原图的割点(割顶)。
举个例子,这张图内的割点明显只有
首先我们先通过 DFS 给它们打上时间戳:
记录在 low
,用它来存储不经过其父亲能到达的最小的时间戳。
好,和 Tarjan 扯上关系了,那么首先 Tarjan 预处理一下,可以得到定理:对于某个顶点
但是对于搜索树的根节点需要特殊处理:若该点不是割点,则其他路径亦能到达全部结点,因此从起始点只「向下搜了一次」,即在搜索树内仅有一个子结点。如果在搜索树内有两个及以上的儿子,那么他一定是割点了(设想上图从
#include <bits/stdc++.h> using namespace std; const int N = 2e4 + 10; int n, m, dfncnt, dfn[N], low[N], bel[N], stk[N], top, fstk[N], sc, sz[N], cnt; vector<int> g[N]; bool ans[N]; void tarjan (int x, int fa) { int son = 0; dfn[x] = low[x] = ++dfncnt, fstk[x] = 1, stk[++top] = x; for (int i : g[x]) { if (!dfn[i]) { son++; tarjan(i, x), low[x] = min(low[x], low[i]); if (fa != x && low[i] >= dfn[x] && !ans[x]) { ans[x] = 1, cnt++; } } else if (i != fa) { low[x] = min(low[x], dfn[i]); } } if (fa == x && son >= 2 && !ans[x]) { ans[x] = 1, cnt++; } } int main () { ios::sync_with_stdio(0), cin.tie(0); cin >> n >> m; for (int i = 1, x, y; i <= m; i++) { cin >> x >> y, g[x].push_back(y), g[y].push_back(x); } for (int i = 1; i <= n; i++) { if (!dfn[i]) { dfncnt = 0, tarjan(i, i); } } cout << cnt << '\n'; for (int i = 1; i <= n; i++) { if (ans[i]) { cout << i << ' '; } } return 0; }
割边
对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。严谨来说,就是:假设有连通图
, 是其中一条边(即 ),如果 是不连通的,则边 是图 的一条割边(桥)。
比如下图中红色边就是割边。
和割点差不多,不需要特殊考虑根节点,只要把
void tarjan (int x, int fa) { dfn[x] = low[x] = ++dfncnt, fstk[x] = 1, stk[++top] = x; for (int i : g[x]) { if (!dfn[i]) { tarjan(i, x), low[x] = min(low[x], low[i]); if (low[i] > dfn[x] && !ans[x]) { ans[x] = 1, cnt++; } } else if (i != fa) { low[x] = min(low[x], dfn[i]); } } }
本文作者:wnsyou の blog
本文链接:https://www.cnblogs.com/wnsyou-blog/p/tarjan.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步