kosaraju 和 tarjan演算法詳解(強連通分量)

qq_43842119發表於2019-09-18

定 義
在有向圖G中,如果任意兩個不同的頂點相互可達,則稱該有向圖是強連通的。
有向圖G的極大強連通子圖稱為G的強連通分支。
 轉置圖的定義:將有向圖G中的每一條邊反向形成的圖稱為G的轉置GT 。(注意到原圖和GT 的強連通分支是一樣的)

Korasaju演算法

 1.深度優先遍歷G,算出每個結點u的結束時間f[u],起點如何選擇無所謂。

    每個結點的結束時間和開始時間是dfs序,開始時間是此點第一次被遍歷到時,結束時間為此點已經沒法擴充,從棧中彈出,即已經遍歷結束,不懂dfs序,可以看這個dfs序
 2.深度優先遍歷G的轉置圖GT ,選擇遍歷的起點時,按照結點的結束時間從大到小進行。遍歷的過程中,
     一邊遍歷,一邊給結點做分類標記,每找到一個新的起點,分類標記值就加1。

    如果此節點已經在反圖中遍歷過,就不再從它遍歷,挑選下一個結束時間晚的
 3. 第2步中產生的標記值相同的結點構成深度優先森林中的一棵樹,也即一個強連通分量

至於證明。。。emmm,我沒看懂,就自己想了一下

 如果正圖中 b->a,那麼反圖中為 a->b,而b的結束時間一定比a要晚,所以先遍歷b,如果b->a,則說明在原圖中a->b


 

#include<bits/stdc++.h>
using namespace std;
int f[1000][1000];//儲存正圖 
int rf[1000][1000];//儲存反圖 
int vis[1000];
stack<int> s;//用來儲存節點離開時間 
stack<int> s1[1000];
int n;
//對原圖dfs,找出每個節點的離開時間,用棧儲存,直接pop,就不用再逆序 
void dfs(int a){
	vis[a]=1;
	for(int i=1;i<=n;i++){
		if(f[a][i]&&!vis[i]){
			dfs(i);
		}
	}
	s.push(a);
}
void rdfs(int a,int k){
	vis[a]=1;
	for(int i=1;i<=n;i++){
		if(rf[a][i]&&!vis[i]){
			rdfs(i,k);
		}
	}
	s1[k].push(a);//對每個連通分量分支,記錄下來 
}
int main(){
	int m;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int x,y;
		cin>>x>>y;
		f[x][y]=1;
		rf[y][x]=1;
	}
	//對原圖dfs 
	for(int i=1;i<=n;i++){
		if(!vis[i])
		dfs(1);
	}
	memset(vis,0,sizeof(vis));
	int k=0;
	//對反圖dfs 
	while(!s.empty()){
		if(!vis[s.top()])//節點按照節點離開時間從大到小遍歷 
		rdfs(s.top(),++k);//計算連通分量個數 
		s.pop();
	} 
	cout<<k<<endl;
	//輸出 
	for(int i=1;i<=k;i++){
		while(!s1[i].empty()){
			cout<<s1[i].top();
			s1[i].pop();
		}
		cout<<endl;
	}
	return 0;
}

用鄰接表寫的程式碼(老師寫的)

#include <cstdio>
#include <cstring>
const int V = 1e4 + 7, E = 5e4 + 7;
int hd[V], to[E], fr[E], nt[E], pr[E], tl[E];
int n, m, id[V], vis[V], cnt[V], scc;
bool out[V];
void dfs(int u, int &clk) {//用引用,減少全域性變數的使用
	id[u] = 1;
	for (int i = hd[u]; i; i = nt[i])
		if (!id[to[i]]) dfs(to[i], clk);
	vis[++clk] = u;//當一個節點是葉子節點時,記錄結束時間且只記錄結束時間
}
void rdfs(int v, int clk) {
	id[v] = clk, ++cnt[clk];
	for (int i = tl[v]; i; i = pr[i])
		if (!id[fr[i]]) rdfs(fr[i], clk);
}
void Korasaju() {
	for (int i = 1, clk = 0; i <= n; ++i) if (!id[i]) dfs(i, clk);//最晚結束時間必然是n
	for (int i = 1; i <= n; ++i) id[i] = 0;
	for (int i = n; i; --i)
		if (!id[vis[i]]) rdfs(vis[i], ++scc);
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1, u, v; i <= m; ++i) {
		scanf("%d%d", &u, &v);
		nt[i] = hd[u], pr[i] = tl[v], hd[u] = tl[v] = i;
		to[i] = v, fr[i] = u;//同時儲存正向與反向圖
	}
	int ans = 0;
	Korasaju();
	//自此,產生了一個以強連通分量編號作為結點,構成的一個DAG圖 
	for (int i = 1; i <= n; ++i)
		for (int j = hd[i]; j; j = nt[j])
			if (id[i] != id[to[j]]) out[id[i]] = true;
