HNOI2012矿场搭建
题目连接https://www.luogu.org/problem/P3225
题目描述
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
题意
给出一张连通的无向图,要求在这张无向图中去掉某个点(煤矿坍塌),当去掉这个点后,在剩下的图中选取一些点(出口),保证图中其他的点能和这些点直接或者间接相连(能逃出)。问:无论去掉哪些点,至少需要多少出口,才能保证其他点能安全逃出,这些出口设立有多少可能性。
思路
根据题意,我们很容易看出这题和割点有关,只有割点发生坍塌才是最危险的,为什么呢,割点的性质决定,去掉割点后图会分成几部分,那么出口才是“至少需要的”,所以找出割点是第一步,因为每个割点都会发生坍塌,所以应该对整个割点集进行考虑。
之后,去掉割点后,可以发现我们只需要在每个连通子图中各设立一个出口就行,而方案种数是每个连通子图大小乘积,因此我想到了并查集,可惜wa了,原因就是不应该一次性把所有的割点去掉,题意只是去掉某一个,后来发现特例。
正解是用点双来做的,分三种情况。首先将整张图划分为若干点双,不管怎样点双集一定会包括整张图的边,点双在无向图中有两种情况,一种是形成环,一种是单边(两点一线)。根据点双的性质,去掉点双中某个点不影响连通性,我们对每个点双中割点的数目来进行分类讨论。
1、当点双中割点的数量为0时,这时的点双一定是环状的(单边的话割点数为1或2),在这个封闭的空间中,至少需要选两个出口,选一个不行,因为可能那个出口刚好坍塌,就出不去了。选取的可能性有C_n^2=n(n-1)/2.
2、当点双中割点的数量为1时,点双为环状或者单边,不管怎么样,这个割点可能坍塌,所以还要选一个点作为出口。选取的可能性为 n-1
3、当点双中割点的数量大于等于2时,不需要设立出口。因为这些割点中无论哪个割点坍塌,都不影响其他点与另一个(或多个)割点相连,而这些割点一定会存在在其他的某个点双,这个不证,画图就可理解,割点一定存在在某个点双,点双不一定存在割点。要么在其他的点双中要么割点数继续大于等于2,传递下去,要么到达终点,割点数为1,在那个点双中必然已经设立了一个出口,这时这些点从这个出口逃离就行。
代码
#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define maxm 100005
#define inf 1e9
#define IOS ios::sync_with_stdio(false)
#define ll long long
int head[maxn], next[maxm], tov[maxm], num;
void add(int from, int to)
{
next[++num] = head[from];
tov[num] = to;
head[from] = num;
}
ll n;
struct Edge
{
int st, en;
Edge() {}
Edge(int a, int b)
{
st = a, en = b;
}
};
stack <Edge> palm; //保存路径
vector <Edge> block[maxn]; //同一个点双的边保存在一起
int dfn[maxn], low[maxn];
int ind, T; //ind为点双的数量,T为时间
bool ans[maxn];
void tarjan(int u, int pre)//点双+求割点
{
int rc = 0;
dfn[u] = low[u] = ++T;
for (int i = head[u]; i != -1; i = next[i])
{
int v = tov[i];
if (!dfn[v])
{
palm.push(Edge(u, v));
tarjan(v, u);
if (dfn[u] <= low[v])
{
if (u != pre) //标记割点
ans[u] = true;
for (Edge temp; !palm.empty(); )
{
temp = palm.top();
if (dfn[temp.st]<dfn[v]) break;
block[ind].push_back(temp), palm.pop();
}
block[ind++].push_back(Edge(u, v));
palm.pop();
}
if (u == pre)
rc++;
if (low[u]>low[v]) low[u] = low[v];
}
else if (v != pre && dfn[v]<dfn[u])
{
palm.push(Edge(u, v));
if (low[u]>dfn[v]) low[u] = dfn[v];
}
}
if (u == pre && rc>1) //对根节点的特判,是否为割点
ans[u] = true;
}
void init(int M)
{
for (int i = 0; i <= M; i++)
{
dfn[i] = 0, low[i] = 0;
ans[i] = false;
}
while (!palm.empty())
palm.pop();
for (int i = 0; i <= M; i++)
block[i].clear();
}
int main()
{
IOS;
int k = 1;
while (cin >> n)
{
num = 0, ind = 0, T = 0;
memset(head, -1, sizeof(head));
if (n == 0)break;
int M = 0;
for (int i = 0; i<n; i++)
{
int x, y;
cin >> x >> y;
add(x, y);
add(y, x);
M = max(M, max(x, y));
}
init(M);
tarjan(1, 1);
ll sum = 1, tt = 0;
for (int i = 0; i<ind; i++)
{
int cnt_ans = 0;
bool flag[maxn] = { false };
for (int j = 0; j<block[i].size(); j++)
{
int x = block[i][j].st, y = block[i][j].en;
if (!flag[x] && ans[x])
cnt_ans++;
if (!flag[y] && ans[y])
cnt_ans++;
flag[x] = true, flag[y] = true;
}
ll n_ans = 0;
for (int j = 1; j <= M; j++)
if (flag[j])
n_ans++;
if (cnt_ans == 0)
{
tt += 2;
sum *= n_ans * (n_ans - 1) / 2;
}
if (cnt_ans == 1)
{
sum *= (n_ans - 1);
tt++;
}
}
cout << "Case " << k << ": " << tt << " " << sum << "\n";
k++;
}
return 0;
}