1. 程式人生 > >求解有向圖的強連通分量的SCC問題---POJ 2186 Popular Cows

求解有向圖的強連通分量的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
       onto S.
  • 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,
      breadth-first search
       (BFS) can be used instead of depth-first search.

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.

實際上和使用基於DFS的拓撲排序中新增頂點到最終結果棧中的過程幾乎一致,只不過,只不過這裡的圖不一定是DAG,而拓撲排序中的圖一定是DAG。

對非有向無環圖來說,是不能進行拓撲排序的,所以實際上的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;
}

參考資料: