题解:AcWing 367 学校网络

【题目来源】

AcWing:367. 学校网络 - AcWing题库

【题目描述】

一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 \(A\) 支援学校 \(B\),并不表示学校 \(B\) 一定要支援学校 \(A\))。

当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。

因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。

现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校?

最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件?

【输入】

第 \(1\) 行包含整数 \(N\),表示学校数量。

第 \(2..N+1\) 行,每行包含一个或多个整数,第 \(i+1\) 行表示学校 \(i\) 应该支援的学校名单,每行最后都有一个 \(0\) 表示名单结束(只有一个 \(0\) 即表示该学校没有需要支援的学校)。

【输出】

输出两个问题的结果,每个结果占一行。

【输入样例】

5
2 4 3 0
4 5 0
0
0
1 0

【输出样例】

1
2

【核心思想】

  1. 问题分析:给定 \(N\) 个学校及其支援关系(有向图),需要回答两个问题:(1)最少需要将软件直接提供给多少个学校,才能使所有学校都能获得软件;(2)最少需要添加几条支援关系,使得将软件提供给任意一个学校,其他所有学校都能获得软件。这是一个强连通分量(SCC)+ 缩点问题,关键在于将互相可达的学校缩成一个分量,然后分析分量之间的拓扑关系。

  2. 算法选择

    • Tarjan算法:使用DFS求强连通分量,时间复杂度 \(O(N + M)\)
    • 缩点:将每个强连通分量视为一个超级节点,构建缩点后的DAG
    • 入度/出度分析:统计缩点后每个分量的入度和出度
  3. 关键步骤

    • 建图:读取每个学校的支援名单,构建有向图邻接表
    • Tarjan求SCC
      • dfn[x]:节点 \(x\) 的DFS时间戳
      • low[x]:节点 \(x\) 能回溯到的最早时间戳
      • 使用栈维护当前DFS路径上的节点
      • dfn[x] == low[x] 时,找到一个强连通分量的根
    • 缩点统计入度/出度
      • 遍历所有边 \((x, y)\),若 scc[x] != scc[y],则分量 scc[x] 出度加1,分量 scc[y] 入度加1
    • 计算答案
      • 问题1(最少发放学校数):统计入度为0的分量数量 \(a\),这些分量无法从其他分量到达,必须直接发放
      • 问题2(最少添加边数):若只有一个SCC(图强连通),答案为0;否则需要添加 \(\max(a, b)\) 条边,其中 \(b\) 是出度为0的分量数量。原理是将入度为0的分量作为起点、出度为0的分量作为终点,构建环状连接
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N + M)\),Tarjan算法线性时间
    • 空间复杂度:\(O(N + M)\),邻接表和辅助数组
  5. 强连通分量与缩点的核心思想

    • 强连通分量:有向图中互相可达的最大子图,分量内所有节点可以视为等价(软件可以互相传递)
    • 缩点思想:将每个SCC收缩为一个超级节点,原图变为DAG,简化问题
    • 入度为0的性质:入度为0的分量无法从其他分量获得软件,必须直接发放
    • 使图强连通的策略:将出度为0的分量连接到入度为0的分量,形成环,最少需要 \(\max(\text{入度为0数}, \text{出度为0数})\) 条边
    • 适用于有向图可达性分析、最小覆盖集、图强连通化类问题

【算法标签】

强连通分量

【代码详解】

#include <bits/stdc++.h>
using namespace std;

// ================= 常量与全局变量 =================

const int N = 105;      // 最大点数

int n;                  // 学生数量(也是图中的节点数)
vector<int> e[N];       // 邻接表,存单向依赖关系

// ---------- Tarjan 算法相关 ----------
int dfn[N];             // 时间戳:节点首次被访问的顺序
int low[N];             // 能追溯到的最早栈中节点时间戳
int tot;                // 时间戳计数器

int stk[N];             // 手写栈,保存当前 DFS 路径
int instk[N];           // 标记节点是否在栈中
int top;                // 栈顶指针

// ---------- 强连通分量(SCC)相关 ----------
int scc[N];             // 每个节点所属的强连通分量编号
int cnt;                // 强连通分量的总数

int din[N];             // 每个 SCC 的入度
int dout[N];            // 每个 SCC 的出度

// ================= Tarjan 算法 =================
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;   // 初始化时间戳
    stk[++top] = x;             // 当前节点入栈
    instk[x] = 1;               // 标记为在栈中

    // 遍历 x 的所有出边
    for (int y : e[x])
    {
        if (!dfn[y])            // y 尚未访问
        {
            tarjan(y);          // 递归处理
            low[x] = min(low[x], low[y]);   // 用子树更新 low
        }
        else if (instk[y])      // 发现返祖边
        {
            low[x] = min(low[x], dfn[y]);   // 用回边更新 low
        }
    }

    // 若 x 是强连通分量的根
    if (dfn[x] == low[x])
    {
        int y;
        ++cnt;                  // 新开一个强连通分量
        do
        {
            y = stk[top--];     // 弹出栈顶
            instk[y] = 0;       // 取消栈中标记
            scc[y] = cnt;       // 归属到当前 SCC
        }
        while (y != x);         // 直到弹出 x
    }
}

// ================= 主函数 =================
int main()
{
    cin >> n;

    // 读入依赖关系(以 0 结束)
    for (int i = 1; i <= n; i++)
    {
        int t;
        while (cin >> t, t)
            e[i].push_back(t);
    }

    // 对每个未访问的点执行 Tarjan
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);

    // ========= 缩点后统计 SCC 的出入度 =========
    for (int x = 1; x <= n; x++)
    {
        for (int y : e[x])
        {
            int a = scc[x];
            int b = scc[y];
            if (a != b)         // 不在同一个 SCC 中
            {
                dout[a]++;      // a 所在 SCC 出度 +1
                din[b]++;       // b 所在 SCC 入度 +1
            }
        }
    }

    // ========= 统计入度为 0 和出度为 0 的 SCC =========
    int a = 0, b = 0;
    for (int i = 1; i <= cnt; i++)
    {
        if (!din[i]) a++;       // 入度为 0 的 SCC 数量
        if (!dout[i]) b++;      // 出度为 0 的 SCC 数量
    }

    // 至少需要 a 次才能“启动”所有 SCC
    cout << a << endl;

    // 若整个图已经是强连通的,则不需要额外操作
    if (cnt == 1)
        cout << 0 << endl;
    else
        cout << max(a, b) << endl;

    return 0;
}

【运行结果】

5
2 4 3 0
4 5 0
0
0
1 0
1
2
posted @ 2026-06-27 14:20  团爸讲算法  阅读(7)  评论(0)    收藏  举报