煤矿工地可以看成是由隧道连接挖煤点组成的无向图。
为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。
于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
输入格式
输入文件有若干组数据,每组数据的第一行是一个正整数 N,表示工地的隧道数。
接下来的 N 行每行是用空格隔开的两个整数 S 和 T(S≠T),表示挖煤点 S 与挖煤点 T 由隧道直接连接。
注意,每组数据的挖煤点的编号为 1∼Max,其中 Max 表示由隧道连接的挖煤点中,编号最大的挖煤点的编号,可能存在没有被隧道连接的挖煤点。
输入数据以 0 结尾。
输出格式
每组数据输出结果占一行。
其中第 i 行以 Case i:
开始(注意大小写,Case
与 i
之间有空格,i
与 :
之间无空格,:
之后有空格)。
其后是用空格隔开的两个正整数,第一个正整数表示对于第 i 组输入数据至少需要设置几个救援出口,第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总数。
输入数据保证答案小于 2^64,输出格式参照以下输入输出样例。
数据范围
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
2)分别看每个连通块:
(1)无割点:有两个出口的情况下,无论哪个点坏掉,都满足题意
(2)有割点:进行缩点【1】每个割点单独作为一个点;【2】从每个V-DCC(点双连通分量)向其所包含的每个割点连边
3)V-DCC度数为1
需在该分量内部(非割点)放一个出口
V-DCC度数>1
无需设置出口
视频讲解:https://www.acwing.com/file_system/file/content/whole/index/content/158171/
#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<math.h>
#include<map>
#include<sstream>
#include<deque>
#include<unordered_map>
#include<unordered_set>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> PII;
const int N = 1000 + 5, M = 1e3 + 5, INF = 0x3f3f3f3f;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
vector<int> dcc[N];
int dcc_cnt;
int stk[N], top;
int cut[N];
int root;
// 添加图的边
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void tarjan(int u) {
// 初始化节点的dfn和low为当前时间戳
dfn[u] = low[u] = ++timestamp;
// 将节点u加入栈中
stk[++top] = u;
int cnt = 0;
// 判断是否为割点,即u为图的根节点且没有出边
if (u == root && h[u] == -1) {
// 找到一个双连通分量,更新计数器并保存节点u
dcc_cnt++;
dcc[dcc_cnt].push_back(u);
return;
}
// 遍历节点u的所有出边
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
// 如果节点j未访问,则递归调用Tarjan算法
if (!dfn[j]) {
tarjan(j);
// 更新节点u的low值为其子树中的最小dfn值
low[u] = min(low[u], low[j]);
// 如果u是割点,或者u的子节点j满足dfn[u] <= low[j]
if (dfn[u] <= low[j]) {
cnt++;
if (u != root || cnt > 1)
cut[u] = 1;
int y;
dcc_cnt++;
// 弹栈,将栈中的节点加入当前双连通分量
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) {
// 清空双连通分量
for (int i = 1; i <= dcc_cnt; i++) {
dcc[i].clear();
}
memset(h, -1, sizeof h);
memset(dfn, 0, sizeof dfn);
memset(cut, 0, sizeof cut);
timestamp = idx = dcc_cnt = top = n = 0;
// 读入边的信息并构建图
for (int i = 1, a, b; i <= m; i++) {
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
n = max(n, a), n = max(n, b);
}
// 对每个未访问的节点调用Tarjan算法
for (root = 1; root <= n; root++) {
if (!dfn[root])
tarjan(root);
}
// 计算结果
int ret = 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) {
if (dcc[i].size() > 1)
ret += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2;
else
ret++;
}
else if (cnt == 1)
ret++, num *= dcc[i].size() - 1;
}
// 输出结果
printf("Case %d: %d %llu\n", T++, ret, num);
}
return 0;
}