P3225 [HNOI2012]矿场搭建
tarjan求割点
P3225 [HNOI2012]矿场搭建
题目描述
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
输入格式
输入文件有若干组数据,每组数据的第一行是一个正整数 \(N\)(\(N\le 500\)),表示工地的隧道数,接下来的 \(N\) 行每行是用空格隔开的两个整数 \(S\) 和 \(T\),表示挖 \(S\) 与挖煤点 \(T\) 由隧道直接连接。输入数据以 \(0\) 结尾。
输出格式
每行对应一组输入数据的 结果。其中第 i 行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i 与:之间无空格,: 之后有空格),其后是用空格隔开的两个正整数,第一个正整数表示对于第 i 组输入数据至少需 要设置几个救援出口,第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总 数。
输入数据保证答案小于 \(2^{64}\)。
先用 tarjan 求出割点,那么显然割点会将原图分成若干个联通块(双联通分量)
点双缩点后会变成一个树(因为不能有环,而他有时无向图,还要求联通,那么就是树),分类讨论:
- 如果原图只有一个双联通分量,那么无论哪个点坍塌,整个图总还是联通的
- 点数大于 \(1\),那么要放 \(2\) 个出口,一个塌了另一个还能用,方案数是 \(\tbinom{n}{2}\),这里 \(n\) 指的是点数,和题中不一样
- 点数等于 \(1\),放一个就行了,个数方案数均为 \(1\)
- 剩余情况
- 一个联通块(双联通分量),如果只与一个割点相连,就是叶子节点,那么在这里肯定要有一个出口,不然割点塌了就出不去了,出口数加一,方案数乘 \(size\)
- 与对于一个割点相连,那么其中一个塌了还能从另外一个走,就不用放了
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
register int x=0;register int y=1;
register char c=std::getchar();
while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
return y?x:-x;
}
#define N 506
#define M 1006
int n,m;
int fir[N],nex[M],to[M],tot;
int cut[N],dfn[N],low[N],dfscnt;
int vis[N];
inline void add(int x,int y){
to[++tot]=y;
nex[tot]=fir[x];fir[x]=tot;
}
inline void clear(){
n=0;tot=0;dfscnt=0;
std::memset(cut,0,sizeof cut);std::memset(fir,0,sizeof fir);
std::memset(to,0,sizeof to);std::memset(nex,0,sizeof nex);
std::memset(low,0,sizeof low);std::memset(dfn,0,sizeof dfn);
std::memset(vis,0,sizeof vis);
}
void tarjan(int u,int fa){//return size of subtree u
dfn[u]=low[u]=++dfscnt;
int children=0;
for(reg int v,i=fir[u];i;i=nex[i]){
v=to[i];
if(!dfn[v]){
tarjan(v,fa);
low[u]=std::min(low[u],low[v]);
if(low[v]>=dfn[u]&&u!=fa) cut[u]=1;
children++;
}
else low[u]=std::min(low[u],dfn[v]);
}
if(children>1&&u==fa) cut[u]=1;
}
int dfs(int u,int &num,int block_id){
//block_id 记录了当前是第几个联通块,它的作用是:
//一个割点在一次 dfs 中,可能会被重复计算,但只能算一次,所以每次计算后打上标记,如果有标记就不在计算了
//但不同的两次 dfs 中,割点是要重复计算的,所以为每个标记区分是在第几次 dfs 中打的
vis[u]=block_id;
int size=1;
for(reg int i=fir[u],v;i;i=nex[i]){
v=to[i];
if(cut[v]&&vis[v]!=block_id) num++,vis[v]=block_id;
if(!cut[v]&&!vis[v]) size+=dfs(v,num,block_id);
}
return size;
}
int main(){
m=read();
for(reg int cases=1;m;cases++){
clear();
for(reg int i=1,u,v;i<=m;i++){
u=read();v=read();
n=std::max(std::max(u,v),n);
add(u,v);add(v,u);
}
for(reg int i=1;i<=n;i++)if(!dfn[i]) tarjan(i,i);
LL ans1=0,ans2=1;
for(reg int i=1,block_id=1;i<=n;i++)if(!vis[i]&&!cut[i]){
int num=0;
int size=dfs(i,num,block_id);block_id++;
if(!num){
if(size==1) ans1++;
else ans1+=2,ans2*=size*(size-1)/2;
}
else if(num==1) ans1++,ans2*=size;
}
std::printf("Case %d: %lld %lld\n",cases,ans1,ans2);
m=read();
}
return 0;
}