题目描述
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
输入
输入文件有若干组数据,每组数据的第一行是一个正整数 N(N≤500),表示工地的隧道数,接下来的 N 行每行是用空格隔开的两个整数 S 和 T,表示挖 S 与挖煤点 T 由隧道直接连接。输入数据以 0 结尾。
输出
输入文件中有多少组数据,输出文件 output.txt 中就有多少行。每行对应一组输入数据的 结果。其中第 i 行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i 与:之间无空格,: 之后有空格),其后是用空格隔开的两个正整数,第一个正整数表示对于第 i 组输入数据至少需 要设置几个救援出口,第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总数。输入数据保证答案小于 2^64。输出格式参照以下输入输出样例。
样例输入
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
提示
Case 1 的四组解分别是(2,4),(3,4),(4,5),(4,6);
Case 2 的一组解为(4,5,6,7)。
题解
这道题与割点密不可分。
可以想象一个连通块,如果中间没有割点,那么只建一个逃生通道肯定是不够的(万一就炸这个),所以至少得有2个,这是一个连通块没有割点的情况。
如果有一个割点并且这个割点把这个连通块和另一个连通块(或者另几个)连接在一起,那么逃生通道只需要建一个就可以了。如果这个逃生通道没有被炸,那么这个连通块的挖煤点直接走这个逃生通道即可;如果不幸地被炸了,那么由于这个连通块没有割点,所以必定除了这个逃生通道能够互相到达,又由于只炸一个点,所以就可以有另外连通块的逃生通道通行。
如果连通块边缘上有不止一个割点,那么这个连通块一个逃生通道都不用——外部总能找到一个逃生通道(确保其他的连通块能逃生),当然这个只是外部有逃生通道的情况,可能有卡这个的图,这个时候可以认为直接建2个(整个地图),就是说:每一个连通块边缘上都有不止一个割点时,需要单独特判。
那么如果一个连通块内部有割点,那么就可以按照割点把这个连通块割成很多个小的连通块,继续进行上面两个操作(判断)。
这是对于本题第一小问的求法。
至于第二小问,求的是组合数学。
假设当前的第二问答案已经累计到了ans,这个连通块(小的)大小为cnt(内部不存在割点,边上如果有割点不能算)。如果取了2个逃生通道,那么就是cnt*(cnt-1)/2种选择方式,把ans与这个值相乘就是这一步累计到的ans。 如果只取一个逃生通道,那么直接ans乘cnt即可。值得注意的是,计算cnt时注意不算割点,而计算小连通块的割点个数num时,一定不能重复计算,有一种方式是用完一个割点就染一个色(可以染成当前小连通块的颜色),这样就能避免重复(不这样就只能得20分)。
大体的思路清楚了,现在就是具体细节了。首先用Tarjan求解割点,注意,图可能不是联通的,所以要更新root再dfs,毕竟root判割点有点特殊。然后对于每一个点枚举是否vis过以及是否是割点(cut)。如果都不是,就可以让染色元素T++并且dfs。此处讲讲dfs,就是深搜像外扩展,遇到没有VIS过的(未染色,也就意味着在同一个连通块内),就染色后继续dfs,同时cnt++;如果遇到边缘的割点,就把割点变成当前颜色(避免重复的操作),并且累加num。注意,每次新dfs时cnt和num都要清零。
大概的写法就是这样,但是观察样例可知,n不是点的个数,但一定是从1开始编号的。因此只需要记录最大的编号,把这个编号作为n继续下面的操作就可以了。
参考代码
#include<cstdio>
#include<cstring>
using namespace std;
int dfn[1000],low[1000],vis[1000],cut[1000];
struct tree
{
int nxt,to;
}tr[1000];
int cases=0,ans1,m;long long ans2,cnt,num;
int head[1000],cnt1=0,n,flag,tot=0,root,T;
int min1(int p,int q) { return p<q?p:q; }
int max1(int p,int q) { return p>q?p:q; }
void build_tree(int u,int v)
{
tr[++cnt1].nxt=head[u];
tr[cnt1].to=v;
head[u]=cnt1;
}
void Tarjan(int k,int f)
{
dfn[k]=low[k]=++tot;
for(int i=head[k];i;i=tr[i].nxt)
{
int to=tr[i].to;
if(!dfn[to])//基本Tarjan写法
{
Tarjan(to,k);
low[k]=min1(low[k],low[to]);
if(low[to]>=dfn[k])
{
if(k==root) flag++;//根节点特判
if(k!=root||flag>=2) cut[k]=1;
}
}
else if(to!=f)
low[k]=min1(low[k],dfn[to]);
}
}
void dfs(int k)
{
vis[k]=T;
if(cut[k]) return;
cnt++;
for(int i=head[k];i;i=tr[i].nxt)
{
int to=tr[i].to;
if(cut[to]&&vis[to]!=T) num++,vis[to]=T;//找割点,去重
if(!vis[to]) dfs(to);
}
}
int main()
{
while(1)
{
cases++;n=-1;
scanf("%d",&m);
if(m==0) break;
cnt1=0;tot=0;T=0;ans1=0;ans2=1ll;
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(vis,0,sizeof(vis));
memset(cut,0,sizeof(cut));
memset(tr,0,sizeof(tr));
memset(head,0,sizeof(head));
//一堆初始化,注意ans2是累乘,所以初值为1
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
build_tree(u,v);
build_tree(v,u);
n=max1(max1(u,v),n);//求出最大编号
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) //判割点
{
root=i;
flag=0;
Tarjan(i,0);
}
}
for(int i=1;i<=n;i++)
{
if(!cut[i]&&!vis[i])
{
T++;cnt=num=0;//开始枚举连通块,并染色
dfs(i);
if(!num)//不存在割点
{
ans1+=2;
ans2*=cnt*(cnt-1ll)/2ll;
}
else if(num==1)//有一个割点
{
ans1++;
ans2*=cnt;
}
}
}
printf("Case %d: %d %lld\n",cases,ans1,ans2);
}
return 0;
}