1. 程式人生 > >有向圖的強連通分支及DAG

有向圖的強連通分支及DAG

有向圖的強連通分支:在有向圖G中,如果任意兩個不同的頂點相互可達,則稱該圖是強連通的。有向圖G的極大連通子圖為G的強連通分支;

圖中每個節點只屬於一個強連通分支;

有關DAG(有向無環圖)的一些定理性質:(有時需要特殊考慮只有一個節點的時候)

1、DAG中唯一出度為0的點,一定可以由任意點出發可達。因為無環,所以從任何點出發,必然終止於一個出度為0的點;

2、DAG中,所有入度不為0的點一定可以由某個入度為0的點出發可達。因為無環,所以從入度不為0的點往回走,必然終止於一個入度為0的點;

3、在DAG中加max(n,m)(其中n為入度為0的點數,m為出度為0的點數)條邊,能使DAG變成強連通的;即為每個入度為0的點新增入邊,為每個出度為0的點添加出邊;

有向圖的強連通分支問題常通過求強連通分量,縮點,轉化為DAG問題;

求解有向圖強連通分量的Korasaju演算法:(G的轉置T和G具有相同的強連通分量)

1、深度優先遍歷G,按照dfs完成的先後順序將節點記錄下來;

2、深度優先遍歷G的轉置圖T,按照1中節點結束時間從大到小選擇為遍歷的起點,遍歷T。遍歷過程中給節點分類,每找到一個新的起點,分類就加1;

3、第二步產生的標記值相同的節點構成深度優先深林中的一棵樹,即一個強連通分量;

演算法時間複雜度為(V+E)

注意:Korasaju演算法求強連通分量得到標記點,根據原圖G對其縮點建DAG後,標記編號最大的點對應的一定是DAG中的出度為0的點,編號最小的點一定是DAG中的一個入度為0的點(特殊考慮原圖本身就是雙連通的,即縮點後只有一個點)

poj2186 Korasaju演算法;

#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<climits>
#include<cctype>
#include<iostream>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<string>
#define ll long long
#define MAX 10010
#define INF INT_MAX
#define eps 1e-8

using namespace std;

struct Edge{
	int from,to;
};

vector<Edge>edges;
vector<int> G[MAX],T[MAX],A[MAX];
vector<int>S;

int n;

void init(){
	for (int i=0; i<= n; i++){
		G[i].clear();
		T[i].clear();
		A[i].clear();
	}
	S.clear();
	edges.clear();
}

int vis[MAX],sccno[MAX],scc_cnt;

void dfs1(int u){
	if (vis[u]) return;
	vis[u] = 1;
	for (int i=0; i<G[u].size(); i++)  dfs1(G[u][i]);
	S.push_back(u);  //按照dfs完成的先後順序將節點儲存起來
}

void dfs2(int u){
	if (sccno[u]) return;
	sccno[u] = scc_cnt;
	for (int i=0; i<T[u].size(); i++) dfs2(T[u][i]);
}

void find_scc(){
	scc_cnt = 0;
	memset(sccno,0,sizeof(sccno));
	memset(vis,0,sizeof(vis));
	for (int i=1; i<=n; i++) dfs1(i);   //節點結束時間從大到小選擇為遍歷的起點,遍歷T
	for (int i = n-1; i>=0; i--){
		if (!sccno[S[i]]) {
			scc_cnt++;
			dfs2(S[i]);
		}
	}
}

/*int mark[MAX];

void dfs(int s){
	if (mark[s]) return;
	mark[s] = 1;
	for (int i=0; i<A[s].size(); i++){
		dfs(A[s][i]);
	}
}*/

int out[MAX]; 

