强连通分量

发布时间 2023-08-22 19:33:51作者: PHarr

概念

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

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

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

DFS 生成树

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

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

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

Tarjan

在 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);

缩点

求出强连通分量后还可以进行缩点,即把同一个强连通分量的点当做一个点来处理。这样可以得到一个有向无环图。注意因为编号较小的点不能到达编号较大的点,所以 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] );

luogu 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;
}

luogu 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;
}