[BZOJ 2730][HNOI 2012]矿场搭建(Tarjan求割点与桥+计数问题)

题目链接

http://www.lydsy.com/JudgeOnline/problem.php?id=2730

思路

这篇题解是我一个月之前写的,为了凑齐HNOI 2012 Day1的题解所以又发了一遍,23333

注意到点双联通分量的性质:去掉点双中任意一个点,点双中其他点仍然双联通。因此可以发现,在整个图中,一个只含有一个割点的点双联通分量中必须建一个井口,因为若这个点双的割点塌陷,那么这个点双中所有点都将与整个图其他的联通部分不再联通,因此必须建一个井口,而且这个井口绝对不能建立在割点上(建立在割点上,那么割点塌了,这个点双中的矿工照样死翘翘大笑)。但是一个含有多个割点的点双不需要建立井口,因为如果这个点双中塌了一个割点,但是其他割点仍然安然无恙,这个点双中的矿工可以从这些完好的割点撤离。

因此我们只需要先通过Tarjan找出整个图的所有割点和点双,并给每个割点做标记。对于每个只有一个割点的点双,修一个矿井,这样第一问就解决了。

那么还有第二问,其实也比较好做,观察发现每个需要放井口的点双(只有一个割点的点双)中除了割点以外,其他位置都可以修井口,因此用乘法计数便可求出总方案数,第二问也解决了。

注意还有一个特判:如果整个图都是点双,那么只需要修建两个井口(如果只修建一个井口,有可能最终这个井口塌了,剩余的图没井口了,矿工闷死在井里出不来大笑),最终的方案数为在n个不同的点中选2个点的无序组合的方案数,也就是n*(n-1)/2。

代码

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <vector>

#define MAXE 1010
#define MAXV 1010

using namespace std;

typedef long long int LL;

struct edge
{
    int u,v,next;
}edges[MAXE];

int head[MAXE],nCount=0;

void AddEdge(int U,int V)
{
    edges[++nCount].u=U;
    edges[nCount].v=V;
    edges[nCount].next=head[U];
    head[U]=nCount;
}

int low[MAXV],dfn[MAXV],dfs_clock=0;
int bccNo[MAXV]; //bccno[i]=点i所属点双编号
int stack[MAXV],top=0; //Tarjan时用的栈(其中保存的是边的编号)
LL ans1,ans2; //第一问与第二问的答案
bool isCut[MAXV]; //isCut[i]=true表明i是割点
int bccCnt; //bccCnt=点双个数

vector<int>bcc[MAXV]; //bcc[i]存放第i个点双中的点的编号

void Tarjan_BCC(int u,int fa) //Tarjan求点双联通分量
{
    low[u]=dfn[u]=++dfs_clock;
    int childNum=0; //u在DFS中的儿子个数
    for(int p=head[u];p!=-1;p=edges[p].next)
    {
        int v=edges[p].v;
        if(!dfn[v])
        {
            stack[++top]=p;
            childNum++;
            Tarjan_BCC(v,u);
            low[u]=min(low[u],low[v]);
            if(dfn[u]<=low[v]) //找到了一个新的点双联通分量,且u为割点
            {
                isCut[u]=true;
                bccCnt++;
                bcc[bccCnt].clear();
                while(1)
                {
                    int num=stack[top--]; //取出栈顶的一条边
                    if(bccNo[edges[num].u]!=bccCnt) //边上的端点u所在的点双不是当前这个点双或者还没确定它所属的点双编号
                    {
                        bcc[bccCnt].push_back(edges[num].u); //标记边num的端点u是当前点双中的一个点
                        bccNo[edges[num].u]=bccCnt;
                    }
                    if(bccNo[edges[num].v]!=bccCnt) //边上的端点v所在的点双不是当前这个点双或者还没确定它所属的点双编号
                    {
                        bcc[bccCnt].push_back(edges[num].v); //标记边num的端点v是当前点双中的一个点
                        bccNo[edges[num].v]=bccCnt;
                    }
                    if(edges[num].u==u&&edges[num].v==v) //又转回到了边u->v,则找完了这个点双中的所有点
                        break;
                }
            }
        }
        else if(dfn[v]<dfn[u]&&v!=fa)
        {
            stack[++top]=p;
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(fa<0&&childNum==1) //u下面只有一个儿子,而且u是dfs的起点,那么u肯定不是割点
        isCut[u]=false;
}

void FindBCC(int n) //在点数为n的图中找点双
{
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(bccNo,0,sizeof(bccNo));
    memset(isCut,false,sizeof(isCut));
    dfs_clock=bccCnt=0;
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            Tarjan_BCC(i,-1);
}

int n,m;

void solve() //求出最少要多少个井口以及最小答案的不同方案数
{
    FindBCC(n);
    ans1=0,ans2=1;
    for(int i=1;i<=bccCnt;i++) //遍历第i个点双
    {
        int numOfCut=0; //这个点双中的割点个数
        for(int j=0;j<bcc[i].size();j++)
            if(isCut[bcc[i][j]]) //在第i个点双中找到一个割点
                numOfCut++;
        if(numOfCut==1) //有一个割点,则必须要在这个点双中建一口矿井
        {
            ans1++;
            ans2*=(LL)(bcc[i].size()-numOfCut); //这个井口可以建立在除割点以外点双中的任何位置,乘法计数原理统计答案
        }
    }
    if(bccCnt==1) //特判:若整个图都是点双,那么至少要建立2个井口(如果只建立了一个井口,可能这个井口塌了,矿工就出不去了),随便在哪个点建立井口都可以
    {
        ans1=2;
        ans2=bcc[1].size()*(bcc[1].size()-1)/2;
    }
    printf("%lld %lld\n",ans1,ans2);
}

int main()
{
    int TestCase=0;
    while(scanf("%d",&m)!=EOF&&m)
    {
        n=0;
        nCount=0;
        memset(head,-1,sizeof(head));
        for(int i=1;i<=m;i++)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            n=max(n,u);
            n=max(n,v);
            AddEdge(u,v);
            AddEdge(v,u);
        }
        printf("Case %d: ",++TestCase);
        solve();
    }
    return 0;
}

你可能感兴趣的:([BZOJ 2730][HNOI 2012]矿场搭建(Tarjan求割点与桥+计数问题))