Tarjan 算法的應用

近期一直在刷這方面的題 由於無法學新知識 但又想寫點什麼 就水篇博文吧算法

關於 Tarjan算法

發明者 Robert E.Tarjan 羅伯特·塔揚,美國計算機科學家spa

塔老爺子發明過不少算法,並且大可能是以他的名字命名的,因此 Tarjan算法 也分不少種code

這裏主要講 縮點,割點,割邊 和 2-SATget

\[\]

引理一

什麼是強連通份量?it

強連通份量的定義是:極大的強連通子圖。又叫 SCCclass

簡單來講,在一個有向圖中,若全部點之間兩兩互相直接可達,則將這個圖成爲強連通份量計算機科學

求一個圖中的強連通份量可使用 Tarjan,Kosaraju 或者 Garbow 算法變量

引理二

什麼是雙聯通份量?原理

雙聯通分爲 點雙聯通 與 邊雙連通 兩種搜索

在一張連通的無向圖中,對於兩個點 \(u\)\(v\),若是不管刪去哪一條邊都不能使它們不連通,咱們就說 \(u\)\(v\) 邊雙連通

在一張連通的無向圖中,對於兩個點 \(u\)\(v\) ,若是不管刪去哪個除本身以外的點都不能使它們不連通,咱們就說 \(u\)\(v\) 點雙連通

這裏有兩個結論:

  • 邊雙連通具備傳遞性,即若 \(x\)\(y\) 邊雙連通, \(y\)\(z\) 邊雙連通,則 \(x\)\(z\) 邊雙連通

  • 點雙連通不具備傳遞性

手玩幾組樣例便可證實,比較顯然

\[\]

有向圖縮點

縮點,簡單說就是把一個圖中全部的強連通份量縮成一個點,使之造成一個 DAG

縮完點後的圖中每一個點會有一個新的編號,同處一個強連通份量中的點編號相同

想要完成這一操做,首先須要知道什麼是 DFS序

一個結點 \(x\) 的 DFS序 是指深度優先搜索遍歷時改結點被搜索的次序,簡記爲 \(dfn[x]\)

而後,再維護另外一個變量 \(low[x]\)

\(low[x]\) 表示如下節點的 DFS序 的最小值:以 \(x\) 爲根的子樹中的結點 和 從該子樹經過一條不在搜索樹上的邊能到達的結點

根據 DFS 的遍歷原理能夠發現

  • 一個結點的子樹內結點的 DFS序 都大於該結點的 DFS序

  • 從根開始的一條路徑上的 DFS序 嚴格遞增,low值 嚴格非降

知道了這些,再來看 Tarjan算法 求強連通份量的具體內容

咱們通常只對尚未肯定其 DFS序 的節點進行 Tarjan 操做,操做主要包括兩個部分

第一部分

以 DFS 的形式,處理出當前點 \(x\)\(dfn[x]\)\(low[x]\)

對當前點打一個標記表示已經遍歷過,在以後的 DFS 中根據是否遍歷過來進行不一樣處理,具體方式以下:

設當前枚舉點爲 \(fr\)\(fr\) 連出去的點記爲 \(to\)

  1. \(to\) 未被訪問:繼續對 \(to\) 進行深度搜索。在回溯過程當中,用 \(low[to]\) 更新 \(low[fr]\)。由於存在從 \(fr\)\(to\) 的直接路徑,因此 \(to\) 可以回溯到的已經在棧中的結點, \(fr\) 也必定可以回溯到。
  2. \(to\) 被訪問過,已經在棧中:即已經被訪問過,根據 low值 的定義(可以回溯到的最先的已經在棧中的結點),則用 \(dfn[to]\) 更新 \(low[fr]\)
  3. \(to\) 被訪問過,已不在在棧中:說明 \(to\) 已搜索完畢,其所在連通份量已被處理,因此不用對其作操做。

這一部分代碼實現以下:

low[fr]=dfn[fr]=++cnt;vis[fr]=1;
for(int i=head[fr];i;i=e[i].nxt){
    int to=e[i].to;
    if(!dfn[to]) tarjan(to),low[fr]=min(low[fr],low[to]);
    else if(vis[to]) low[fr]=min(low[fr],dfn[to]);
}

第二部分

對於一個連通份量圖,咱們很容易想到,在該連通圖中有且僅有一個 \(dfn[x]=low[x]\)

該結點必定是在深度遍歷的過程當中,該連通份量中第一個被訪問過的結點,由於它的 DFS序 和 low值 最小,不會被該連通份量中的其餘結點所影響

咱們能夠維護一個棧,存儲全部枚舉到的點