//如果度用++統計的話,需要考慮兩個連通分量之間存在重複邊(此題並不需要)
	for (int i = 1; i <= scc; ++i)
		if (!out[i]) {
			if (ans) { puts("0"); return 0; }
			ans = cnt[i];
		}
	printf("%d\n", ans);
	return 0;
}

tarjan演算法(很重要)

low陣列和dfn陣列,dfn是第一次搜到此點的時間戳,low值是此點所連線的點中最小dfn

棧用來儲存搜尋到的點

沿著起點一直搜尋,如果搜到返祖邊,則將此點的low值更改,low[u]=min(low[u],dfn[v])   

至於為什麼是dfn[v],不是low[v]?

low[v]並不是最終結果,不該使用,就算low[v]=dfn[v],也不使用他

當此點無法再搜下去,比較dfn與low值,相等則說明棧中從此點一直到棧頂為一個連通分量,將這些從棧中彈出,然後回溯,回溯時比較點的low值,再次更新low值,low[u]=min(low[u],low[v]) 

至於縮點只要將一個分量的染色就好了

受歡迎的牛

有用的定理

有向無環圖中唯一出度為0的點,一定可以由任何點出發均可達(由於無環,所以從任何點出發往前走,必然終止於
一個出度為0的點)

因為強連通所有點都可互相到達,可以把強連通分量縮成一個點,如果有唯一的一個點的出度為0,那麼這個分量的個數即為答案

如果不止一個出度為0,則無解。

#include<iostream>
#include<vector>
#include<algorithm>
#include<stack> 
using namespace std;
int n,m;
const int e=1e4+4;
//標記時間戳 
int low[e],dfn[e],index;
//tarjan中的棧,和判斷是否在棧中 
stack<int> s;
bool ins[e];
//標記所屬分量,用於縮點 
int col[e];
//計算有幾個分量 
int cnt;
//每個分量中的成員 
vector<int> ve[e];
//鄰接表 
struct edge{
	int to,next;
}; 
edge f[5*e];
int head[e],id; 
void add(int from,int to){
	f[++id].next=head[from];
	f[id].to=to;
	head[from]=id;
}
//Tarjan 
void Tarjan(int u){
	dfn[u]=low[u]=++index;
	s.push(u);
	ins[u]=1;
	for(int i=head[u];i;i=f[i].next){
		int v=f[i].to;
		if(!dfn[v])//沒有深搜的點
			Tarjan(v),low[u]=min(low[u],low[v]);//回溯階段,無法繼續探索,則從底往上逐級返回//low值 
		else if(ins[v])
			low[u]=min(low[u],dfn[v]);//在棧中的點
	}
	//當此點全部探索完成,low值依然沒有改變,說明為聯通分量的根節點 ,可以出棧 
	if(dfn[u]==low[u]){
		++cnt;
		int t;
		do{
			t=s.top();
			s.pop();
			ins[u]=0;//標記出棧 
			ve[cnt].push_back(t);
			col[t]=cnt;//縮點(染色標記) 
		}while(t!=u); 
	}
}
int main(){
   cin>>n>>m;
   for(int i=1;i<=m;i++){
   	int a,b;
   	cin>>a>>b;
   	add(a,b);
   }
   for(int i=1;i<=n;i++){
   	if(!dfn[i])
   	  Tarjan(i);
   } 
   int out[e]={0};//統計每個聯通塊的出度 
   for(int i=1;i<=n;i++){
   	  for(int j=head[i];j;j=f[i].next){
   	  	   int to=f[i].to;
   	  	   if(col[i]!=col[to])//不是同一個連通塊,則出度+1 
   	  	     ++out[col[i]];
		} 
   }
   int sum=0,ans;
   for(int i=1;i<=cnt;i++){
   	  if(!out[i])  sum++,ans=ve[i].size();
   } 
   if(sum==1)
     cout<<ans<<endl;
   else
     cout<<0<<endl;
    return 0;
}

對於tarjan中的棧的用處,有個部落格寫的特別詳細

https://blog.csdn.net/qq_38234381/article/details/79981531

用陣列模擬棧的寫法(老師寫的)

