强连通分量

基础概念

  • 强连通图:任意两点间能互相到达的有向图称为 强连通图

  • 强连通分量:有向图的极大强连通子图称为 强连通分量,英文缩写为 SCC

  • 缩点:把强连通分量替换为点的操作称为 缩点,经过缩点之后的图是 DAG。

Tarjan 算法

DFS 生成树

以下为 DFS 生成树的示意图。

有向图的 DFS 生成树主要有 \(4\) 种边,但不一定 \(4\) 种全部出现。

  • 树边:搜索到一个未访问过的点时形成一条 树边。示意图中以黑色边表示。

  • 返祖边:终点是起点的祖先的边称为 返祖边。示意图中以红色边(即 \(7 \rightarrow 1\))表示。

  • 前向边:起点是终点的祖先的边称为 前向边,搜索到子树中的结点形成一条前向边。示意图中以绿色边(即 \(3 \rightarrow 6\))表示。

  • 横叉边:搜索到一个已访问过的结点,该点不是当前结点的祖先时形成一条 横叉边,也是起点和终点没有什么关系的额外边。示意图中以蓝色边(即 \(9 \rightarrow 7\))表示。

考察 DFS 生成树与强连通分量之间的关系。

若结点 \(u\) 是某个强连通分量在搜索树中遇到的第一个点,则该强联通分量的其余结点必定在以 \(u\) 为根的子树中。称 \(u\) 为该 强连通分量的根

定义 \(dfn\)\(low\)

  • \(dfn[u]\) 表示 DFS 中结点 \(u\) 的时间戳,即 \(u\) 被搜索的次序。

  • \(low[u]\) 表示在 \(u\) 的子树中能通过一条额外边回溯到的最早的已在栈中的结点,即从 \(u\) 能回溯到的最顶端的祖先的 \(dfn\) 值。

特别地,一个结点 \(u\) 总能访问到它自己,所以 \(low[u] \leq dfn[u]\)

统计思路与维护

考察额外边对于搜索树结构的影响,发现以下两条性质。

  • 横叉边起点的 DFS 序必定比终点的 DFS 序大。

  • 前向边相当于树中 \(u\)\(v\) 的链。

换句话说,横叉边、前向边对于求解强连通分量是无用的。

在搜索过程中,用栈来维护强连通分量,对于 \(u\) 及其相邻子结点 \(v\) 考虑 \(3\) 种情况。

  • \(v\) 未被访问:继续对 \(v\) 搜索。在回溯过程中,用 \(low[v]\) 更新 \(low[u]\)

  • \(v\) 被访问过,已在栈中:此时该边为树边或返祖边,用 \(dfn[v]\) 更新 \(low[u]\)

  • \(v\) 被访问过,已出栈:\(v\) 所在的强连通分量已被处理,不做操作。

\(low[u] = dfn[u] \Longleftrightarrow\)\(u\) 是强连通分量的顶端

此时栈中 \(u\) 及其上方的点构成一个强连通分量,可以做缩点操作。

代码实现

void tarjan(int u) {
    // SCC
    dfn[u] = low[u] = ++tim;
    stk[++top] = u; instk[u] = true;
    for (int v : e[u]) {
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (instk[v]) low[u] = min(low[u], dfn[v]);
    }
    // 缩点
    if (low[u] == dfn[u]) {
        scc_cnt++;
        while (top) {
            int x = stk[top--];
            instk[x] = false;
            scc[x] = scc_cnt;
            if (x == u) break;
        }
    }
}

例题

校园网

问题 \(1\) 的实质是求缩点后入度为 \(0\) 的点的个数。

问题 \(2\) 则是求最少添加几条边能使 DAG 变成 SCC。因为环中每个结点的入度和出度都不为 \(0\),显然是从出度为 \(0\) 的点向入度为 \(0\) 的点连边。答案则是入度为 \(0\) 的点和出度为 \(0\) 的点的个数的最大值。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, dfn[128], low[128], scc[128], scc_cnt, tim, stk[128], top;
int in[128], out[128], ans1, ans2;
bool instk[128];
vector<int> e[128];
void tarjan(int u) {
    dfn[u] = low[u] = ++tim;
    stk[++top] = u; instk[u] = true;
    for (int v : e[u]) {
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (instk[v]) low[u] = min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]) {
        scc_cnt++;
        while (top) {
            int x = stk[top--];
            instk[x] = false;
            scc[x] = scc_cnt;
            if (x == u) break;
        }
    }
}
int main() {
    cin >> n;
    for (int u = 1, v; u <= n; u++) {
        cin >> v;
        while (v) {
            e[u].push_back(v);
            cin >> v;
        }
    }
    for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i);
    for (int u = 1; u <= n; u++)
        for (int v : e[u])
            if (scc[u] != scc[v])
                out[scc[u]]++, in[scc[v]]++;
    for (int i = 1; i <= scc_cnt; i++)
        ans1 += (in[i] == 0), ans2 += (out[i] == 0);
    if (scc_cnt == 1) cout << "1\n0";
    else cout << ans1 << "\n" << max(ans1, ans2);
    return 0;
}

