1. 程式人生 > 其它 >求有向圖的強連通分量個數(kosaraju演算法)

求有向圖的強連通分量個數(kosaraju演算法)

求有向圖的強連通分量個數(kosaraju演算法)
1. 定義

連通分量:在無向圖中,即為連通子圖。

上圖中,總共有四個連通分量。頂點A、B、C、D構成了一個連通分量,頂點E構成了一個連通分量,頂點F,G和H,I分別構成了兩個連通分量。

強連通分量:有向圖中,儘可能多的若干頂點組成的子圖中,這些頂點都是相互可到達的,則這些頂點成為一個強連通分量。

上圖中有三個強連通分量,分別是a、b、e以及f、g和c、d、h。
2. 連通分量的求解方法
對於一個無向圖的連通分量,從連通分量的任意一個頂點開始,進行一次DFS,一定能遍歷這個連通分量的所有頂點。所以,整個圖的連通分量數應該等價於遍歷整個圖進行了幾次(最外層的)DFS。一次DFS中遍歷的所有頂點屬於同一個連通分量。
下面我們將介紹有向圖的強連通分量的求解方法。
3. Kosaraju演算法的基本原理
我們用一個最簡單的例子講解Kosaraju演算法

顯然上圖中有兩個強連通分量,即強連通分量A和強連通分量B,分別由頂點A0-A1-A2和頂點B3-B4-B5構成。每個連通分量中有若干個可以相互訪問的頂點(這裡都是3個),強連通分量與強連通分量之間不會形成環,否則應該將這些連通分量看成一個整體,即看成同一個強連通分量。

我們現在試想能否按照無向圖中求連通分量的思路求解有向圖的強連通分量。我們假設,DFS從強連通分量B的任意一個頂點開始,那麼恰好遍歷整個圖需要2次DFS,和連通分量的數量相等,而且每次DFS遍歷的頂點恰好屬於同一個連通分量。但是,我們若從連通分量A中任意一個頂點開始DFS,就不能得到正確的結果,因為此時我們只需要一次DFS就訪問了所有的頂點。所以,我們不應該按照頂點編號的自然順序(0,1,2,……)或者任意其它順序進行DFS,而是應該按照被指向的強連通分量的頂點排在前面的順序進行DFS。上圖中由強連通分量A指向了強連通分量B。所以,我們按照
B3, B4, B5, A0, A1, A2
的順序進行DFS,這樣就可以達到我們的目的。但事實上這樣的順序太過嚴格,我們只需要保證被指向的強連通分量的至少一個頂點排在指向這個連通分量的所有頂點前面即可,比如
B3, A0, A1, A2, B4, B5
B3排在了強連通分量A所有頂點的前面。
現在我們的關鍵問題就是如何得到這樣一個滿足要求的頂點順序,Kosaraju給出了這解決辦法:對原圖取反,然後從反向圖的任意節點開始進行DFS的逆後序遍歷,逆後序得到的順序一定滿足我們的要求。
DFS的逆後序遍歷

是指:如果當前頂點未訪問,先遍歷完與當前頂點相連的且未被訪問的所有其它頂點,然後將當前頂點加入棧中,最後棧中從棧頂到棧底的順序就是我們需要的頂點順序。

上圖表示原圖的反向。

我們現在進行第一種假設:假設DFS從位於強連通分量A中的任意一個節點開始。那麼第一次DFS完成後,棧中全部都是強連通分量A的頂點,第二次DFS完成後,棧頂一定是強連通分量B的頂點。保證了從棧頂到棧底的排序強連通分量B的頂點全部都在強連通分量A頂點之前。
我們現在進行第二種假設:假設DFS從位於強連通分量B中的任意一個頂點開始。顯然我們只需要進行一次DFS就可以遍歷整個圖,由於是逆後續遍歷,那麼起始頂點一定最後完成,所以棧頂的頂點一定是強連通分量B中的頂點,這顯然是我們希望得到的頂點排序的結果。
上面使用了最簡單的例子說明Kosaraju演算法的原理,對於有多個強連通分量,連線複雜的情況,仍然適用。大家可以自行舉例驗證。
綜上可得,不論從哪個頂點開始,圖中有多少個強連通分量,逆後續遍歷的棧中頂點的順序一定會保證:被指向的強連通分量的至少一個頂點排在指向這個連通分量的所有頂點前面。所以,我們求解強連通分量的步驟可以分為兩步:
(1)對原圖取反,從任意一個頂點開始對反向圖進行逆後續DFS遍歷
(2)按照逆後續遍歷中棧中的頂點出棧順序,對原圖進行DFS遍歷,一次DFS遍歷中訪問的所有頂點都屬於同一強連通分量。

#include <iostream>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <stack>
#include <algorithm>
using namespace std;
#define INF 999999999
#define MAXN 551
#define MOD 1000000009
int n;
int mp[MAXN][MAXN], nmp[MAXN][MAXN], vis[MAXN];
stack<int> s;
void dfs_1(int v)
{
//    cout << v <<endl;
      vis[v] = 1;
      for(int i = 1; i <= n; ++i)
         if(!vis[i] && mp[v][i])
         dfs_1(i);
     s.push(v);
}
void dfs_2(int v)
{
    vis[v] = 1;
    for(int i = 1; i <= n; ++i)
        if(!vis[i] && nmp[v][i])
        dfs_2(i);
}
int kosaraju()
{
    while(!s.empty())
        s.pop();
    memset(vis, 0, sizeof(vis));
    for(int i = 1; i <= n; ++i)
       if(!vis[i])
        dfs_1(i);
       int ans=0;
	memset(vis, 0, sizeof(vis));
    while(!s.empty())
    {
        int v = s.top();
        s.pop();
        if(!vis[v])
        {
//            cout << v <<endl;
            ans++;
            dfs_2(v);
        }
    }
    return ans;
}
int main()
{
    int m, a, b;
    //n個點,m條邊
    cin >> n >>m;
    memset(mp, 0, sizeof(mp));
    memset(nmp, 0, sizeof(nmp));
    for(int i = 0; i < m; ++i)
    {
         cin >> a >>b;
         mp[a][b] = 1;
         nmp[b][a] = 1;
    }
    cout << kosaraju() <<endl;
    return 0;
}
/*
5 5
1 2
2 1
2 3
3 4
4 1
樣例輸出:
2
*/