201509-4高速公路(tarjan算法求強聯通分量)

強連通分量(取自百度百科):

    有向圖強連通分量:在有向圖G中,如果兩個頂點vi,vj間(vi>vj)有一條從vi到vj的有向路徑,同時還有一條從vj到vi的有向路徑,則稱兩個頂點強連通(strongly connected)。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱爲強連通分量(strongly connected components)。

問題描述:

試題編號: 201509-4
試題名稱: 高速公路
時間限制: 1.0s
內存限制: 256.0MB
問題描述:

問題描述

  某國有n個城市,爲了使得城市間的交通更便利,該國國王打算在城市之間修一些高速公路,由於經費限制,國王打算第一階段先在部分城市之間修一些單向的高速公路。
  現在,大臣們幫國王擬了一個修高速公路的計劃。看了計劃後,國王發現,有些城市之間可以通過高速公路直接(不經過其他城市)或間接(經過一個或多個其他城市)到達,而有的卻不能。如果城市A可以通過高速公路到達城市B,而且城市B也可以通過高速公路到達城市A,則這兩個城市被稱爲便利城市對。
  國王想知道,在大臣們給他的計劃中,有多少個便利城市對。

輸入格式

  輸入的第一行包含兩個整數n, m,分別表示城市和單向高速公路的數量。
  接下來m行,每行兩個整數a, b,表示城市a有一條單向的高速公路連向城市b

輸出格式

  輸出一行,包含一個整數,表示便利城市對的數量。

樣例輸入

5 5
1 2
2 3
3 4
4 2
3 5

樣例輸出

3

樣例說明


  城市間的連接如圖所示。有3個便利城市對,它們分別是(2, 3), (2, 4), (3, 4),請注意(2, 3)和(3, 2)看成同一個便利城市對。

評測用例規模與約定

  前30%的評測用例滿足1 ≤ n ≤ 100, 1 ≤ m ≤ 1000;
  前60%的評測用例滿足1 ≤ n ≤ 1000, 1 ≤ m ≤ 10000;
  所有評測用例滿足1 ≤ n ≤ 10000, 1 ≤ m ≤ 100000。

解題思路:

題目中給出了一個“便利城市對”的概念,簡單地說就是A能經過某條路徑到達B,B也能經過某條路徑到達A,那麼二者就構成了一個“便利城市對”,由於強連通分量中,每一對節點都是一個“便利城市對”,而一個強連通分量之外的任何一個節點都不能與該強連通圖中的任何一個節點構成“便利城市對”。因此,本題可以轉化爲求出所有的強連通分量,若一個強連通分量的節點數爲n,則其中的“便利城市對”個數爲:n*(n-1)/2,將所有強連通分量的“便利城市對”數目求和就是總的結果。

算法:tarjan

算法思想:

首先需要兩個數組:dfn[maxn](用於記錄當前節點的搜索編號,即時間戳),low[maxn](用於記錄當前節點或其子樹能能夠追溯到的最早的棧中節點的搜索編號),當dfn[u]==low[u]時,得到一個以u爲根強連通分量。簡單的說,就是當前搜索到的節點是之前已經被搜索過一次的,並且從上一次搜索到現在,還沒有遇到當前這種搜到一次又走回來的情況,那麼這個從第一次被搜索到現在走過的路徑上的每一對節點都可以相互訪問(可以想象成一個環),那麼我們就得到了一個強連通分量。

舉個百度百科上的栗子:

(1)

從1號節點開始深度搜索(DFS),首先dfn[1]=1(第一個時間戳),1號節點入棧,low[1]=1(該節點就是1或以1爲根的子樹在棧中最早的節點);接着一直深度遍歷到節點6,low[6]=dfn[6]=4,此時已經無路可走,只能回溯,發現又回到了6,並且節點6就是6或者以6爲根的子樹在棧中的最早的節點,也就是說,以6爲根,只能得到{6}這個強連通分量,6出棧;

(2)

接着對5號節點進行DFS,發現只能到達已經遍歷過而且還不在棧中的節點6,而已經被遍歷且不在棧中就以爲着該節點已經屬於某一個得到了的強連通分量,因此不再考慮這種節點。這樣的話,節點5也是無路可走了,只能回溯,此時low[5]=dfn[5],與(1)同理,我們又得到了一個新的強連通分量{5},節點5出棧,回溯到節點3;

(3)

