题解 P3225 【[HNOI2012]矿场搭建】

· · 题解

来发一发找双连通分量的tarjan模板解法。。。

加深一下对tarjan的理解

思路还是找出割点和所有的双连通分量,然后统计每个双连通分量里的个点的个数分情况讨论。

  1. 若该连通分量里有不少于两个割点,则它是安全的,因为无论哪个割点炸了,里面的点可以通过其他的没炸的割点跑到其他的双连通分量里去。

  2. 若该连通分量里只有一个割点,那么如果这个割点炸了,则里面的点就不可能跑到其他的双连通分量里去了,所以要在这个割点里建一个出口。

  3. 若该连通分量里一个割点也没有,说明它与外界完全不连通,这时如果只建一个出口的话,那么如果这个出口炸了就GG,所以还需要另一个出口“以防万一”*(即建两个出口)

对于方案数的话,我们发现如果要建出口的话,该双连通分量里的任何一个非割点的节点都是可以的,因此用一下组合数学就可以搞定了。

好像偏离了主题……

主要想讨论的是找出并统计双联通分量的信息事实上也可以使用tarjan的模板来解决,只是判断的位置及条件变了一下(相对于求强连通分量的tarjan算法来说)或者判断里做的事变了一下(相对于求割点的tarjan算法来说)而已。

忽然感觉自己好啰嗦啊,为了不让小伙伴们心烦,那就上代码吧:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
using namespace std;
const int MAXN=501;
long long int ans=0,fanganshu=1;//记录最终的答案,出口数与方案数 
int n,m,u,v,fenliangtot=0,casenum=0;//顶点数、边数、储存边的信息的临时变量(u、v)、连通分量的总数、数据组数 
int zhan[MAXN],top=0;//dfs时用到的栈 
int visit=0,dfn[MAXN],low[MAXN];//tarjan“必备组件” 
int root[MAXN],num[MAXN],roottot=0;//在进行tarjan时记录根节点以及删除该节点后的分量个数
//在做题时怕会出现图不连通的情况,于是开了个roottot记录tarjan时根节点的个数
bool vis[MAXN];//dfs时用(事实上感觉像一个弱化了的dfn数组) 
struct fenliang//记录图的每一个“联通分量”的数据 
{
    int num,gediannum;//连通分量中的非割点数及割点数 
    fenliang() {num=0;gediannum=0;}
}group[MAXN];
struct edge//邻接表存图 
{
    int v;
    edge *NEXT;
    edge() {v=0;NEXT=NULL;}
}*con[MAXN];
inline void ins(int start,int end)//往图中加边 
{
    edge *p=new(edge);
    p->v=end;
    p->NEXT=con[start];
    con[start]=p;
}
inline void tarjan(int nv)//tarjan模板 
{
    dfn[nv]=low[nv]=++visit;
    for(edge *p=con[nv];p;p=p->NEXT)
    {
        //nv跟vv在同一条边上,则有两种情况(在搜索树中):
        //1、vv是nv的祖先,此时所遍历到的边就是一条后向边
        //2、vv是nv的儿子节点
        int vv=p->v;
        if(dfn[vv]==0)//如果vv之前没有被访问过,则vv是nv的儿子 
        {
            tarjan(vv);
            low[nv]=min(low[nv],low[vv]); //递归以及更新 
            if(low[vv]>=dfn[nv]) ++num[nv];
            //low[vv]>=dfn[nv]说明在nv的这一棵子树中没有任何一个节点会连到nv的祖先上去
            //所以去掉nv后图就不连通了(nv是割点)
            //但因为无论根节点去掉之后图是否连通,low[vv]>=dfn[nv]一定会成立,所以要减1(见main函数) 
        }
        else low[nv]=min(low[nv],dfn[vv]);//否则就是情况一 
    }
}
inline void dfs(int nv)
{
    vis[nv]=true;
    zhan[++top]=nv;//在遍历时把遍历到的节点入栈 
    for(edge *p=con[nv];p;p=p->NEXT)
    {
        int vv=p->v;
        if(vis[vv]==false)
        {
            dfs(vv);//事实上这个函数和上面tarjan也就是多了下面的判断 
            //在有向图求强连通分量时,我们使用过一个栈,在栈中的元素有可能在同一个强连通分量里面
            //而对于这个无向图,同样的道理,只是我们把强连通分量换成了双联通分量 
            //具体操作事实上和在求强连通的分量时的if(low[nv]==dfn[nv])时一样的
            //只不过一个点可能会在多个双联通分量里面,而一个点最多只会在一个强连通分量里面
            //所以while要换到前面来,并且在最后加一个特判(判断这个点是不是一个割点) 
            if(low[vv]>=dfn[nv])
            {
                ++fenliangtot; //求到了一个双连通分量 
                int place=zhan[top];
                while(place!=nv)
                {
                    --top;
                    if(num[place]) ++group[fenliangtot].gediannum; //现在这个点是割点 
                    else ++group[fenliangtot].num; //现在这个点不是割点 
                    place=zhan[top];
                }
                //对于这一棵子树的根节点的特判 
                if(num[place]) ++group[fenliangtot].gediannum;
                else ++group[fenliangtot].num;
            }
        }
    }
}
inline int read()//日常读入优化 
{
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();}
    while(c>='0'&&c<='9') {x=x*10+c-'0';c=getchar();}
    return x*f;
}
int main()
{
    while(true)
    {
        m=read();//读入边数 
        if(m==0) return 0;//输入结束 
        memset(con,0,sizeof(con));
        memset(dfn,0,sizeof(dfn));
        memset(num,0,sizeof(num));
        memset(vis,0,sizeof(vis));
        memset(group,0,sizeof(group));
        ans=n=roottot=fenliangtot=0;
        fanganshu=1;
        //该初始化的东西全部初始化一遍 
        while(m--)
        {
            u=read();v=read();
            n=max(n,max(u,v));
            ins(u,v);ins(v,u);
        }//不断往图中加边,因为是无向图,所以加两次 
        for(int i=1;i<=n;++i)
            if(dfn[i]==0)
            {root[++roottot]=i;tarjan(i);}//tarjan找割点 
        //记录根节点是因为根节点要特判,要在tarjan中求出的num值减一才是它的真实的num值 
        for(int i=1;i<=roottot;++i)
            if(num[root[i]]!=0) --num[root[i]];//经过这样处理后所有num值为真的点就是割点了 
        for(int i=1;i<=n;++i)
            if(vis[i]==false) dfs(i);//dfs求点双连通分量 
        for(int i=1;i<=fenliangtot;++i)//遍历求到的双连通分量的数据 
        {
            int a=group[i].num,b=group[i].gediannum;
            if(b>=2) continue;//如果这个双连通分量中割点数大于等于2,说明不要建出口 
            else if(b==1) {++ans;fanganshu*=a;}//如果割点数等于1,则需要建一个出口 
            else if(b==0) {ans+=2;fanganshu*=(a*(a-1)/2);}//如果不存在割点,则要建两个出口 
        }
        printf("Case %d: %lld %lld\n",++casenum,ans,fanganshu);//输出结果
    }
}

希望对各位小伙伴有帮助QAQ。