scc tarjan + 缩点

· · 个人记录

强连通分量(scc)

定义

强连通:在有向图 G 中,两个不同的顶点 u,v,即存在 uv 的有向路径,也存在 vu 的有向路径,则称两个顶点是强连通的。

弱连通:忽略有向图中的方向,如果得到的无向图是连通的,那么原本的图就是弱联通的。

强连通图:在有向图 G 中,如果每两个节点都相互可达,则称 G 是一个强连通图。

强连通分量:在有向图中最大的一个强连通子图,子图本身是强连通图,且无法再增加原图中其他任何点。

Tarjan

维护一个栈,以及下列几个变量。

$low_i$ 表示点 $i$ 能回溯到的时间戳最小的节点,且在栈中。 $vis_i$ 表示点 $i$ 是否已入栈。 *** ### 实现步骤 首先,给当前节点的 $dfn_u$ 分配时间戳 $tim$,$low_u$ 初始值与 $dfn_u$ 相同。将当前节点入栈,标记 $vis_u = 1$。 接下来,枚举当前节点能到达的点 $v$。如果 $dfn_v$ 还没有被分配过时间戳,继续递归到点 $v$,因为返回后 $v$ 的 $low$ 值可能更小,所以更新 $low_u = \min(low_u, low_v)$。否则,判断 $v$ 是否入栈,即判断 $vis_v$ 是否为 $1$,若是,更新 $low_u = \min(low_u, dfn_v)$。 最后,判断当前节点是否为强连通分量的入口。当 $dfn_u = low_u$ 时,证明 $u$ 是这个强连通分量的入口,弹出栈内所有元素,将栈顶元素的入栈标记清空,这些栈内的元素构成了一个 scc。 ```c++ void tarjan(int u) { stk.push(u); vis[u] = 1; dfn[u] = low[u] = ++tim; for (int v : nbr[u]) { if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if (vis[v]) { low[u] = min(dfn[v], low[u]); } } if (dfn[u] == low[u]) { while (!stk.empty()) { vis[stk.top()] = 0; if (u == stk.top()) { stk.pop(); break; } stk.pop(); } } } ``` *** ### [B3609 [图论与代数结构 701] 强连通分量](https://www.luogu.com.cn/problem/B3609) 在原来的基础上,为其每一个 scc 染色,以及存储当前 scc 内的节点,以便后面输出。 ```c++ #include <bits/stdc++.h> using namespace std; const int N = 1e4 + 10; int n, m, dfn[N], low[N], col[N], scc, tim; bool vis[N]; stack<int> stk; vector<int> nbr[N], v[N]; void tarjan(int u) { stk.push(u); vis[u] = 1; dfn[u] = low[u] = ++tim; for (int v : nbr[u]) { if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if (vis[v]) { low[u] = min(dfn[v], low[u]); } } if (dfn[u] == low[u]) { ++scc; while (!stk.empty()) { col[stk.top()] = scc; vis[stk.top()] = 0; v[scc].push_back(stk.top()); if (u == stk.top()) { stk.pop(); break; } stk.pop(); } } } signed main() { cin >> n >> m; for (int i = 1; i <= m; i++) { int u, v; cin >> u >> v; nbr[u].push_back(v); } for (int i = 1; i <= n; i++) { if (dfn[i] == 0) tarjan(i); } cout << scc << '\n'; for (int i = 1; i <= n; i++) { if (dfn[i] == 0) continue; sort(v[col[i]].begin(), v[col[i]].end()); for (auto it : v[col[i]]) { cout << it << ' '; dfn[it] = 0; } cout << '\n'; } return 0; } ``` *** ### [P2661 [NOIP 2015 提高组] 信息传递](https://www.luogu.com.cn/problem/P2661) 因为每个点只有一条出边,所以最大的强连通分量会是一个环。 所以只需要记录不是一个点的强连通分量个数即可。 ```c++ #include <bits/stdc++.h> using namespace std; const int N = 2e5 + 10; int n, m, dfn[N], low[N], col[N], scc, tim, siz[N]; bool vis[N]; stack<int> stk; vector<int> nbr[N], v[N]; void tarjan(int u) { stk.push(u); vis[u] = 1; dfn[u] = low[u] = ++tim; for (int v : nbr[u]) { if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if (vis[v]) { low[u] = min(dfn[v], low[u]); } } if (dfn[u] == low[u]) { ++scc; while (!stk.empty()) { col[stk.top()] = scc; vis[stk.top()] = 0; v[scc].push_back(stk.top()); siz[scc]++; if (u == stk.top()) { stk.pop(); break; } stk.pop(); } } } signed main() { cin >> n; for (int i = 1; i <= n; i++) { int x; cin >> x; nbr[x].push_back(i); } for (int i = 1; i <= n; i++) { if (!dfn[i]) tarjan(i); } int ans = 1e9; for (int i = 1; i <= n; i++) { if (v[col[i]].size() > 1) ans = min(ans, int(v[col[i]].size())); } cout << ans; return 0; } ``` *** ### [P3387 【模板】缩点](https://www.luogu.com.cn/problem/P3387) 缩点其实就是将每一个强连通分量都变成一个点,形成一个 DAG。 定义 $sum_i$ 为第 $i$ 个 scc 里点权的和。 我们只要在弹出栈元素的时候求 $sum$,然后后面进行树形 dp 即可。 ```c++ #include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 10; int n, m, dfn[N], low[N], col[N], scc, tim, sum[N], dp[N], a[N], in[N]; bool vis[N]; stack<int> stk; vector<int> nbr[N], new_nbr[N]; pair<int, int> g[N]; void tarjan(int u) { stk.push(u); vis[u] = 1; dfn[u] = low[u] = ++tim; for (int v : nbr[u]) { if (!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if (vis[v]) { low[u] = min(dfn[v], low[u]); } } if (dfn[u] == low[u]) { ++scc; while (!stk.empty()) { col[stk.top()] = scc; vis[stk.top()] = 0; sum[scc] += a[stk.top()]; if (u == stk.top()) { stk.pop(); break; } stk.pop(); } } } void dfs(int u) { if (dp[u] != -1) return; dp[u] = sum[u]; int maxn = 0; for (int v : new_nbr[u]) { if (dp[v] == -1){ dfs(v); } maxn = max(maxn, dp[v]); } dp[u] += maxn; return; } signed main() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= m; i++) { int u, v; cin >> u >> v; nbr[u].push_back(v); g[i] = {u, v}; } for (int i = 1; i <= n; i++) { if (!dfn[i]) tarjan(i); } for (int i = 1; i <= m; i++) { if (col[g[i].first] != col[g[i].second]) new_nbr[col[g[i].first]].push_back(col[g[i].second]); } fill (dp + 1, dp + 1 + n, -1); int ans = 0; for (int i = 1; i <= n; i++) { if (dp[i] == -1) { dfs(i); ans = max(ans, dp[i]); } } cout << ans; return 0; } ``` ||scc缩点|并查集缩点|edcc(边双缩点)| |:-:|:-:|:-:|:-:| |适用图|有向图|无向图|无向图| |缩点之后得到的图的类型|DAG|无向森林(多棵树)|连通图之中直接变成一棵树| |一般求解的问题|拓扑+dp、2-SAT、环路分析|欧拉回路、最小生成树|树形 dp|