接着對節點3進行DFS,到達節點4,low[4]=dfn[4]=5,4入棧,接着搜索到節點6,在(2)中我們說過,6已經出局了,故此路不通,因此4搜索到節點1,此時發現,節點1雖然被遍歷過了,但是它還在棧中,這時節點1是節點4的一顆子樹,那麼我們就要修改一下low[4]了,比較dfn[1]和當前的low[4]發現dfn[1]更小(說明存在比自己更早被搜索到,但尚未出棧的,也就是說,這個更早的節點一定可以與自己共存在一個強連通分量裏),因此low[4]被更新爲1,此時low[4]!=dfn[4],故還未得到新的強連通分量,暫時不出棧,回溯到節點3,由於4是3的子樹,4在走了一圈後,他的low有所改變(相當於4拓展了新的子樹),那麼4的子樹也必然是3的子樹,因此需要把low[3]和low[4]比較,low[3]被更新爲更小的1,此時3也已經無路可走,並且low[3]!=dfn[3],同樣是尚未得到新的強連通分量,也不出棧,繼續回溯到節點1;

(4)

節點1繼續DFS,走到尚未被訪問的節點2,low[2]=dfn[2]=6節點2繼續DFS,走到已經被訪問的節點4,並且發現節點4在棧中,由於4是2的子樹,low[2]與dfn[4]相比,dfn[4]更小,因此low[2]被更新爲5,節點4無路可走,回溯到2,節點2同樣無路可走,並且low[2]!=dfn[2]因此尚未得到新的強連通分量,2不出棧,繼續回溯到1,此時1也已經無路可走,檢查發現low[1]==dfn[1],說明自己回到了自己,又一個新的強連通分量誕生了!,開始退棧到節點1。

由於全部節點已經搜索完畢,因此得到了全部的強連通分量:{1,3,4,2},{5},{6};

注意:因爲上圖中,所有的強連通分量都是互相連接的,因此可以搜索到全部節點,如果不是這樣,比如這道CCF題,那就需要人爲遍歷所有爲被訪問過的節點!

總結一下tarjan的思想:從一個沒被訪問過的節點出發,進行DFS,如果不能走了,就檢查當前節點是否是自己或是以自己爲根的子樹中進入棧最早,並且還在棧中的,如果是,那就是自己回到了自己,得到了一個新的強連通分量,退棧到自己的節點號。然後繼續回溯,如果回溯到的節點也無路可走,那就重複剛纔的動作,如果有路可走,那就一定要堅持啊!!!因爲這是爲了找出和自己在同一個強連通分量的所有節點,一個都不能放棄!!!(以上過程需要對所有尚未被訪問的節點實施!)

算法:

#include <iostream>
#include <vector>
#include<stack> 
using namespace std;
stack<int> S;
vector<int> edge[10005];
int dfn[10005],low[10005],sum=0,index=0;
bool in_stack[10005];
void tarjan(int u)
{
	index++;
	dfn[u]=index;
	low[u]=index;
	S.push(u);//把此節點壓棧 
	in_stack[u]=true;
	for(int i=0;i<edge[u].size();i++)
	{
		int v=edge[u][i];//當前節點的鄰接節點
		if(dfn[v]==0)//如果當前節點從未被訪問過
		{
			tarjan(v);//深度搜索該鄰接節點 
			low[u]=min(low[v],low[u]);//更新low[u]爲當前節點以及當前節點子樹能追溯到的棧中的最早的節點 
		} 
		else//如果該節點被訪問過
		{
			if(in_stack[v])//如果該節點在棧中,說明遇到了之前路徑上的節點 
			{
				low[u]=min(low[u],dfn[v]);//更新low[u]爲當前節點以及當前節點子樹能追溯到的在棧中的最早的節點
			} 
			//如果該節點不在棧中,那就說明遍歷到了之前已經作爲強連通分量中的節點 
		} 
	}
	//前方無路可走 
	if(low[u]==dfn[u])//找到了最近一次自己返回到自己的情況,得到了一個強連通分量,開始出棧 
	{
		int count=0;
		while(true)
		{
			count++;
			if(S.top()==u) 
			{
				in_stack[u]=false;
				S.pop();
				break;
			}
			else 
			{
				in_stack[S.top()]=false;
				S.pop();
			}
		}
		sum+=count*(count-1)/2;
	} 
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=0;i<m;i++)
	{
		int a,b;
		scanf("%d%d",&a,&b);
		edge[a].push_back(b);
	}
	for(int i=1;i<=n;i++)
	{
		dfn[i]=0;
		low[i]=0;
		in_stack[i]=false;
	}
	//in_stack[1]=true;
	for(int i=1;i<=n;i++)
	{
		if(dfn[i]==0) //只要當前節點沒被訪問過,那就從該節點開始,求強聯通分量 
			tarjan(i);
	}
	printf("%d\n",sum);
	return 0;
}

運行結果:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章