AcWing 396 矿场搭建


题目传送门

一、解题思路

首先容易想到割点,因为删去割点后,分割出的两个或以上的点双联通分量中,每一个都至少要有一个救援出口。而且在最优方案里,割点上是不能够设置救援出口的。所以算法是显然的:用\(Tarjan\)双连通分量

首先求出所有割点。

没有割点:那么这个点双联通分量中,至少要有两个救援出口,而且每个都可以被选择,这时的选择方案数是\(C_{size}^2\)种,即\(1/2*size*(size?1)\)种。

有割点:

如果某点双连通分量连接的割点只有一个,那么该双连通分量里必须设置一个救援出口,方案数是\(size\)

如果某点双连通分量连接的割点有两个或以上,那么删去其中一个割点后,必然还剩下一些与之相连割点,这个时候只要剩下的割点连接的点双连通分量中有救援出口,它就可以利用它们的救援出口,因此这个点双连通分量中是不需要设置出口的。

\(DFS\)求出点双连通分量时顺便求出连接的割点数即可。最后的方案数采用乘法原理处理。

二、完整代码

#include 
using namespace std;
typedef unsigned long long ULL;
const int N = 1010, M = 1010;
int n, m;
int dfn[N], low[N], timestamp; // tarjan算法求割点
int stk[N], top;               //栈
int dcc_cnt;
vector dcc[N]; //双连通分量
bool cut[N];        //哪些节点是割点
int root;
int h[N], e[M], ne[M], idx;
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
/*
本题思路十分简单,用tarjan跑出所有的v_DCC和割点.
1.若此分量内有割点>1,则无论哪一个被堵,连通性依旧。贡献为0
2.若割点==1,则在割点及分量内任意一点各建一个,ans*=DCC.size()-1。贡献为1
3.若割点==0,则任建两个,ans=DCC.size()(DCC.size()-1)/2。贡献为2

首先一遍tarjan找出割点,将图缩点,这些大点中如果有只包含一个割点的,那么如果这个割点被去掉,则这个大点与图不连通,所以这个大点内必须有一个出口;
而如果没有割点,需要建两个出口,以防止一个出口点被去掉;
方案数就是放出口的大点的size乘积;没有割点则方案数为C(m,2);
注意自己记录点数。
*/
//无向图点双连通性  v?DCC
void tarjan(int u) {
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u;
    if (u == root && h[u] == -1) {
        dcc_cnt++;
        dcc[dcc_cnt].push_back(u); //记录每个连通块中的点
        return;
    }
    int cnt = 0;
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if (low[j] >= dfn[u]) {
                cnt++;
                //记录割点u
                if (u != root || cnt > 1) cut[u] = true;
                dcc_cnt++;
                int y;
                do {
                    y = stk[top--];
                    dcc[dcc_cnt].push_back(y);
                } while (y != j);
                dcc[dcc_cnt].push_back(u);
            }
        } else
            low[u] = min(low[u], dfn[j]);
    }
}
int main() {
    int T = 1;
    while (cin >> m, m) { //多组测试数据
        //每次清除上次记录的dcc_cnt连通块中点的向量数组
        for (int i = 1; i <= dcc_cnt; i++) dcc[i].clear();
        // idx:全新建图
        // n:这题太讨厌了,n居然让我们自己取max计算出来,shit~
        // timestamp:tarjan用的时间戳序号
        // top:tarjan中stack用的变量
        // dcc_cnt:连通分量的个数
        idx = n = timestamp = top = dcc_cnt = 0;
        memset(h, -1, sizeof h);    //链表头,全新建图
        memset(dfn, 0, sizeof dfn); //每个节点的dfs序时间戳序号
        memset(cut, 0, sizeof cut); //清空割点数组

        int a, b;
        for (int i = 0; i < m; i++) {
            cin >> a >> b;
            n = max(n, a), n = max(n, b); //鄙视一下~
            add(a, b), add(b, a);
        }
        //因为可能不是全连通的,需要枚举每个节点,找出所有连通块
        for (root = 1; root <= n; root++)
            if (!dfn[root]) tarjan(root); //以root为根开始找出割点和连通分量

        int res = 0;
        ULL num = 1;
        for (int i = 1; i <= dcc_cnt; i++) { //枚举每个双连通分量
            int cnt = 0;
            for (int j = 0; j < dcc[i].size(); j++) //此分连通分量中有哪些点
                if (cut[dcc[i][j]]) cnt++;          //此双连通分量中有几个割点

            if (cnt == 0) { //如果没有割点
                //如果数量大于2,救援出口可以在dcc[i].size()中选择两个,不区分顺序
                if (dcc[i].size() > 1)
                    res += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2;
                else
                    res++;
            } else if (cnt == 1)                 //如果只有一个割点
                res++, num *= dcc[i].size() - 1; //需要添加一个救援出口
        }
        printf("Case %d: %d %llu\n", T++, res, num); //救援出口个数,方案数
    }
    return 0;
}

相关