所以,在回溯的過程當中,斷定 \(dfn[x]=low[x]\) 的條件是否成立,若是成立,則從棧中取出一個點,處理它所在的強連通份量的編號以及大小,也能夠處理其餘的一些操做,這樣直到把全部點處理完爲止

這一部分的代碼實現以下:

zhan[++top]=u;
if(dfn[u]==low[u]){
	++siz[++t];
        int pre=zhan[top--];
        vis[pre]=0;num[pre]=t;
	while(pre!=u){
	    ++siz[t];pre=zhan[top--]; 
	    vis[pre]=0;num[pre]=t;
	}
}

至此,即可以處理出一個點所在的強連通份量,時間複雜度爲 \(O(n+m)\)

\[\]

無向圖縮點

能夠處理 割點與橋 以及 雙聯通份量 相關的一些題

由於是無向圖,必須加兩條邊,而加兩條邊後跑 Tarjan 會很麻煩

這裏有另外一個處理方法:經過 異或 來一次選中兩條邊

咱們知道 \(0\oplus1=1\)\(1\oplus1=0\) ; \(2\oplus1=3\) , \(3\oplus1=2\) ; \(4\oplus1=3\) , \(3\oplus1=4\) ;

而建邊的時候兩條邊的編號相差 \(1\),因此能夠每次處理第 \(i\) 條邊的時候處理第 \(i\oplus 1\) 條邊,解決這個問題

而有向圖和無向圖 Tarjan 的寫法也差很少,low值 的更新方式和縮點的編號等都相同,只有標記的地方不同

代碼實現以下:

void tarjan(int u){
	low[u]=dfn[u]=++cnt;zhan[++top]=u;
	for(int i=head[u];i;i=e[i].nxt){
	    if(!vis[i]){
	    	vis[i]=vis[i^1]=1;
		    int to=e[i].to;
		    if(!dfn[to]) tarjan(to),low[u]=min(low[u],low[to]);
		    else low[u]=min(low[u],dfn[to]); 	
		}
	}
	if(dfn[u]==low[u]){
		++siz[++t];int pre=zhan[top--];num[pre]=t;
		while(pre!=u){++siz[t];pre=zhan[top--];num[pre]=t;}
	}
}

\[\]

2-SAT

SAT 是適定性(Satisfiability)問題的簡稱。通常形式爲 k-適定性問題,簡稱 k-SAT。而當 \(k>2\) 時該問題爲 NP 徹底的。因此咱們只研究 \(k=2\) 的狀況。 —— OI Wiki

我的感受,就是一個實際應用類的知識吧

就是指定 \(n\) 個集合,每一個集合包含兩個元素,給出若干個限制條件,每一個條件規定不一樣集合中的某兩個元素不能同時出現,最後問在這些條件下可否選出 \(n\) 個不在同一集合中的元素

這個問題通常用 Tarjan算法 來求解,也可使用爆搜,能夠參考OI Wiki上的說明,這裏就只講用 Tarjan 實現

但這種問題的實現主要不是難在 Tarjan 怎麼寫,而是難在圖怎麼建

咱們能夠考慮怎麼經過圖來構造其中的關係

既然給出了條件 \(a\)\(b\),必須只知足其中之一,那麼存在兩種狀況,一是選擇 \(a\)\(\lnot b\),二是選擇 \(b\)\(\lnot a\)

那咱們就能夠將 \(a\) 連向 \(\lnot b\)\(b\) 連向 \(\lnot a\),表示選了 \(a\) 必須選 \(\lnot b\),選了 \(b\) 必須選 \(\lnot a\)

舉個例子,假設這裏有兩個集合 \(A=\{x_1,y_1\}\)\(B=\{x_2,y_2\}\),規定 \(x_1\)\(y_2\) 不可同時出現,那咱們就建兩條有向邊 \((x_1,y_1)\)\((y_2,x_2)\),表示選了 \(x_1\) 必須選 \(y_1\),,選了 \(y_2\) 必須選 \(x_2\)

這樣建完邊以後只須要跑一邊 Tarjan 縮點判斷有無解,如有解就把幾個不矛盾的強連通份量拼起來就行了

這裏注意,由於跑 Tarjan 用了棧,根據拓撲序的定義和棧的原理,能夠獲得 跑出來的強連通份量編號是反拓撲序 這一結論

咱們就能夠利用這一結論,在輸出方案時倒序獲得拓撲序,而後肯定變量取值便可

具體形如這樣:

\\mem[i] 表示非 i
for(int i=1;i<=n;i++)if(num[i]==num[mem[i]]){printf("無解");return 0;}\\若兩條件必須同時選,則不成立
for(int i=1;i<=n*2;i++)if(num[i]<num[mem[i]]) printf("%d\n",i);return 0;\\輸出其中一種選擇方案

