POJ 2186 Popular Cows(強連通分量縮點,Tarjan演算法)

gungnir__發表於2015-11-16

【題目連結】
http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=16578

【解題報告】
給你一個有向圖,問你有多少個點可以被其它所有點訪問到。
因為圖中存在環,而環裡的節點可以互相訪問到,所以我們應當將一個環縮為一個點(這是不影響對題目的分析的)。
這樣這個有向圖就變成了DAG(有向無環圖)。
容易看出,在這個圖中,如果只存在一個節點出度為0,那麼它可以被其它所有結點訪問到。如果存在多個節點出度為0,那麼本題就無解。(n個點被分為了多棵樹,每顆樹無法相互訪問)

那麼這道題的核心就在於縮點這個步驟。

關於縮點所需要的tarjan演算法,在下面給出的連結裡有相當清晰的說明,這裡不再贅述。其它說明請參考程式碼註釋。

ps:其實看懂了tarjan演算法之後會覺得這個演算法思路很易懂程式碼也相當簡潔的。不過網上的資料往往直接描述演算法,而缺乏對本身的分析,所以看起來很難懂。找了一些自己覺得不錯的資料貼在後面,希望能幫助讀者理解求解強連通分量的演算法。

【參考資料】
《 深度理解鏈式前向星 》–Acdreamers
http://blog.csdn.net/acdreamers/article/details/16902023

《處理SCC(強連通分量問題)的Tarjan演算法》–comzyh
https://comzyh.com/blog/archives/517/

《有向圖強連通分量的Tarjan演算法》–byvoid
https://www.byvoid.com/blog/scc-tarjan/

【參考程式碼】

#include<cstdio>
#include<iostream>
#include<cstring>
#include<stack>
#include<cmath>
using namespace std;
const int maxn=1e4+50;
const int maxm=5e4+50;

int head[maxn],LOW[maxn],DFN[maxn],id[maxn],cnt[maxn];
bool mark[maxn];
int N,M,time=0,scc=0;
stack<int>sta;

struct edge_t{
    int to,next;
}edge[maxm];

/*
DFN(u)為節點u搜尋的次序編號(時間戳)
Low(u)為u或u的子樹能夠追溯到的最早的棧中節點的次序號
當DFN(u)=Low(u)時,以u為根的搜尋子樹上所有節點是一個強連通分量。
*/
void tarjan( int u )
{
    DFN[u]=LOW[u]=++time;
    sta.push(u);
    mark[u]=true;
    for( int k=head[u]; k!=-1; k=edge[k].next )
    {
        int v=edge[k].to;
        if(!DFN[v])
        {
            tarjan(v);
            LOW[u]=min( LOW[u],LOW[v] );
        }
        else if(mark[v])//v已經進入過棧但是還沒出棧
        {
            LOW[u]=min(LOW[u],DFN[v]);
        }
    }
    if(DFN[u]==LOW[u])
    {
        scc++;
        int v;
        do
        {
            v=sta.top();
            sta.pop();
            id[v]=scc;  //v這個節點屬於哪個scc
            mark[v]=false;
            cnt[scc]++;  //標記每個scc包含的節點數量
        }
        while(u!=v);
    }
}

void solve()
{
    memset(mark,0,sizeof mark);//用來標記節點是否在棧裡
    memset(DFN,0,sizeof DFN);
    for( int i=1; i<=N; i++ )  if(!DFN[i])  tarjan(i);

    memset(mark,0,sizeof mark);//用來標記某個scc是否有出度
    for( int i=1; i<=N; i++ )
    {
        for( int k=head[i]; k!=-1; k=edge[k].next )
        {
            if(id[i]!=id[edge[k].to])
                mark[id[i]]=true;
        }
    }
    int pos=0,lab=0;
    for( int i=1; i<=scc; i++ )if(!mark[i])
    {
        lab++;
        pos=i;
    }
    if(lab>1)printf("0\n");
    else printf("%d\n",cnt[pos]);
}

int main()
{
    scanf("%d%d",&N,&M);
    memset(head,-1,sizeof head);
    memset(cnt,0,sizeof cnt);//記錄每個scc有多少個節點
    for( int i=0; i<M; i++ )
    {
        int a,b;
        scanf("%d%d",&a,&b);
        edge[i].to=b;  //利用了類似前向星的儲存方法
        edge[i].next=head[a];
        head[a]=i;
    }
    solve();

    return 0;
}

相關文章