题目地址:
https://www.acwing.com/problem/content/398/
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
输入格式:
输入文件有若干组数据,每组数据的第一行是一个正整数
N
N
N,表示工地的隧道数。接下来的
N
N
N行每行是用空格隔开的两个整数
S
S
S和
T
T
T,表示挖煤点
S
S
S与挖煤点
T
T
T由隧道直接连接。注意,每组数据的挖煤点的编号为
1
∼
M
a
x
1∼Max
1∼Max,其中
M
a
x
Max
Max表示由隧道连接的挖煤点中,编号最大的挖煤点的编号,可能存在没有被隧道连接的挖煤点。输入数据以
0
0
0结尾。
输出格式:
每组数据输出结果占一行。其中第i
行以Case i:
开始(注意大小写,Case
与i
之间有空格,i
与:
之间无空格,:
之后有空格)。其后是用空格隔开的两个正整数,第一个正整数表示对于第i
组输入数据至少需要设置几个救援出口,第二个正整数表示对于第i
组输入数据不同最少救援出口的设置方案总数。输入数据保证答案小于
2
64
2^{64}
264,输出格式参照以下输入输出样例。
数据范围:
1
≤
N
≤
500
1≤N≤500
1≤N≤500
1
≤
M
a
x
≤
1000
1≤Max≤1000
1≤Max≤1000
题目里涉及到去掉某个点之后,剩余图里的点与点之间的连通问题,可以先将整个图求一下点双连通分量(点双连通分量是指没有割点的极大连通子图),然后缩点之后再考虑。注意在点双连通分量的缩点里,割点会在每个点双连通分量里多“复制”一份,这是因为割点它本身是会出现在多个点双连通分量里的(也就是说点双连通分量并不能将整个图的点做划分,这和边双连通分量和强连通分量非常不一样)。显然每个点双连通分量可以单独考虑,总出口个数可以用加法原理计算,而方案数可以用乘法原理计算。对每个点双连通分量分下面几个情况考虑:
1、如果该连通分量里没有割点,那么它自己就是个连通块。这个时候有两种可能:
它自己就是单独的一个点,此时这个点要设置个出口(由于题目里的数据边数是正的,所以当某条边的某个点坍塌之后,这个孤立点的人也要有个出口,所以这个孤立点本身需要设置一个出口)。出口数是
1
1
1,方案数是
1
1
1;
它不是个单独的点,那么这个时候至少要设两个出口,如果设一个出口的话,这个出口坍塌了就不行了。而两个出口是够的,由于这个连通块没有割点,所以删掉任意一个点之后剩余的点还是连通的,所以总可以走到另一个出口处。所以出口数是
2
2
2,设该连通块有
c
c
c个点,则方案数是
c
(
c
−
1
)
/
2
c(c-1)/2
c(c−1)/2;
2、如果该连通分量里有一个割点,那么这个割点处坍塌的话,这个连通分量的人就走不到其它连通块了,所以在这个连通分量的非割点处一定要设一个出口,在任意非割点的点都可以,所以出口数是
1
1
1,设该连通分量有
c
c
c个点,则方案数是
c
−
1
c-1
c−1;
3、如果该连通分量里有多于
1
1
1个割点,那么这个连通分量里不需要设出口,可以从这个连通分量里的任一点开始走,走到割点处,然后走到另一个不同的点双连通分量里,再沿着这个连通分量的另一个割点走,再走到下一个不同的连通分量里,一定能走到某个只有一个割点的连通分量里(如果走不到的话说明某些点双连通分量可以成环,但是这是不可能的,因为这个环里的任意一个点都不可能是割点)。所以一旦这个连通分量里的任意一个点坍塌了,总能走到那个“只有一个割点的连通分量里”,而这个连通分量里是有一个出口的。所以出口数是
0
0
0,方案数是
1
1
1。
只需求出所有点双连通分量的割点数和总点数,再用加法和乘法原理即可。求点双连通分量可以用Tarjan算法,思路类似求边双连通分量的算法,参考https://blog.csdn.net/qq_46105170/article/details/117375502,但是需要注意的是,在求出某个割点连接的点双连通分量的时候,不要把割点出栈,因为割点有可能要被多个连通分量所共享(当然如果没有割点的话,这么做也是成立的,所以不需要特判这个)。代码如下:
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int N = 1010, M = 510 << 1;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top, dcc_cnt;
// 记录每个点双连通分量有哪些点
vector<int> dcc[N];
// 记录某个点是否是割点
bool cut[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void tarjan(int u, int from) {
// 特判一下u是孤立点的情况
if (u == from && h[u] == -1) {
dcc_cnt++;
dcc[dcc_cnt].push_back(u);
return;
}
// u不是孤立点,则开始DFS
dfn[u] = low[u] = ++timestamp;
stk[top++] = u;
// cnt记录u有多少个互不连通的孩子
int cnt = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
// v无法不通过DFS树而绕到u的上面,那么就产生了一个新的点双连通分量;
// 但是u是否是割点还需要后续判断
if (low[v] >= dfn[u]) {
// 互不连通孩子数加1
cnt++;
// 把当前分出的点双连通分量求出来
dcc_cnt++;
int y;
do {
y = stk[--top];
dcc[dcc_cnt].push_back(y);
// 注意这里不把u出栈,只出栈到v,因为u可能会被多个点双连通分量所共享
} while (y != v);
// 把u也要加进这个连通分量里去。无论u是不是割点,这一步都是要做的
dcc[dcc_cnt].push_back(u);
}
} else low[u] = min(low[u], dfn[v]);
}
// u是割点有两种情况,要么u不是树根,并且其有至少一个互不连通的孩子,
// 这个时候u的上面和下面在u不存在的情况下就不连通了;要么u是树根,
// 那么其要有至少两个不连通孩子才能是割点,这两个不连通孩子在u不存在的情况下不连通
cut[u] = u == from && cnt > 1 || u != from && cnt;
}
int main() {
int T = 1;
while (cin >> m, m) {
// 清空用到的所有数组和变量
for (int i = 0; i <= dcc_cnt; i++) dcc[i].clear();
idx = n = timestamp = top = dcc_cnt = 0;
memset(h, -1, sizeof h);
memset(dfn, 0, sizeof dfn);
memset(cut, 0, sizeof cut);
for (int i = 1; i <= m; i++) {
int a, b;
cin >> a >> b;
n = max(n, a), n = max(n, b);
add(a, b), add(b, a);
}
for (int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i, i);
int res = 0;
long num = 1;
for (int i = 1; i <= dcc_cnt; i++) {
int cnt = 0, sz = dcc[i].size();
for (int j = 0; j < sz; j++)
if (cut[dcc[i][j]])
cnt++;
if (!cnt) {
// 特判孤立点
if (sz > 1) res += 2, num *= sz * (sz - 1) / 2;
else res++;
} else if (cnt == 1) res++, num *= sz - 1;
}
printf("Case %d: %d %ld\n", T++, res, num);
}
return 0;
}
每个测试数据时间复杂度 O ( n + m ) O(n+m) O(n+m),空间 O ( n ) O(n) O(n), n n n和 m m m分别是图的点数和边数。