Tarjan求強連通分量

休伯利安礦工發表於2019-04-22

[演算法定義]

在有向圖中,如果兩個頂點至少存在一條路徑(可以相互通達),則稱兩個頂點強連通(strongly connected)。

如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖

非強連通有向圖的極大強連通子圖,稱為強連通分量(strongly connected components)。

在上圖中,{1 , 2 , 3 , 4 } , { 5 } ,  { 6 } 三個區域可以相互連通,稱為這個圖的強連通分量。

Tarjan演算法是基於對圖深度優先搜尋的演算法,每個強連通分量為搜尋樹中的一棵子樹。

DFN[ i ] : 在DFS中該節點i被搜尋的次序(時間戳)。

LOW[ i ] : 為i或i的子樹能夠追溯到的最早的棧中節點的次序號。

當DFN[ i ]==LOW[ i ]時,已i為根的搜尋子樹上所有節點是一個強連通分量。

【心得】:如果有環,dfn是傳遞自己的下一代,low是繼承自己的上一代或自己(上一代無環)

搜尋時,把當前搜尋樹中未處理的節點加入一個堆疊。

回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。


[演算法圖解]

從1開始dfs搜尋,把遍歷到的節點加入棧中。

搜尋到i=6時,節點都入棧了,此時就進行回溯。

DFN[6]=LOW[6],以節點6為根的搜尋子樹是一個強連通分量(節點6沒有子樹)。

6出棧。


依次類推,DFN[5]=LOW[5],5為強連通分量

並且5的邊也都找完了,5出棧。

接下來回溯到3,4入棧。


然後從4找到1,發現節點1已存在。

將1看做根節點往回搜尋子節點,子節點LOW[i]=low[根]=1。

子節點low繼承的是根的dfn,根的low就是根的dfn,最小的那個

現在只是將1,3,4看做環,1的邊還沒有找完。

沒有找完自然不會進行根節點1成環的回溯出棧操作。


繼續找1的邊,找到2。

再訪問4(還在棧中),所以LOW[2]=DFN[4]=5。

那麼4的根是2,為什麼不繼承1的dfn,是為了讓縮點與割點程式碼一致

結果不影響,連通分量還是一起出棧(並染色)

從2返回1後,發現DFN[1]=LOW[1],把棧中節點全部取出,組成一個強連通分量{1,2,3,4}。

【例】如果2跟1不成環,那麼2不會連4,(2將自己丟擲)或(2與2的子節點成環整個丟擲)

           最後執行到1,再進行1的回溯,組成強連通分量{1,3,4}出棧


自此演算法結束,找到{1,2,3,4},{5},{6}三個強連通分量


[程式碼1]不要慌全解系列

#include<iostream>
#include<algorithm>
#include<cstring>
#include<stack> 
using namespace std;

struct ss{
	int v;
	int next;
	/*  
	v指節點v 
	next永遠指u點->上一個點的邊序(1,2,3···)
	邊序 即u到其他點(上一個點)是第幾條邊(num) 
	上一條邊沒有就是-1 
	*/ 
}s[1000]; 

int head[1000];//邊序 
int dfn[1000];
int low[1000];
int vis[1000];//相當於棧 
int color[1000];//染色 
int n,m;
int cnt;
int num;
stack<int >st;

