洛谷 P3225 [HNOI2012]矿场搭建 题解
思路
先求出所有点双连通分量,注意tarjan弹栈时要保留u,因为u有可能会被其它分量包含。
然后对每个分量单独分类讨论。
1. 分量内无割点,且分量点数大于1
任选两个点作为出口即可,因为坍塌后剩余部分仍然连通。
所以对个数的贡献是
2. 分量内无割点,且分量点数等于1
显然这个点一定要是出口,对个数的贡献是
3. 分量内有一个割点
如果坍塌的点不是割点,那剩下的点可以和其它连通分量互通。
如果坍塌的点是割点,那剩下的点里至少要有一个出口。
所以任选除割点外一点作为出口即可,对个数的贡献是
4.分量内割点数量大于1
无论坍塌的是哪个点,该分量都会和其它分量连通,所以无需新增出口。
tips
根据加法原理和乘法原理,统计个数时用加法,统计方案数时用乘法。
存储方案数的变量要开 unsigned long long。
Code
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 1005;
struct Edge{
int to, nxt;
}g[N];
int h[N], idx;
int dfn[N], low[N], vist;
int sta[N], top;
vector<int> dcc[N];
int dcc_cnt, root;
bool cut[N];
void add(int a, int b)
{
g[++idx].to = b, g[idx].nxt = h[a], h[a] = idx;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++vist;
sta[++top] = u;
if (u == root && !h[u])
{
dcc[++dcc_cnt].push_back(u);
return;
}
int cnt = 0;
for (int i = h[u]; i; i = g[i].nxt)
{
int j = g[i].to;
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
if (low[j] >= dfn[u])
{
cnt ++;
if (u != root || cnt > 1)
cut[u] = true;
dcc_cnt ++;
int y;
do {
y = sta[top--];
dcc[dcc_cnt].push_back(y);
} while(y != j);
dcc[dcc_cnt].push_back(u);
}
}
else
low[u] = min(low[u], dfn[j]);
}
}
void init()
{
memset(h, 0, sizeof h);
memset(dfn, 0, sizeof dfn);
idx = vist = top = 0;
for (int i=1;i<=dcc_cnt;i++)
dcc[i].clear();
dcc_cnt = 0;
memset(cut, false, N);
}
void work(int m)
{
int n = 0;
while (m--)
{
int a, b;
scanf("%d%d", &a, &b);
n = max(n, max(a, b));
add(a, b);
add(b, a);
}
for (root=1;root<=n;root++)
if (!dfn[root])
tarjan(root);
int ans = 0;
unsigned long long sum = 1;
for (int i=1;i<=dcc_cnt;i++)
{
int cnt = 0;
for (int j : dcc[i])
if (cut[j])
cnt ++;
if (cnt == 0)
{
if (dcc[i].size() > 1)
{
ans += 2;
sum *= dcc[i].size() * (dcc[i].size() - 1) / 2;
}
else
ans ++;
}
else if (cnt == 1)
{
ans ++;
sum *= dcc[i].size() - 1;
}
}
printf("%d %llu\n", ans, sum);
}
int main()
{
for (int T=1;;T++)
{
int m;
scanf("%d", &m);
if (m == 0)
return 0;
init();
printf("Case %d: ", T);
work(m);
}
return 0;
}