int main(){
	int m;
	while (scanf("%d%d",&n,&m) != EOF){
		int u,v;
		init();
		for (int i=0; i<m; i++){
			scanf("%d%d",&u,&v);
			G[u].push_back(v);
			T[v].push_back(u);
			edges.push_back((Edge){u,v});
		}
		find_scc();
		memset(out,0,sizeof(out));
		for (int i=0; i<edges.size(); i++){   //縮點:思考?如何縮點
			int u = edges[i].from;
			int v = edges[i].to;
			if (sccno[u] == sccno[v]) continue;
		//	A[sccno[v]].push_back(sccno[u]); 
			out[sccno[u]] ++;        
		}	
		int k = 1,cnt = 0;
		for (int i=1; i<=scc_cnt; i++) if (!out[i]){   //DAG的性質1;
			k = i;
			cnt++;
		}
	//	printf("k = %d\n",k);
		if (cnt > 1) printf("%d\n",0);
		else {
			int ans = 0;
			for (int i=1; i<=n; i++) if (sccno[i] == k) ans++;
			printf("%d\n",ans);
		}
	/*	memset(mark,0,sizeof(mark));
		dfs(scc_cnt);
		int ok = 1;
		for (int i=1; i<=scc_cnt; i++) if (!mark[i]) ok = 0;
		if (ok){
			int res = 0;
			for (int i=1; i<=n; i++) if(sccno[i] == scc_cnt) res++;
			printf("%d\n",res);
		}
		else printf("0\n");*/
	} 
	return 0;
}


求強連通分量的Tarjan演算法:

做一遍dfs,記錄每個節點在dfs中訪問的開始時間pre[i],顯然,pre值越小,就是越早訪問的;用一個數組low[i]記錄從i節點出發的dfs過程中,i下方的節點能達到的最早的節點開始時間。初始值為low[i] = pre[i];

dfs中,碰到哪個節點,就將那個節點入棧(無向圖的點雙連通分量是將邊入棧:因為無向圖的雙連通分量中,每個邊恰好屬於一個雙連通分量)。棧中的節點只有在其所屬的強連通分量已經全部求出來時,才會出棧。

對於u的子節點v,從v出發進行的dfs回到u時,使得low[u]  = min(low[u],low[v]),因為u可達v,所以v可達的最早節點,也是u可達的;

如果一個節點u,從其出發的dfs完成時,low[u] == pre[u],則說明u可達的所有節點多不能達到比u更早的節點,此時,棧中u上方的節點,都不能達到比u更早的節點。將棧中節點彈出,一直彈到u(包括u)就構成了一個強連通分量;

注意:與Korasaju演算法不同,Tarjan演算法中如果按照彈出順序給強連通分兩編號,則根據原圖縮點建DAG後後,編號最小的節點出度一定為0,編號最大的節點入度一定為0(特殊考慮只有一個點的時候);

poj2186Tarjan演算法:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<climits>
#include<cctype>
#include<iostream>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<string>
#define ll long long
#define MAX 10010
#define eps 1e-8
#define INF INT_MAX

using namespace std;

struct Edge{
	int from, to;
};

vector<int> G[MAX],A[MAX];
vector<Edge>edges;
stack<int>S;

int n;

void init(){
	for (int i=0; i<=n; i++){
		G[i].clear();
		A[i].clear();
	}
	edges.clear();
	while (!S.empty()) S.pop();
}

int pre[MAX],lowlink[MAX],sccno[MAX],dfs_clock,scc_cnt;

void dfs(int u){
	pre[u] = lowlink[u] = ++dfs_clock;
	S.push(u);
	for (int i=0; i<G[u].size(); i++){
		int v =G[u][i];
		if (!pre[v]){
			dfs(v);
			lowlink[u] = min(lowlink[u],lowlink[v]);
		}
		else if(!sccno[v]){
			lowlink[u] = min(lowlink[u],pre[v]);
		}
	}
	if (lowlink[u] == pre[u]){
		scc_cnt++;
		while (1){
			int x = S.top();
			S.pop();
			sccno[x] = scc_cnt;
			if (x == u) break;
		}
	}
}

void  find_scc(){
	dfs_clock = scc_cnt = 0;
	memset(sccno,0,sizeof(sccno));
	memset(pre,0,sizeof(pre));
	for (int i=1; i<=n; i++){
		if (!pre[i]) dfs(i);
	}
}

int mark[MAX];