抢掠计划

“可以经过同一点或边多次”说明该有向图存在环,自然想到将它缩点成 DAG。

由于指定了一个起点,又要使利益最大化,不难想到在 DAG 上跑单源最长路。这里使用 SPFA 来实现。

启示:在读入点权之前先跑一遍 Tarjan,再记录数据有利于代码实现的简单化。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 8;
int dfn[N], low[N], scc[N], tim, stk[N], top, scc_cnt;
int n, m, st, p, a[N], dis[N];
bool instk[N], inque[N], bar[N];
vector<int> e[N], g[N];
void Tarjan(int u) {
    dfn[u] = low[u] = ++tim;
    stk[++top] = u; instk[u] = true;
    for (int v : e[u]) {
        if (!dfn[v]) {
            Tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (instk[v]) low[u] = min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]) {
        scc_cnt++;
        while (top) {
            int x = stk[top--];
            scc[x] = scc_cnt;
            instk[x] = false;
            if (x == u) break;
        }
    }
}
void SPFA() {
    st = scc[st]; queue<int> q;
    q.push(st); inque[st] = true; dis[st] = a[st];
    while (!q.empty()) {
        int u = q.front(); q.pop(), inque[u] = false;
        for (int v : g[u])
            if (dis[v] < dis[u] + a[v]) {
                dis[v] = dis[u] + a[v];
                if (!inque[v]) inque[v] = true, q.push(v);
            }
    }
}
int main() {
    cin >> n >> m;
    for (int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        e[u].push_back(v);
    }
    for (int i = 1; i <= n; i++) if (!dfn[i]) Tarjan(i);
    for (int i = 1, x; i <= n; i++) {
        cin >> x;
        a[scc[i]] += x;
    }
    cin >> st >> p;
    for (int i = 1, x; i <= p; i++) {
        cin >> x;
        bar[scc[x]] = true;
    }
    for (int i = 1; i <= n; i++)
        for (int v : e[i])
            if (scc[i] != scc[v])
                g[scc[i]].push_back(scc[v]);
    SPFA();
    int ans = 0;
    for (int i = 1; i <= scc_cnt; i++) if (bar[i]) ans = max(ans, dis[i]);
    cout << ans;
    return 0;
}

受欢迎的牛

对于图中的强连通分量来说,只要有一个是“明星”,该强连通分量中的所有点都是“明星”。

反过来思考,如果两个点都是“明星”,那么它们一定在同一个强连通分量中。

进一步考虑,所有“明星”一定在一个出度为 \(0\) 的强连通分量中。答案就是这个强连通分量的点数。

特别地,若图中有大于等于 \(2\) 个强连通分量,则它们无法到达对方,也就没有“明星”了。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 8;
int n, m, ans, sz[N], out[N], num;
int dfn[N], low[N], tim, scc_cnt, scc[N], stk[N], top;
bool instk[N];
vector<int> e[N];
void Tarjan(int u) {
    dfn[u] = low[u] = ++tim;
    stk[++top] = u; instk[u] = true;
    for (int v : e[u]) {
        if (!dfn[v]) {
            Tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (instk[v]) low[u] = min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) {
        scc_cnt++;
        while (top) {
            int x = stk[top--];
            scc[x] = scc_cnt;
            sz[scc_cnt]++;
            instk[x] = false;
            if (x == u) break;
        }
    }
}
int main() {
    cin >> n >> m;
    for (int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        e[u].push_back(v);
    }
    for (int i = 1; i <= n; i++) if (!dfn[i]) Tarjan(i);
    for (int u = 1; u <= n; u++)
        for (int v : e[u])
            if (scc[u] != scc[v])
                out[scc[u]]++;
    for (int i = 1; i <= scc_cnt; i++)
        if (out[i] == 0) ans += sz[i], num++;
    cout << (num == 1 ? ans : 0);
    return 0;
}
posted @ 2025-11-30 20:29  zheyutao  阅读(5)  评论(0)    收藏  举报