近期一直在刷這方面的題 由於無法學新知識 但又想寫點什麼 就水篇博文吧算法
發明者 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\)
這一部分代碼實現以下:
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;} } }
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 仍是隻爲了告訴別人本身作對了
比我還水