void judge(int u){
	if (mark[u]) return;
	mark[u] = 1;
	for (int i=0; i<A[u].size(); i++){
		judge(A[u][i]);
	}
}

int main(){
	int m;
	while (scanf("%d%d",&n,&m) != EOF){
		int u,v;
		init();
		for (int i=0; i<m; i++){
			scanf("%d%d",&u,&v);
			edges.push_back((Edge){u,v});
			G[u].push_back(v);
		}
		find_scc();
		int ans = 0;
		for (int i=1; i<=n; i++){
			if (sccno[i] == 1) ans++;      //注意與Kosaraju演算法的不同 
		} 
		//printf("%d ",sccno[i]); printf("\n");
		for (int i = 0; i<edges.size(); i++){   //此處時從入度為0得點沿又向邊逆向搜尋,看是否能到達所有點,也可以用DAG性質1直接判斷;
			int x = edges[i].from;
			int y = edges[i].to;
			if (sccno[x] == sccno[y]) continue;
			A[sccno[y]].push_back(sccno[x]);  //注意這個DAG和原圖對應的DAG是相反方向的
		}
		memset(mark,0,sizeof(mark));
		judge(1);
		int ok = 1;
		for (int i=1; i<=scc_cnt; i++) if (!mark[i]) ok = 0;
		if (ok) printf("%d\n",ans);
		else printf("%d\n",0); 
	}
	return 0;
}
poj1236 Tarjan演算法以及DAG的一些性質;
 #include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<climits>
#include<cctype>
#include<iostream>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<string>
#define ll long long
#define MAX 110
#define eps 1e-8
#define INF INT_MAX

using namespace std;

struct Edge{
	int from, to;
};

vector<int> G[MAX],A[MAX];
vector<Edge>edges;
stack<int>S;

int n;

void init(){
	for (int i=0; i<=n; i++){
		G[i].clear();
		A[i].clear();
	}
	edges.clear();
	while (!S.empty()) S.pop();
}

int pre[MAX],lowlink[MAX],sccno[MAX],dfs_clock,scc_cnt;

void dfs(int u){
	pre[u] = lowlink[u] = ++dfs_clock;
	S.push(u);
	for (int i=0; i<G[u].size(); i++){
		int v =G[u][i];
		if (!pre[v]){
			dfs(v);
			lowlink[u] = min(lowlink[u],lowlink[v]);
		}
		else if(!sccno[v]){
			lowlink[u] = min(lowlink[u],pre[v]);
		}
	}
	if (lowlink[u] == pre[u]){
		scc_cnt++;
		while (1){
			int x = S.top();
			S.pop();
			sccno[x] = scc_cnt;
			if (x == u) break;
		}
	}
}

void  find_scc(){
	dfs_clock = scc_cnt = 0;
	memset(sccno,0,sizeof(sccno));
	memset(pre,0,sizeof(pre));
	for (int i=1; i<=n; i++){
		if (!pre[i]) dfs(i);
	}
}

int in[MAX],out[MAX];

int main(){
	while (scanf("%d",&n) != EOF){
		int t;
		init();
		for (int i=1; i<=n; i++){
			while (scanf("%d",&t) && t != 0){
				G[i].push_back(t);
				edges.push_back((Edge){i,t});
			}
		}
		find_scc();
		if (scc_cnt == 1){    //注意只有一個節點是要特判一下
			printf("%d\n%d\n",1,0);
			continue;
		}
		memset(in,0,sizeof(in));
		memset(out,0,sizeof(out));
		for (int i=0; i<edges.size(); i++){
			int x = edges[i].from;
			int y = edges[i].to;
			if (sccno[x] == sccno[y]) continue;     //注意這個縮點、建圖將DAG只有一個節點的情況看作出度、入度都為0的
			in[sccno[y]] ++;
			out[sccno[x]]++;
		}
		int cnt1 = 0,cnt2 = 0;
		for (int i=1; i<=scc_cnt; i++){
			if(!in[i])  cnt1++;
			if (!out[i]) cnt2++;
		}
		printf("%d\n%d\n",cnt1,max(cnt1,cnt2));
	}
}