void init()
{
	memset(head,-1,sizeof(head));
	memset(vis,0,sizeof(vis));
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));
	//memset(color,0,sizeof(color));	
	num=0;
	cnt=0;
}
void add(int u,int v)
{
	s[num].v = v;
	s[num].next = head[u];
	head[u] = num++;
	/*
	將v存進去
	將u->上一個點的邊序掛上next
	num一直在++(總邊數不會錯)
	head[u]更新當前u的邊序 			

	如果雙向再存u掛v邊序 
	eg[num].v = u;
	eg[num].next = head[v];
	head[v] = num++;
	*/ 	
}
void Tarjan(int u)
{
	st.push(u);
	dfn[u] = low[u] = ++cnt;//搜尋次序號 
	vis[u]=1;//節點x在棧中
	for(int i = head[u]; i != -1; i = s[i].next)
	{
		//通過對u點上一個邊序的掛鉤
		//構造對連線u點的所有邊數遍歷查詢對應v點 
		int v = s[i].v;
		if(!dfn[v])//不曾訪問過 
		{
			Tarjan(v);//找v點 
			low[u] = min(low[u],low[v]);
			/* 
			根節點的dfn值是區分不同環的值
                        low是為了讓這個環都等dfn[根]
                        low[根]可能不等dfn[根]
                        根的low繼承根的根的dfn 
			1.如果v是根節點
			  不論只有v一個點還是有一個環 
			  low[v]確實永遠比low[u]大(u比v先入)
			  v的環low值=dfn[v]都比low[u]的大 
			  v不對u產生影響
			2. 
			  如果v點與u點成環
			  那麼順著v點或v連著的點找下去
			  總有一個能連到根節點
			  low值回溯的時候繼承根的dfn值
                          根的dfn是這個環裡面最小的
			  low[v]等於dfn[根]
			  v對u產生影響->low[u]=low[v] 
			*/ 
		}
		else if(vis[v])//訪問過但還在棧中 
			/*
			因為根u點還沒有將邊都找完
			出棧的點都是根節點邊已經找完的點或者環 
			已經沒有與剩下的點有任何關係才能出 
			*/ 
			low[u] = min(low[u],dfn[v]);
			/*
			這相當於根節點有兩個分叉口a,b 
			並且a找到已經在棧中的b
				  
			那麼這一步其實也可以寫成 
			low[u] = min(low[u],low[v]);
                        反正連到一個環了
		
                        目的是為了讓縮點與割點的程式碼一致 
                        區分相連的環的根有不同的dfn
                        無向圖找割點用的

                        但是縮點是將一起出棧的點縮成一個點(染成一個色)
	                對於縮點結果都無影響	
			*/
	}	

	if(dfn[u]==low[u])//找一遍再是強連通分量 
	{
		int now;
		do{	//取出包括u的環 
			now=st.top();
			color[now]=u; //染色 
			vis[now]=0;
			st.pop();
		}while(now!=u);
	}
	return;	
} 
void out()
{
	for(int i=1;i<=n;i++)
		printf("%d ",i);
	printf("\n");
	for(int i=1;i<=n;i++)
		printf("%d ",color[i]);
	printf("\n");
}

int main()
{
	while(~scanf("%d%d",&n,&m) && (m+n))
	{
		init();
		int u,v;
		while(m--)
		{
			scanf("%d%d",&u,&v);	
			add(u,v);
		}	
		//為了防止一個圖裡有不相連的兩個或多個樹 
		for(int i=1;i<=n;i++) 
			if(!dfn[i]) 
				Tarjan(i);
		out();
	} 
	return 0; 
} 

[程式碼2]

若只是縮點,求強連通分量染色,不與割點程式碼一致,就不需要用棧

#include<iostream>
#include<algorithm>
#include<cstring>
#include<stack> 
#include<vector>
using namespace std;

int dfn[1000];
int low[1000];//就相當於顏色,一個環一個low=dfn[根] 
int vis[1000];
int n,m;
int cnt;
stack<int >st;
vector<int >vc[1000]; 

void init()
{
	memset(vis,0,sizeof(vis));
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));	
	cnt=0;
}

void Tarjan(int u)
{
	st.push(u);
	dfn[u] = low[u] = ++cnt;
	vis[u]=1;
	for(int i = 0; i < vc[u].size(); i++)
	{
		int v = vc[u][i];
		if(!dfn[v])
		{
			Tarjan(v);
			low[u] = min(low[u],low[v]);
		}
		else 
			low[u] = min(low[u],low[v]);	
	} 
	return;	
} 
void out()
{
	for(int i=1;i<=n;i++)
		printf("%d ",i);
	printf("\n");
	for(int i=1;i<=n;i++)
		printf("%d ",low[i]);
	printf("\n");
}

int main()
{
	while(~scanf("%d%d",&n,&m) && (m+n))
	{
		init();
		int u,v;
		while(m--)
		{
			scanf("%d%d",&u,&v);	
			vc[u].push_back(v);
		}	
		for(int i=1;i<=n;i++) 
			if(!dfn[i]) 
				Tarjan(i);
		out();
	} 
	return 0; 
} 

 

 

 

 

 

 

 

 

 

相關文章