時間複雜度爲 \(O(n+m)\)

\[\]

割點

什麼是割點?

若是在一個無向圖中,刪去某個點可使這個圖的極大連通份量數增長,那麼這個點被稱爲這個圖的割點,也叫割頂。

求割點比較暴力的作法是,對於每一個點嘗試刪除而後判斷圖的連通性,不過顯然複雜度極高

考慮用 Tarjan 作

同縮點同樣,用 Tarjan 求割點也須要處理出點的 DFS序 和 low值,而且是用 DFS 的形式處理

每次枚舉一個點,判斷這個點是否爲割點的依據是:

1.若是它有至少一個兒子的 low值 大於它自己的 DFS序,那麼它就是割點
1.若是它自己被搜到且有很多於兩個兒子,那麼它就是割點

對於第一個依據的說明是:若一個兒子的 low值 大於它自己的 DFS序,說明刪去它以後它的這個兒子沒法回到祖先點,那麼它確定是割點
對於第二個依據的說明是:若它的兒子小於兩個,那麼刪去他不會形成任何影響,因此它不會是割點

更新 low值 的方式與縮點類似,可是約束條件不一樣,放僞代碼感性理解一下:

若是 v 是 u 的兒子 low[u] = min(low[u], low[v]);
不然 low[u] = min(low[u], num[v]);

其實割點 Tarjan 的所有代碼實現有不少別的細節,原理很簡單,代碼實現以下:

void tarjan(int u,int fa){
    vis[u]=1;int chi=0;//統計孩子數量
    dfn[u]=low[u]=++cnt;
    for(int i=head[u];i;i=e[i].nxt){
	int to=e[i].to;
	if(!vis[to]){
	    chi++;tarjan(to,u);
	    low[u]=min(low[to],low[u]);
	    if(fa!=u&&low[to]>=dfn[u]&&!flag[u]){//第一個依據
		flag[u]=1;
		res++;\\割點數量
	    }
        }
        else if(to!=fa)
	    low[u]=min(low[u],dfn[to]);
    }
    if(fa==u&&chi>=2&&!flag[u]){//第二個依據
	flag[u]=1;
	res++;
    }
}

可是這樣跑 Tarjan 針對的不是沒有肯定 DFS序 的點,而是沒有訪問過的點,而且每次初始父親都是本身
也就是這樣:

for(int i=1;i<=n;i++) if(!vis[i]) cnt=0,tarjan(i,i);

這樣跑一邊Tarjan後,帶有 \(flag\) 標記的點就是割點

\[\]

割邊

按割點的理解方式,割邊應該是刪去後能使無向圖極大連通份量數量增長的邊

沒錯,就是這樣

割邊,也叫橋。嚴謹來講,假設有連通圖 \(G=\{V,E\}\)\(e\) 是其中一條邊(即 \(e\in E\)),若是 \(G-e\) 是不連通的,則邊 \(e\) 是圖 \(G\) 的一條割邊(橋)。

原理和割點差很少,實現也差很少,只要改一處:\(low[v]>dfn[u]\) 就能夠了,並且不須要考慮根節點的問題。

與判斷割點的第一條依據相似,當一條邊 \((u,v)\)\(low[v]>dfn[u]\) 時,刪去這條邊,\(v\) 就沒法回到祖先節點,所以知足此條件的邊就是圖的割邊

代碼實現以下:

void tarjan(int u,int fat){
    fa[u]=fat;
    low[u]=dfn[u]=++cnt;
    for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].to;
        if(!dfn[v]){
          tarjan(v,u);
	  low[u]=min(low[u],low[v]);
          if(low[v]>dfn[u]){vis[v]=true;++bri;}\\bri 是割邊的數量
        } 
	else if(dfn[v]<dfn[u]&&v!=fat) 
	    low[u]=min(low[u],dfn[v]);
    }
}

其中,當 \(vis[x]=1\) 時,\((fa[x],x)\) 是一條割邊

\[\]

例題

「一本通 3.6 例 1」分離的路徑
「一本通 3.6 例 2」礦場搭建
[APIO2009]搶掠計劃
[USACO5.3]校園網Network of Schools
[ZJOI2007]最大半連通子圖
[POI2001]和平委員會

寫在後面

寫了半上午加半下午,好累qaq
不過有一說一,我是真不理解有人只放個題目連接而後放個代碼是怎麼想的,連思路都不提,是給別人寫 std 仍是隻爲了告訴別人本身作對了
比我還水

相關文章
相關標籤/搜索