AcWing 396 矿场搭建

\(AcWing\) \(396\) 矿场搭建

一、题目描述

煤矿工地可以看成是由 隧道 连接 挖煤点 组成的 无向图

为安全起见,希望在工地发生事故时 所有挖煤点的工人都能有一条出路逃到 救援出口处

于是矿主决定在 某些挖煤点 设立 救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。

请写一个程序,用来计算 至少 需要设置几个救援出口,以及不同最少救援出口的设置方案总数。

输入格式
输入文件有若干组数据,每组数据的第一行是一个正整数 \(N\),表示工地的隧道数。

接下来的 \(N\) 行每行是用空格隔开的两个整数 \(S\)\(T\),表示挖煤点 \(S\) 与挖煤点 \(T\) 由隧道直接连接。

注意,每组数据的挖煤点的编号为 \(1\)\(Max\),其中 \(Max\) 表示由隧道连接的挖煤点中,编号最大的挖煤点的编号,可能存在没有被隧道连接的挖煤点。

输入数据以 \(0\) 结尾。

输出格式
每组数据输出结果占一行。

其中第 \(i\) 行以 \(Case\) \(i\): 开始(注意大小写,\(Case\)\(i\) 之间有空格,\(i\) 与 : 之间无空格,: 之后有空格)。

其后是用空格隔开的两个正整数,第一个正整数表示对于第 \(i\) 组输入数据至少需要设置几个救援出口,第二个正整数表示对于第 \(i\) 组输入数据不同最少救援出口的设置方案总数。

输入数据保证答案小于 \(264\),输出格式参照以下输入输出样例。

数据范围
\(1≤N≤500,1≤Max≤1000\)

输入样例

9
1  3
4  1
3  5
1  2
2  6
1  5
6  3
1  6
3  2
6
1  2
1  3
2  4
2  5
3  6
3  7
0

输出样例

Case 1: 2 4
Case 2: 4 1

二、题解

1、加法原理 与 乘法原理

首先,这个无向图不一定是连通的,所以可以把它分成若干个 连通块 来讨论,对于每个连通块,标记数最少直接 累加 (加法原理),方案数用 乘法原理

2、点双与割点

以测试用例画一张图,标识出点双连通分量绿色和紫色两个区域,其中,点\(1\)是割点。

#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 1010;
int n, m;
int dfn[N], low[N], stk[N], ts, top, root;

int bcnt;
vector<int> bcc[N];
bool cut[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++;
}

