1. 程式人生 > 實用技巧 >[筆記]tarjan+縮點

[筆記]tarjan+縮點

[筆記]tarjan+縮點

演算法用途:

tarjan可以求強連通分量,縮點則是將一個強連通分量縮成一個點

tarjan

概念

1.有向圖的強連通分量:再有向圖G中,如果兩個頂點V_i,V_j間有一條從V_i到V_j的有向路徑同時還有一條從V_j到V_i的有向路徑,則稱這兩個點強連通,在一個圖的子圖中,任意兩點可以相互到達則稱這組成了一個強連通分量。

2.一個單獨的點也是一個強連通分量。

演算法描述

變數:

1.dfn陣列:dfn[i]表示i這個點再dfs時是第幾個被搜到的

2.low[i]:表示的是i這個節點和它的子孫節點中dfn最小的值

3.stack:表示所有可能構成強連通分量的點,其實就是一個棧

演算法過程

一 畫圖理解

tarjan的第一步是對整個圖進行dfs,搜完後會得到一棵dfs樹,舉個例子.這個樹時有向的,顯然在這個樹上是不會存在環的,因此能產生環的只有可能是一條指向已經訪問(搜尋)過的邊,也就是圖中的紅邊和藍邊。經過觀察發現,紅邊可以產生一個強連通分量,而藍邊不行,因為紅邊是由6號節點指向它的祖先4號節點的,這種紅邊稱為後向邊,而藍邊則指向兩個沒有父子關係的點,這種邊稱為橫叉邊,橫叉邊不一定產生環,而後向邊一定產生環。

知道了以上結論後就開始深搜首先會搜到這樣的圖,此時stack = {1,2,3},而3沒有多餘的指向其它點的邊,因此將3彈出棧,單獨作為一個強連通分量,繼續深搜,會搜到這樣一個圖,此時stack = {1,2,7} 發現節點7指向已經搜尋過的節點3,是上述兩種可能存在環的情況,而此時3不再stack中,因此不存在環,將7,2依次彈出棧中,單獨作為一個強連通分量,再次深搜,會搜到這樣一個圖,此時stack = {1,4,5,6} 發現節點6有一條連向其他節點的邊,即紅邊,指向的是節點4,而這個點已經搜尋過了,符合上面說的產生環的條件,而此時發現節點4已經在stack中說明這是一條後向邊,可以產生環,因此4~6號節點中的所有點組成一個強連通分量。演算法結束

但實際情況可能更復雜,這裡出現了大環套小環的問題,我們需要對dfs過程稍作修改(見下)。

二 演算法完整步驟

1.首先初始化dfn[u] = low[u] = 第幾個被搜尋到,但並不是一開始就先跑一遍深搜,而是邊做邊賦值,具體見程式碼

2.將當前搜尋的節點u存入stack中

3.遍歷每一個與節點u相連的點,如果遍歷到的點的dfn值為0則說明這個點之前沒有被訪問過,那麼就對這個沒有被訪問過的點(假設為v)進行深搜並且更新low陣列的值:low[u] = min(low[u],low[v]),如果與u相連的點(假設為g)已經被搜尋過了,也就是說dfn[g]!=0,此時就更新low[u]的值:low[u] = min(low[u],dfn[g]),這樣就可以保證low[u]儲存的是最先被dfs到的點,也就保證了找的環是最大的。但為什麼是用dfn[g]來更新呢?因為節點g可能是另一個強連通分量裡的節點,只是還沒有出棧,因此節點u可能不能到達low[g],但u一定可以到達dfn[g]。

4.那麼什麼時候說明找到了一個完整的最大的環呢?當我們找到一個點u滿足low[u] == dfn[u]時,說明這個點的子樹中不存在比這個點先搜到的點,則節點u為它所在的強連通分量裡的根節點,所以將stack中u及它之後的點全部彈出,這就是一個強連通分量。

縮點

演算法描述

縮點其實很好理解,也很好實現,只需要新建一個col陣列,用來將同一個連通塊內的點染成一個顏色。具體實現看程式碼。縮完點後的圖是一個有向無環圖,各個縮完的點由跨越不同強連通分量的邊來連線。

例題應用

原題鏈

題目分析

首先建圖,如果A認為B受歡迎,則連一條從B到A的有向邊,這樣更方便求強連通分量。然後用tarjan求出所有的強連通分量,再縮點,找到一個出度為0的縮完後的點,這就是答案,因為縮完點後的圖是一個有向無環圖,如果一個縮點有出度,則它一定不能被它連出去的那個縮點的牛所喜歡;而一個縮完點後的圖中,也不能出現兩個沒有出度的縮點,因為這樣它們就不能被對方縮點裡的牛所喜歡,因此我們的答案是隻有一個縮點出度為0的圖中那個縮點的大小

程式碼(tarjan+縮點 & AC code)

#include <bits/stdc++.h>
using namespace std;
struct node{
	int to,next;	
}edge[100010];
int fir[100010],dfn[100010],low[100010],col[100010],num,tot,coln,size[100010],degree[100010];
stack < int > s;
void add(int x,int y){
	tot++;
	edge[tot].to = y;
	edge[tot].next = fir[x];
	fir[x] = tot;
}
void tarjan(int k){
	low[k] = dfn[k] = ++num;
	s.push(k);
	for(int i = fir[k];i;i = edge[i].next){
		int x = edge[i].to;
		if(!dfn[x]){
			tarjan(x);
			low[k] = min(low[k],low[x]);
		}
		else if(!col[x]){//發現後向邊 
			low[k] = min(low[k],dfn[x]);
		}
	}
	if(low[k] == dfn[k]){//找到了一個強連通分量的根節點 
		col[k] = ++coln;//縮到一個點裡 
		++size[coln];//更新縮點的大小 
		while(s.top() != k){
			col[s.top()] = coln;//縮點 
			++size[coln];//更新縮點的大小 
			s.pop();
		}
		s.pop();//千萬不要忘記這一步,要將k節點彈出 
	}
	return;
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(y,x);
	}
	for(int i = 1;i <= n;i++){
		if(!dfn[i])
			tarjan(i); 
	} 
	for(int i = 1;i <= n;i++){
		for(int j = fir[i];j;j = edge[j].next){
			int x = edge[j].to;
			if(col[x] != col[i]){//顏色不同說明不在一個塊中,所以出度增加 
				++degree[col[x]];//因為建的是反向邊,所以其實是從x連到i 
			}	
		}
	} 
	int flag = 0;//記錄有多少出度為0的縮點,如果大於1個則答案為0
	int ans = 0; 
	for(int i = 1;i <= coln;i++){
		if(degree[i] == 0){
			++flag;
			ans = size[i];
		}
	} 
	if(flag != 1){
		printf("0\n");
	}
	else printf("%d\n",ans);
	return 0;
}

完結!