You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 367. 学校网络

一、题目描述

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

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

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

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

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

输入格式1 行包含整数 N,表示学校数量。

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

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

数据范围 2≤N≤100

输入样例

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

输出样例

1
2

二、题目分析

本题唯一的难点在于结论的推导,推导出结论后直接调用Tarjan算法的模板即可解决问题。 首先,至少要将新软件提供给多少个学校,才能保证所有的学校都获得软件。 对于普通的DAG而言,只需要将新软件提供给所有入度为0的节点,就可以保证所有的学校都获得新的软件。

如图所示,入度为0的节点有12,这意味着没有任何节点可以到达12节点,所以至少要提供2个新软件才能保证12都获得软件;同时DAG中任意一个入度不为0的节点都可以从某个入度为0的节点出发遍历到,正如树中的根节点一定可以达到树中的任意内部节点一样,所以只要将新软件提供给入度为0的节点,图中所有的节点都将获得新软件。

对于一般图而言,将原图采用Tarjan算法缩点后会形成新的DAG,只需要提供缩点后入度为0的节点的个数的新软件,就可以让所有的学校都获得软件。因为缩点后的SCC内部节点都是彼此可达的,SCC中一个节点能够获取软件。其他节点就也都可以获取到软件。

总而言之,至少需要提供缩点后入度为0的节点个数个新软件,就能够使得软件被传递到所有的学校。

第二个问题,至少需要添加几条支援关系,才能够使得将新软件提供给任意一个学校,所有学校都可以获取到该软件。

SCC中的节点是彼此可达的,因此我们只需要考虑缩点后的DAG中要添加几条有向边,才能够让图变成一个完整的SCC

在之前的分析中,我们知道,任意一个出度为0的节点都可以通过某入度为0的节点到达,后面我们将入度为0的节点称为起点,出度为0的节点称为终点。也就是说,每一个终点,都可以由某个起点到达,所以只需要连接每个终点到他们对应的起点,那么起点能够到达的位置,该终点也可以到达,比如连接51,那么1原本可以到达的节点,5也都可以到达了,同时,由于51的连接,原图中的起点和终点都少了一个,如果再连接72,图中的起点和终点又都少了一个。现在图中只剩下一个出度为0的节点也就是6,将6连接到1,此时的图就变成了一个SCC,也就是出度和入度为0的节点消失了。

因此,我们可以从入度和出度的角度来看待该问题,假设图的初始状态有m个入度为0的节点和n个出度为0的节点,我们的目标是添加边将其转化为出度或者入度等于0节点的个数为0的一个强连通图。正如上面的例子那样,我们增加一条边,最多可以同时减去一个起点和一个终点,如果起点个数m < n,那么我们前m次消掉了m个起点和终点,后n - m次消掉了n - m个终点,也就是至少n次操作才能将原图转换为不存在出度或者入度是0节点的图;同理m > n时,至少要通过m次加边操作才能够实现目标。

不含有出度为0和入度为0的节点只是强连通图的一个充分条件而已,所以至少要max(mn)次加边操作才能实现目的。

下面证明max(mn)次操作可以将原图转化为强连通图,m > n时,我们每次连接一条终点到起点的边就可以消去一个入度为0的节点以及一个出度为0的节点,这里说的入度为0或者出度为0节点的个数即使是在加边缩点后依旧是减少的。所以n次操作之后只会留下m - n个起点,从原先任意一个终点连m - n条边到达剩下的起点,之后原图就会转化为一个强连通图;m < n的情况也是类似,所以,通过max(m, n)次加边操作可以实现目标。

综上所述,至少需要加max(m, n)条支援关系,才能够使得将新软件提供给任意一个学校,所有学校都可以获取到该软件。

有了上面的结论,我们只需要对原图做一遍Tarjan算法,统计下缩点后入度或者出度为0节点的个数即可得出答案。要注意的是,如果原图本身就是一个强连通图,也就是SCC数目为1时,不需要添加任何支援关系,都可以使得将新软件提供给任意一个学校,所有学校都可以获取到该软件。

三、实现代码

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

const int N = 110, M = 10010;

int n;
int dfn[N], low[N], ts;
int stk[N], top;
int in_stk[N];
int id[N], scc_cnt;
int din[N], dout[N];

// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

// Tarjan算法求强连通分量
void tarjan(int u) {
    dfn[u] = low[u] = ++ts;
    stk[++top] = u;
    in_stk[u] = 1;
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (in_stk[v])
            low[u] = min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) {
        ++scc_cnt;
        int x;
        do {
            x = stk[top--];
            in_stk[x] = 0;
            id[x] = scc_cnt;
        } while (x != u);
    }
}

int main() {
    scanf("%d", &n);
    memset(h, -1, sizeof h);

    for (int i = 1; i <= n; i++) {
        int c;
        while (scanf("%d", &c), c) add(i, c); // i支援t
    }

    for (int i = 1; i <= n; i++)
        if (!dfn[i]) tarjan(i);

    // 统计缩点后的DAG此DAG中出度为0、入度为0的点的个数
    for (int u = 1; u <= n; u++)
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            int a = id[u], b = id[v];
            if (a != b)
                dout[a]++, din[b]++;
        }

    int a = 0, b = 0;
    for (int i = 1; i <= scc_cnt; i++) {
        if (!din[i]) a++;  // 入度为0的强连通块
        if (!dout[i]) b++; // 出度为0的强连通块
    }

    // 输出入度为0的连通块数量,也就是需要软件的数量
    printf("%d\n", a);

    if (scc_cnt == 1) // 如果只有一个强连通块,不用连边
        puts("0");
    else
        printf("%d\n", max(a, b)); // 输出入度为0与出度为0的连通块最大值
    return 0;
}