#include <cstdio>
#include <algorithm>
const int V = 1e4 + 7, E = 5e4 + 7;
//hd-head, fr-from, nt-next, pr-pre, tl-tail
int hd[V], to[E], nt[E], lbl[V], cnt[V], low[V], dfn[V], stk[V], tp, scc;
bool out[V];

void Tarjan(int u, int &clk){
	low[u] = dfn[u] = ++clk;
	stk[++tp] = u;
	for (int i = hd[u]; i; i = nt[i])
		if (not dfn[to[i]])
			Tarjan(to[i], clk), low[u] = std::min(low[u], low[to[i]]);
		else if (not lbl[to[i]])
			low[u] = std::min(low[u], dfn[to[i]]);
	if (low[u] == dfn[u])
		for (++scc; stk[tp+1] != u; --tp)
			lbl[stk[tp]] = scc, ++cnt[scc];
}

int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1, u, v; i <= m; ++i) {
		scanf("%d%d", &u, &v);
		nt[i] = hd[u], hd[u] = i, to[i] = v;
	}
	for (int i = 1, clk = 0; i <= n; ++i)
		if (not lbl[i]) Tarjan(i, clk);
	for (int i = 1; i <= n; ++i)
		for (int j = hd[i]; j; j = nt[j])
			if (lbl[i] != lbl[to[j]]) out[lbl[i]] = true;
	int ans = 0;
	for (int i = 1; i <= scc; ++i)
		if (not out[i]) {
			if (ans) { puts("0"); return 0; }
			ans = cnt[i];
		}
	printf("%d\n", ans);
	return 0;
}

縮點+建圖

用pair儲存每條邊的兩個端點,重點在於處理自環邊和重複邊,用sort處理,重新建圖

#include <cstdio>
#include <queue>
#include <algorithm>
const int N = 5e5 + 7;
int hd[N], to[N], fr[N], nx[N], stk[N], tp, scc;
int low[N], dfn[N], id[N], sum[N], w[N], ans;
bool bar[N], inq[N], stop[N];
std::pair<int, int> eg[N];
void Tarjan(int u, int &clk) {
	low[u] = dfn[u] = ++clk;
	stk[++tp] = u;
	for (int i = hd[u], v; i; i = nx[i])
		if (!dfn[v=to[i]])
			Tarjan(v, clk), low[u] = std::min(low[u], low[v]);
		else if (!id[v])
			low[u] = std::min(low[u], dfn[v]);
	if (low[u] == dfn[u])
		for (++scc; stk[tp+1] != u; --tp)
			id[stk[tp]] = scc, sum[scc] += w[stk[tp]], stop[scc] |= bar[stk[tp]];
}
int dis[N];
void spfa(int s) {
	std::queue<int> que;
	que.push(s), ans = dis[s] = sum[s];
	while (que.size()) {
		int u = que.front(); que.pop();
		inq[u] = false;
		for (int i = hd[u]; i; i = nx[i]) {
			int v = to[i], tot = dis[u] + sum[v];
			if (dis[v] < tot) {
				dis[v] = tot;
				if (stop[v]) ans = std::max(ans, dis[v]);
				if (not inq[v]) que.push(v);
			}
		}
	}
}
int main() {
	int n, m, s, p;
	scanf("%d%d", &n, &m);
	for (int i = 1, u, v; i <= m; ++i) {
		scanf("%d%d", &u, &v);
		nx[i] = hd[u], hd[u] = i, to[i] = v, fr[i] = u;
	}
	for (int i = 1; i <= n; ++i) scanf("%d", w+i);
	scanf("%d%d", &s, &p);
	for (int i = 1, x; i <= p; ++i)
		scanf("%d", &x), bar[x] = true;
	for (int i = 1, clk = 0; i <= n; ++i) if (!dfn[i]) Tarjan(i, clk);
	//縮點建新圖 
	for (int i = 1; i <= m; ++i)
		eg[i].first = id[fr[i]], eg[i].second = id[to[i]];
	std::sort(eg+1, eg+m+1);
	for (int i = 1; i <= scc; ++i) hd[i] = 0;
	for (int i = 1; i <= m; ++i) {
		int u = eg[i].first, v = eg[i].second;
		int uu = eg[i-1].first, vv = eg[i-1].second;
		if (u!=v && (u!=uu || v!=vv))//清除自環邊和重複邊
			nx[i] = hd[u], hd[u] = i, to[i] = v;
	}
	//在新圖上直接跑spfa
	spfa(id[s]);
	printf("%d\n", ans); 
	return 0;
}

 

相關文章