强连通分量

1|0概念


连通 在有向图中存在uv的路径,则称u可达v。如果u,v互相可达,则u,v连通。

强连通 有向图G强连通指G中任意两个结点连通。

强联通分量 有向图的极大强连通子图。

2|0DFS 生成树


在有向图上进行 DFS 会形成森林。DFS会形成4 种边。

  1. Tree Edge,树边
  2. Back Edge,返祖边,指向祖先结点的边
  3. Cross Edge,衡叉边,指向搜索过程中已经访问过的结点,但是这个结点并不是祖先节点
  4. Forward Edge,前向边,指向子孙节点的边

如果结点u是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以u为根的子树中。结点u被称为这个强连通分量的根。

3|0Tarjan


在 Tarjan 算法中为每个结点维护以下几个变量

  1. dfn[i],节点i的dfs序
  2. low[i]i所在子树的节点经过最多一条非树边能到达的结点中最小的 dfs序
  3. scc[i],节点i所在的联通块
  4. inStk[i],节点i是否在栈中

同时还要维护capacity[i]表示,强连通分量i内结点数量

如果出现了low[x] == dfb[x]的情况,则x是他所在强连通分量的根,且栈中x以上的点全部都是这个强连通分量内的结点。

int cnt = 0, sc = 0; // cnt 记录 dfs 序 , sc 记录 强连通分量编号 stack<int> stk; vector<int> dfn, low, inStk, scc, capacity; void tarjan(int x) { low[x] = dfn[x] = ++cnt; inStk[x] = 1, stk.push(x); for (auto y: e[x]) { if (!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]); else if (inStk[y]) low[x] = min(low[x], dfn[y]); } if (low[x] == dfn[x]) { sc++, capacity.push_back(0); for (int y; true;) { y = stk.top(), stk.pop(); inStk[y] = 0, scc[y] = sc, capacity[sc]++; if (x == y) break; } } } // 因为有向图搜索会形成森林 for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i);

4|0缩点


求出强连通分量后还可以进行缩点,即把同一个强连通分量的点当做一个点来处理。这样可以得到一个有向无环图。注意因为编号较小的点不能到达编号较大的点,所以 scc 的编号顺序还符合拓扑序(编号越大拓扑序越靠前)。

for( int x = 1 ; x <= n ; x ++ ) for( auto y : e[x] ) if( scc[x] != scc[y] ) g[scc[x]].push_back( scc[y] );

5|0luogu P2341


反向建边,求出强连通分量,缩点后判断找入读为 0 的点,如果不唯一则无解,反之入读为零的点的大小就是答案。

#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; vector<vector<int>> e, g; int cnt = 0, sc = 0; stack<int> stk; vector<int> dfn, low, inStk, scc, capacity; void tarjan(int x) { low[x] = dfn[x] = ++cnt; inStk[x] = 1, stk.push(x); for (auto y: e[x]) { if (!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]); else if (inStk[y]) low[x] = min(low[x], dfn[y]); } if (low[x] == dfn[x]) { sc++, capacity.push_back(0); for (int y; true;) { y = stk.top(), stk.pop(); inStk[y] = 0, scc[y] = sc, capacity[sc]++; if (x == y) break; } } } int32_t main() { ios::sync_with_stdio(false) , cin.tie(nullptr); int n, m; cin >> n >> m; e.resize(n + 1); dfn.resize(n + 1), low.resize(n + 1), inStk.resize(n + 1), scc.resize(n + 1); capacity.push_back(0); for (int x, y; m; m--) cin >> y >> x, e[x].push_back(y); for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i); cout << capacity[sc]; return 0; }

6|0luogu P2746


首先对图进行缩点,然后求出入度为零的点的数量T,出度为零的点的数量S,任务A答案就是T,任务B答案就是max(S,T)

注意特判整张图本身就强连通的情况。

#include <bits/stdc++.h> using namespace std; vector<vector<int>> e; int cnt = 0, sc = 0; stack<int> stk; vector<int> dfn, low, inStk, scc, capacity; void tarjan(int x) { low[x] = dfn[x] = ++cnt; inStk[x] = 1, stk.push(x); for (auto y: e[x]) { if (!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]); else if (inStk[y]) low[x] = min(low[x], dfn[y]); } if (low[x] == dfn[x]) { sc++, capacity.push_back(0); for (int y; true;) { y = stk.top(), stk.pop(); inStk[y] = 0, scc[y] = sc, capacity[sc]++; if (x == y) break; } } } int32_t main() { ios::sync_with_stdio(false), cin.tie(nullptr); int n; cin >> n; e.resize(n + 1); dfn.resize(n + 1), low.resize(n + 1), inStk.resize(n + 1), scc.resize(n + 1); capacity.push_back(0); for (int i = 1; i <= n; i++) for (int y;;) { cin >> y; if (y) e[i].push_back(y); else break; } for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i); if( sc == 1 ){ cout << "1\n0\n"; return 0; } vector<int> s(sc + 1, 1), t(sc + 1, 1); for (int x = 1; x <= n; x++) for (auto y: e[x]) if (scc[x] != scc[y]) s[scc[x]] = 0, t[scc[y]] = 0; int S = accumulate(s.begin() + 1, s.end(), 0), T = accumulate(t.begin() + 1, t.end(), 0); cout << T << "\n" << max(S, T) << "\n"; return 0; }

__EOF__

本文作者PHarr
本文链接https://www.cnblogs.com/PHarr/p/17649508.html
关于博主:前OIer,SMUer
版权声明CC BY-NC 4.0
声援博主:如果这篇文章对您有帮助,不妨给我点个赞
posted @   PHarr  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示