题解 P3225 【[HNOI2012]矿场搭建】
这里是唯一的一个Tarjan后不用再DFS的题解。
思路:无向图跑Tarjan时就跑出所有割点和DCC(点双连通分量)。
-
如果这个DCC包含了1个以上的割点,那么无论拿一个割点被堵,都可以通过其他割点逃向其他救生点,没有贡献答案。
-
如果这个DCC只有1个割点,如果割点被堵,就在DCC内设立一个逃生点,如果逃生点被堵,就从割点出去。贡献为1,方案数为
size[i]-1
(除去割点本身)。 -
如果这个DCC没有割点。就要用两个逃生点互保。贡献为2,方案数为
size[i]*(size[i]-1)/2
。所有方案数乘起来就是答案。
跑Tarjan时,类似有向图的做法维护一个栈。如果有LOW[y]>=DFN[x]
的情况,就把从y到栈顶的部分一起弹出,他们和x构成了一个DCC(详见代码)。然后统一计算答案。
为什么不能在Tarjan时计算答案:
当你确定一个点双连通分量后,里面的所有点哪些是割点,哪些不是割点还没算完,所以是错的。要等Tarjan完后再算答案。
#include <bits/stdc++.h>
#define MAX (1000 + 7)
#define add push_back
using namespace std;
int T,N,M,K,cnt,ans1,c[MAX],LOW[MAX],DFN[MAX];//c保存是否为割点
long long ans2;
vector <int> L[MAX],DCC[MAX];//DCC数组保存每一个DCC
stack <int> S;
void Tarjan(int x)
{
int fa = 0; S.push(x);
LOW[x] = DFN[x] = ++cnt;
for (int p = 0; p < L[x].size(); p++)
{
const int y = L[x][p];
if (!DFN[y])
{
Tarjan(y);
LOW[x] = min(LOW[x], LOW[y]);
if (LOW[y] >= DFN[x])
{
fa++; K++;
if (x!=1 || fa>1) c[x] = 1;
DCC[K].add(x);
int z = 0;
do{//Tarjan时弹栈并直接求出DCC
z = S.top(); S.pop();
DCC[K].add(z);
}while (z != y);
}
}
else LOW[x] = min(LOW[x], DFN[y]);
}
}
int main()
{
while (scanf("%d", &M) && M)
{
S = stack <int> ();
for (int i = 1; i <= N; i++)
L[i].clear();
for (int i = 1; i <= K; i++)
DCC[i].clear();
N = K = ans1 = cnt = 0; ans2 = 1;
memset(c, 0, sizeof c);
memset(LOW, 0, sizeof LOW);
memset(DFN, 0, sizeof DFN);
for (int i = 1, x = 0, y = 0; i <= M; i++)
{
scanf("%d%d", &x, &y);
L[x].add(y); L[y].add(x);
N = max(N, max(x,y));
}
Tarjan(1);
for (int i = 1; i <= K; i++)
{//统一对每一个DCC计算答案。
int v1 = DCC[i].size(), v2 = 0;
//size()即该DCC的点个数,v2计算割点个数
for (int p = 0; p < v1; p++)
v2 += c[DCC[i][p]];
if (v2 == 1) ans1++, ans2 *= v1 - 1;
if (v2 == 0) ans1 += 2, ans2 *= v1 * (v1-1) / 2;
}
printf("Case %d: %d %lld\n", ++T, ans1, ans2);
}
}