AcWing 396 矿场搭建

AcWing 396 矿场搭建

一、题目描述

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

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

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

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

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

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

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

输入数据以 0 结尾。

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

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

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

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

数据范围
1N5001Max1000

输入样例

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个出口,方案数 =Ccnt2=cnt(cnt1)/2

  • 割点数量=1
    1个出口,方案数 =Ccnt11=cnt1 (不包含割点)
  • 割点数量=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 @   糖豆爸爸  阅读(139)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2014-04-04 CentOS6.9 安装OpenResty
2013-04-04 9、单机运行环境搭建之 --CentOS-6.4下mysqldump 备份与还原数据库
Live2D
点击右上角即可分享
微信分享提示