void tarjan(int u, int fa) {
    low[u] = dfn[u] = ++ts;
    stk[++top] = u;
    int son = 0;
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue;
        if (!dfn[v]) {
            son++;
            tarjan(v, u);
            low[u] = min(low[u], low[v]);

            if (low[v] >= dfn[u]) {
                int x;
                if (u != root || son > 1) cut[u] = 1;
                bcnt++;
                do {
                    x = stk[top--];
                    bcc[bcnt].push_back(x);
                } while (x != v);
                bcc[bcnt].push_back(u);
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }

    if (fa == -1 && son == 0) bcc[++bcnt].push_back(u);
}

int main() {
#ifndef ONLINE_JUDGE
    freopen("396_Prepare.in", "r", stdin);
#endif
    memset(h, -1, sizeof h);
    scanf("%d", &m);
    while (m--) {
        int a, b;
        scanf("%d %d", &a, &b);
        n = max(n, a), n = max(n, b); // 鄙视一下~
        if (a != b) add(a, b), add(b, a);
    }

    for (root = 1; root <= n; root++)
        if (!dfn[root]) tarjan(root, -1);

    cout << "点双个数:" << bcnt << endl;

    for (int i = 1; i <= bcnt; i++) {
        cout << "点双编号:" << i << ", 内部节点:";
        for (int j = 0; j < bcc[i].size(); j++)
            cout << bcc[i][j] << " ";
        cout << endl;
    }
    return 0;
}

3、思路

很显然能够联想到 点双,我们考虑一个点双里面的 割点数量

  • \(0\)个时 说明这个点双与其他的点双之间没有联系,那么这个点双内部至少要建两个救援出口,因为是点双,所以建在哪里都可以,不能只建一个,因为如果恰好是那个点塌了就完了。当然,如果此点双是特殊的点双,也就是 只有一个节点 的情况,那么需要建一个。

  • \(1\)个时 说明这个点双与另一个点双之间有联系,那么这个点双里面就只需要建一个救援出口,如果这个出口塌了,剩下的点也可以去另一个点双里面找救援出口,如果那个割点塌了,那么这个点双内的点去这个点双里面的救援出口就好了。

  • ③ 大于等于\(2\)个时 无论哪一个点塌了,这个点双里面的点都可以去其他点双里面找救援出口,所以这个点双里面不需要建救援出口。

注意:不能在割点上建救援出口,这样一下子就断了两个方向来源的路线,在这创建救援出口不是傻吗?

4、总结

  • 无割点,与其它连通块彼此独立
    • 点双中点的数量为\(1\)
      \(1\)个出口,方案数 \(*= 1\) 注: 题目中数据似乎没有特意卡这个~

    • 点双中点的数量大于\(1\)
      \(2\)个出口,方案数 \(*= C_{cnt}^{2} = cnt*(cnt-1)/2\)

  • 割点数量\(=1\)
    \(1\)个出口,方案数 \(*= C_{cnt-1}^{1} = cnt-1\) (不包含割点)
  • 割点数量\(=2\)
    \(0\)个出口,方案数 \(*= 1\)

三、实现代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010, M = N << 2;
int n, m;

// 链式前向星
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++;
}
/*
1. 用tarjan跑出所有的v_bcc和 原图中哪些点是割点
2. 遍历每个v_bcc(点双),考查点双里的割点个数:
  (1). 若此点双内包含的割点数>1,则无论哪一个节点被毁,连通性依旧,不用处理。贡献为0
  (2). 若此点双内包含的割点数=1,则在分量内任意一点建一个(割点处不用建),一旦分量内建立好的救援出口被毁,
  可以通过割点跑到相临的分量中,走别人的救援出口,ans*=bcc.size()-1。贡献为1
  (3). 若此点双内包含的割点数=0,则任建两个,ans=bcc.size()(bcc.size()-1)/2。贡献为2
*/
int dfn[N], low[N], stk[N], ts, top, root;
int bcnt;
vector<int> bcc[N]; // 双连通分量
bool cut[N];        // 记录割点的桶,割点可能会重复,所以用桶来记录,最后用循环来统计
void tarjan(int u, int fa) {
    low[u] = dfn[u] = ++ts;
    stk[++top] = u;
    int son = 0; // 求割点时,需要记录点双中节点的数量,用于判断是不是单个节点组成的
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue;
        if (!dfn[v]) {
            son++;
            tarjan(v, u);
            low[u] = min(low[u], low[v]);

            // 总结:需要孩子仔细理解原理,能清晰的讲解原理,并默写画出原理图,才能把代码写明白,不能靠简单的背代码,会记不住的
            // 原理 : https://www.cnblogs.com/littlehb/p/16091406.html
            if (low[v] >= dfn[u]) {
                int x;
                // 记录割点
                if (u != root) cut[u] = 1;
                if (u == root && son >= 2) cut[u] = 1; // 如果u是根节点,但是它至少有两个子节点:则u是割点
                // 如果u是根,并且有1个子节点,那么去掉u后,剩下的那个节点还是双点,所以,u不是割点

                // 记录点双中节点有哪些
                bcnt++;
                do {
                    x = stk[top--];
                    bcc[bcnt].push_back(x);
                } while (x != v);       // 将子树出栈
                bcc[bcnt].push_back(u); // 把割点/树根也丢到点双里
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }
    // 因为上面枚举的是边,如果是一个孤立的根,是没有边的,上面的代码不会执行,但它确实是一个点双
    if (u == root && son == 0) bcc[++bcnt].push_back(u);
}

int main() {
    int T = 1;
    while (scanf("%d", &m), m) {
        // 每次清除上次记录的bcnt连通块中点的向量数组
        for (int i = 1; i <= bcnt; i++) bcc[i].clear();

        // n:这题太讨厌了,n居然让我们自己取max计算出来,shit~
        idx = n = ts = top = bcnt = 0;
        memset(h, -1, sizeof h);    // 初始化链式前向星
        memset(dfn, 0, sizeof dfn); // 每个节点的dfs序时间戳
        memset(low, 0, sizeof low);
        memset(stk, 0, sizeof stk); // 栈
        memset(cut, 0, sizeof cut); // 清空割点数组

        while (m--) {
            int a, b;
            scanf("%d %d", &a, &b);
            n = max(n, a), n = max(n, b); // 鄙视一下~
            if (a != b) add(a, b), add(b, a);
        }

        for (root = 1; root <= n; root++)
            if (!dfn[root]) tarjan(root, -1); // 以root为根开始找出 割点 和 点双

        int res = 0; // 增加的救援出口个数
        LL num = 1;  // 增加的救援出口方案数

        for (int i = 1; i <= bcnt; i++) {           // 枚举每个点双
            int cnt = 0;                            // 此点双中割点的数量
            for (int j = 0; j < bcc[i].size(); j++) // 枚举点双中每个点,通过cut这个桶判断是不是割点
                if (cut[bcc[i][j]]) cnt++;

            if (cnt == 0) { // 如果没有割点
                // 如果点双中点的数量大于1,救援出口需要在bcc[i].size()中选择两个,一个坏了还可以走另一个
                if (bcc[i].size() > 1)
                    res += 2, num *= bcc[i].size() * (bcc[i].size() - 1) / 2;
                else
                    // 如果点双中点的数量等于1,孤立的点,那么必须单独设立一个救援出口方案数量不用变化
                    res++;
            } else if (cnt == 1)                 // 如果有一个割点
                res++, num *= bcc[i].size() - 1; // 需要添加一个救援出口
                                                 // 如果有2个或以上的割点,就不用管了,因为一旦某个割点被毁,可以走另一个
        }
        printf("Case %d: %d %lld\n", T++, res, num); // 救援出口个数,方案数
    }
    return 0;
}
posted @ 2022-04-04 15:04  糖豆爸爸  阅读(135)  评论(0编辑  收藏  举报
Live2D