求解有向圖的強連通分量的SCC問題---POJ 2186 Popular Cows
演算法虛擬碼:
Kosaraju's algorithm is simple and works as follows:
- Let G be a directed graph and S be an empty stack.
- While S does not contain all vertices:
- Choose an arbitrary vertex v not in S. Perform a depth-first search starting at v.Each
time that depth-first search finishes expanding a vertex u, pushu
- Choose an arbitrary vertex v not in S. Perform a depth-first search starting at v.Each
time that depth-first search finishes expanding a vertex u, pushu
- Reverse the directions of all arcs to obtain the transpose graph.
- While S is nonempty:
- Pop the top vertex v from S. Perform a depth-first search starting at v. The set of visited vertices will give the strongly connected component containing v; record this and remove all these vertices
from the graph G and the stack S. Equivalently,
- Pop the top vertex v from S. Perform a depth-first search starting at v. The set of visited vertices will give the strongly connected component containing v; record this and remove all these vertices
from the graph G and the stack S. Equivalently,
2)複雜度分析
當圖是使用鄰接表形式組建的,Kosaraju演算法需要對整張圖進行了兩次的完整的訪問,每次訪問與頂點數V和邊數 E之和 V+E成正比,所以可以線上性時間O(V+E)內訪問完成。該演算法在實際操作中要比Tarjan演算法要慢,Tarjan演算法只需要對圖進行一次完整的訪問。
當圖是使用鄰接矩陣形式組建的,演算法的時間複雜度為O(V^2)。
3)拓撲排序
拓撲排序有兩種方法,具體方法可以看這篇部落格。其中一種就是基於DFS的拓撲排序。
kosaraju演算法在對原圖進行DFS的時候在遞迴返回後,Each time that depth-first search finishes expanding a vertex u, pushu onto S.
對非有向無環圖來說,是不能進行拓撲排序的,所以實際上的S棧中的的序列實際上是“偽拓撲排序”,因為最終的結果不一定滿足拓撲排序中嚴格的偏序定義,這是由於回邊的存在。
而而這些迴向邊,就是構成強連通分量的關鍵。為了突出這些迴向邊,Kosaraju演算法將圖進行轉置後,原來的迴向邊就都變成正向邊了,對轉置後的圖按照上面”偽拓撲排序“中頂點出現的順序呼叫DFS,而每次呼叫DFS形成的一顆搜尋樹,就構成了原圖中的一個強連通分量。
4)備註
該演算法和Tarjan演算法具有相似(相反?)的性質:每個強連通分量都是在它的所有後繼強連通分量被求出之前求得的。這是因為在對原圖進行DFS時得到了“偽拓撲序的逆”,再對逆圖進行DFS的時候是按照這個“偽拓撲序的逆”的逆進行的,所以連通分量求出順序和原圖縮點後得到新圖的拓撲序一致。因此,如果將同一強連通分量收縮為一個結點而構成一個有向無環圖,這些強連通分量被求出的順序是這一新圖的拓撲序。
題意:每頭奶牛都夢想著成為牧群中最受奶牛仰慕的奶牛。在牧群中,有N 頭奶牛,1≤N≤10,000,給定M 對(1≤M≤50,000)有序對(A, B),表示A 仰慕B。由於仰慕關係具有傳遞性,也就是說,如果A 仰慕B,B 仰慕C,則A 也仰慕C,即使在給定的M 對關係中並沒有(A, C)。你的任務是計算牧群中受每頭奶牛仰慕的奶牛數量。
題解:
由於仰慕的傳遞性,一強連通分量內的牛都互相仰慕,並且如果一個連通分量A內的一頭牛仰慕另一個連通分量B內的另一頭牛,則A連通分量內的所有牛都仰慕B連通分量的所有牛。所以講圖中的連通分量縮為一個點,構造新圖,則這個圖為DAG。最後作一次掃描,統計出度為0 的頂點個數,如果正好為1,則說明該頂點(是一個新構造的頂點,即對應一個強連通分量)能被其他所有頂點走到,即該強連通分量為所求答案,輸出它的頂點個數即可。
程式碼1:(Tarjan演算法)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<stack>
using namespace std;
const int MAX=10000+10;
vector<int> g[MAX];//——鄰接表存圖
stack<int> st;//—棧
int dfn[MAX],low[MAX],instack[MAX];//dfn——dfs訪問次序,low——通過其子樹能訪問到的最小的dfn,instack——是否在棧中
int sccno[MAX],sccsize[MAX];//sccno——每個點強連通分量的標號,sccsize——每個強連通分量內點的個數
int out[MAX];//——強連通分量的出度
int dfs_cnt,scc_cnt;//dfs_cnt——dfs訪問序號,scc_cnt——強連通分量個數
int n,m;//點數,邊數
void dfs(int u)
{
dfn[u]=low[u]=++dfs_cnt;
instack[u]=1;
st.push(u);
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(!dfn[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
else if(instack[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
scc_cnt++;
int v;
do
{
v=st.top(); st.pop();
instack[v]=0;
sccno[v]=scc_cnt;
sccsize[scc_cnt]++;
}
while(u!=v);
}
}
void tarjan()
{
dfs_cnt=scc_cnt=0;
memset(dfn,0,sizeof(dfn));
memset(instack,0,sizeof(instack));
memset(sccno,0,sizeof(sccno));
memset(sccsize,0,sizeof(sccsize));
while(!st.empty()) st.pop();
for(int i=1;i<=n;i++)
{
if(!dfn[i]) dfs(i);
}
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
while(scanf("%d%d",&n,&m)!=EOF)
{
for(int i=1;i<=n;i++)
g[i].clear();
for(int i=0;i<m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
g[u].push_back(v);
}
tarjan();
memset(out,0,sizeof(out));
for(int u=1;u<=n;u++)
{
for(int j=0;j<g[u].size();j++)
{
int v=g[u][j];
if(sccno[u]!=sccno[v])
{
out[sccno[u]]++;
}
}
}
int ans,cou=0;
for(int i=1;i<=scc_cnt;i++)
{
if(out[i]==0)
{
ans=sccsize[i];
cou++;
}
}
printf("%d\n",cou==1?ans:0);
}
return 0;
}
這裡進行了一個改進,根據Kosaraju演算法的性質(連通分量求出順序為縮點後新圖的拓撲序),Kosaraju演算法完成後,拓撲排序其實也完成了,判斷是否與所有連通分量都和拓撲序終點相連即可,如果是則結果為其頂點數,否則為0。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int MAX=10000+10;
vector<int> G[MAX];
vector<int> rG[MAX];
vector<int> vs;
int vis[MAX];
int sccno[MAX];
int scc_cnt;
int n,m;
void dfs(int u)
{
vis[u]=1;
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i];
if(!vis[v]) dfs(v);
}
vs.push_back(u);
}
void rdfs(int u)
{
sccno[u]=scc_cnt;
vis[u]=1;
for(int i=0;i<rG[u].size();i++)
{
int v=rG[u][i];
if(!vis[v]) rdfs(v);
}
}
void kosaraju()
{
vs.clear();
scc_cnt=0;
memset(sccno,0,sizeof(sccno));
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)
{
if(!vis[i]) dfs(i);
}
memset(vis,0,sizeof(vis));
for(int i=vs.size()-1;i>=0;i--)
{
int v=vs[i];
if(!vis[v])
{
scc_cnt++;
rdfs(v);
}
}
}
int main()
{
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
while(scanf("%d%d",&n,&m)!=EOF)
{
for(int i=1;i<=n;i++)
{
G[i].clear();
rG[i].clear();
}
for(int i=0;i<m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
G[a].push_back(b);
rG[b].push_back(a);
}
kosaraju();
int ans=0,u;
for(int i=1;i<=n;i++)
{
if(sccno[i]==scc_cnt)
{
u=i;
ans++;
}
}
memset(vis,0,sizeof(vis));
rdfs(u);
for(int i=1;i<=n;i++)
{
if(!vis[i])
{
ans=0;
break;
}
}
printf("%d\n",ans);
}
return 0;
}
參考資料: