tarjan求強連通份量+縮點+割點/割橋(點雙/邊雙)以及一些證實

「tarjan陪伴強聯通份量ios

生成樹完成後思路才閃光數組

歐拉跑過的七橋古塘學習

讓你 心馳神往」----《膜你抄》spa

 

自從聽完這首歌,我就對tarjan開始心馳神往了,不過因爲以前水平不足,一直沒有時間學習。這兩天好不容易學會了,寫篇博客,也算記錄一下。3d

 

1、tarjan求強連通份量code

一、什麼是強連通份量?component

引用來自度孃的一句話:blog

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

一臉懵逼......不過倒也不難理解。string

反正就是在圖中找到一個最大的圖,使這個圖中每一個兩點都可以互相到達。這個最大的圖稱爲強連通份量,同時一個點也屬於強連通份量。

如圖中強連通份量有三個:1-2-3,4,5

 

二、強連通份量怎麼找?

噫......固然,經過肉眼能夠很直觀地看出1-2-3是一組強連通份量,但很遺憾,機器並無眼睛,因此該怎麼判斷強連通份量呢?

若是還是上面那張圖,咱們對它進行dfs遍歷。

能夠注意到紅邊很是特別,由於若是按照遍歷時間來分類的話,其餘邊都指向在本身以後被遍歷到的點,而紅邊指向的則是比本身先被遍歷到的點。

 

若是存在這麼一條邊,那麼咱們能夠yy一下,emmmm.......

從一個點出發,一直向下遍歷,而後忽得找到一個點,那個點居然有條指回這一個點的邊!

那麼想必這個點可以從自身出發再回到自身

想必這個點和其餘向下遍歷的該路徑上的全部點構成了一個環,

想必這個環上的全部點都是強聯通的。

但只是強聯通啊,咱們須要求的但是強連通份量啊......

 

那怎麼辦呢?

咱們仍是yy出那棵dfs樹

不妨想一下,何時一個點和他的全部子孫節點中的一部分構成強連通份量

他的子孫再也沒有指向他的祖先的邊,卻有指向他本身的邊

由於只要他的子孫節點有指向祖先的邊,顯然能夠構成一個更大的強聯通圖

 

好比說圖中紅色爲強連通份量,而藍色只是強聯通圖

 

那麼咱們只須要知道這個點u下面的全部子節點有沒有連着這個點的祖先就好了。

但彷佛還有一個問題啊......

 

咱們怎麼知道這個點u它下面的全部子節點必定是都與他強聯通的呢?

這彷佛是不對的,這個點u之下的全部點不必定都強聯通

那麼怎麼在退回到這個點的時候,知道全部和這個點u構成強連通份量的點呢?

開個記錄就好了

什麼?!這麼簡單?

沒錯~就是這麼簡單~

若是在這個點以後被遍歷到的點已經能與其下面的一部分點(也可能就只有他一個點)已經構成強連通份量,即它已是最大的

那麼把它們一塊兒從棧裏彈出來就好了。

因此最後處理到點u時若是u的子孫沒有指向其祖先的邊,那麼它以後的點確定都已經處理好了,一個常見的思想,能夠理解一下。

因此就能夠保證棧裏留下來u後的點都是能與它構成強連通份量的。

 

彷佛作法已經明瞭了,用程序應該怎麼實現呢?

 

因此爲了實現上面的操做,咱們須要一些輔助數組

(1)、dfn[ ],表示這個點在dfs時是第幾個被搜到的。

(2)、low[ ],表示這個點以及其子孫節點連的全部點中dfn最小的值

(3)、stack[ ],表示當前全部可能能構成是強連通份量的點。

(4)、vis[ ],表示一個點是否在stack[ ]數組中。

那麼按照之上的思路,咱們來考慮這幾個數組的用處以及tarjan的過程。

 

假設如今開始遍歷點u:

 

(1)、首先初始化dfn[u]=low[u]=第幾個被dfs到

dfn能夠理解,但爲何low也要這麼作呢?

 由於low的定義如上,也就是說若是沒有子孫與u的祖先相連的話,dfn[u]必定是它和它的全部子孫中dfn最小的(由於它的全部子孫必定比他後搜到)

 

(2)、將u存入stack[ ]中,並將vis[u]設爲true

stack[ ]有什麼用?

若是u在stack中,u以後的全部點在u被回溯到時u和棧中全部在它以後的點都構成強連通份量。

 

(3)、遍歷u的每個能到的點,若是這個點dfn[ ]爲0,即仍未訪問過,那麼就對點v進行dfs,而後low[u]=min{low[u],low[v]}

low[ ]有什麼用?

應該能看出來吧,就是記錄一個點它最大能連通到哪一個祖先節點(固然包括本身)

若是遍歷到的這個點已經被遍歷到了,那麼看它當前有沒有在stack[ ]裏,若是有那麼low[u]=min{low[u],low[v]}

若是已經被彈掉了,說明不管如何這個點也不能與u構成強連通份量,由於它不能到達u

若是還在棧裏,說明這個點確定能到達u,一樣u能到達他,他倆強聯通

 

(4)、假設咱們已經dfs完了u的全部的子樹那麼以後不管咱們再怎麼dfs,u點的low值已經不會再變了。

那麼若是dfn[u]=low[u]這說明了什麼呢?

再結合一下dfn和low的定義來看看吧

dfn表示u點被dfs到的時間,low表示u和u全部的子樹所能到達的點中dfn最小的。

這說明了u點及u點之下的全部子節點沒有邊是指向u的祖先的了,即咱們以前說的u點與它的子孫節點構成了一個最大的強連通圖即強連通份量

此時咱們獲得了一個強連通份量,把全部的u點之後壓入棧中的點和u點一併彈出,將它們的vis[ ]置爲false,若有須要也能夠給它們打上相同標記(同一個數字)

 

tarjan到此結束

至於手模?tan90°!網上有很多大佬已經手摸了很多樣例了,想必不須要本蒟蒻再補充了。

 

結合上面四步代碼已經能夠寫出了:

對了,tarjan一遍不能搜完全部的點,由於存在孤立點或者其餘

因此咱們要對一趟跑下來尚未被訪問到的點繼續跑tarjan

怎麼知道這個點有沒有被訪問呢?

看看它的dfn是否爲0

這看起來彷佛是o(n^2)的複雜度,但其實均攤下來每一個點只會被遍歷一遍

因此tarjan的複雜度爲o(n)

 

來一道例題吧,這是模板題,應該作到提交框AC

[USACO06JAN]牛的舞會The Cow Prom

 給你n個點,m條邊,求圖中全部大小大於1的強連通份量的個數

輸入樣例#1:
5 4 2 4 3 5 1 2 4 1
輸出樣例#1: 
1



顯然是tarjan水題,數出強連通份量的個數,給每一個強連通份量的點染色,統計出每一個強連通份量中點的個數,若是大於一,則答案加一。

代碼:

#include<queue> #include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define inf 0x3f3f3f3f vector<int> g[10010]; int color[10010],dfn[20020],low[20020],stack[20020],vis[10010],cnt[10010]; int deep,top,n,m,sum,ans; void tarjan(int u) { dfn[u]=++deep; low[u]=deep; vis[u]=1; stack[++top]=u; int sz=g[u].size(); for(int i=0;i<sz;i++) { int v=g[u][i]; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else { if(vis[v]) { low[u]=min(low[u],low[v]); } } } if(dfn[u]==low[u]) { color[u]=++sum; vis[u]=0; while(stack[top]!=u) { color[stack[top]]=sum; vis[stack[top--]]=0; } top--; } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); } for(int i=1;i<=n;i++) { if(!dfn[i]) { tarjan(i); } } for(int i=1;i<=n;i++) { cnt[color[i]]++; } for(int i=1;i<=sum;i++) { if(cnt[i]>1) { ans++; } } printf("%d\n",ans); }

 

2、tarjan縮點

其實這也是利用了tarjan求強連通份量的方法,對於一些貢獻具備傳導性,好比友情啊、路徑上的權值啊等等。

思想就是由於強連通份量中的每兩個點都是強連通的,能夠將一個強連通份量當作一個超級點,而點權按題意來定。

 

來看一道題吧。

poj2186 Popular Cows

告訴你有n頭牛,m個崇拜關係,而且崇拜具備傳遞性,若是a崇拜b,b崇拜c,則a崇拜c,求最後有幾頭牛被全部牛崇拜。

 

Sample Input
3 3
1 2
2 1
2 3
Sample Output
1

 

顯然一個強聯通份量內的全部點都是知足條件的,咱們能夠對整張圖進行縮點,而後就簡單了。

剩下的全部點都不是強連通的,如今整張圖就是一個DAG(有向無環圖)

那麼就變成一道水題了,由於這是一個有向無環圖,不存在全部點的出度都不爲零的狀況。

因此必然有1個及以上的點出度爲零,若是有兩個點出度爲零,那麼這兩個點確定是不相連的,即這兩圈牛不是互相崇拜的,因而此時答案爲零,若是有1個點出度爲0,那麼這個點就是被全體牛崇拜的,

這個點多是一個強聯通份量縮成的超級點,因此應該輸出整個強聯通份量中點的個數。

代碼:

#include<cmath> #include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; int dfn[10010],low[10010],vis[10010],stack[10010],color[10010],du[10010],cnt[10010]; int n,m,top,sum,deep,tmp,ans; vector<int> g[10010]; void tarjan(int u) { dfn[u]=low[u]=++deep; vis[u]=1; stack[++top]=u; int sz=g[u].size(); for(int i=0; i<sz; i++) { int v=g[u][i]; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else { if(vis[v]) { low[u]=min(low[u],low[v]); } } } if(dfn[u]==low[u]) { color[u]=++sum; vis[u]=0; while(stack[top]!=u) { color[stack[top]]=sum; vis[stack[top--]]=0; } top--; } } int main() { while(scanf("%d%d",&n,&m)!=EOF) { memset(vis,0,sizeof(du)); memset(vis,0,sizeof(low)); memset(dfn,0,sizeof(dfn)); memset(vis,0,sizeof(vis)); memset(vis,0,sizeof(cnt)); memset(vis,0,sizeof(color)); memset(vis,0,sizeof(stack)); for(int i=1; i<=n; i++) { g[i].clear(); } for(int i=1; i<=m; i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); } for(int i=1; i<=n; i++) { if(!dfn[i]) { tarjan(i); } } for(int i=1; i<=n; i++) { int sz=g[i].size(); for(int j=0; j<sz; j++) { int v=g[i][j]; if(color[v]!=color[i]) { du[color[i]]++; } } cnt[color[i]]++; } for(int i=1; i<=sum; i++) { if(du[i]==0) { tmp++; ans=cnt[i]; } } if(tmp==0) { printf("0\n"); } else { if(tmp>1) { printf("0\n"); } else { printf("%d\n",ans); } } } }

3、tarjan求割點、橋

一、什麼是割點、橋

再來引用一遍度娘:

在一個無向圖中,若是有一個頂點集合,刪除這個頂點集合以及這個集合中全部頂點相關聯的邊之後,圖的連通份量增多,就稱這個點集爲割點集合。

又是一臉懵逼。。。。

總而言之,就是這個點維持着雙聯通的繼續,去掉這個點,這個連通份量就沒法在維持下去,分紅好幾個連通份量。

好比說上圖紅色的即爲一個割點。

橋:

若是一個無向連通圖的邊連通度大於1,則稱該圖是邊雙連通的 (edge biconnected),簡 稱雙連通或重連通。一個圖有橋,當且僅當這個圖的邊連通度爲 1,則割邊集合的惟一元素 被稱爲橋(bridge),又叫關節邊(articulationedge)。一個圖可能有多個橋。(該資料一樣來自百度)

對於連通圖有兩種雙聯通,邊雙和點雙,橋之於邊雙如同割點之於點雙

如圖則是一個橋。

二、割點和橋怎麼求?

與以前強連通份量中的tarjan差很少。但要加一個特判,根節點若是有兩個及以上的兒子,那麼他也是割點。

 

 

模板題:洛谷3388

求割點的個數和數量

代碼:

#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
#define hi printf("hi!");
using namespace std; vector<int> g[10010]; int dfn[10010],low[10010],iscut[10010],son[10010]; int deep,root,n,m,ans; int tarjan(int u,int fa) { int child=0,lowu; lowu=dfn[u]=++deep; int sz=g[u].size(); for(int i=0;i<sz;i++) { int v=g[u][i]; if(!dfn[v]) { child++; int lowv=tarjan(v,u); lowu=min(lowu,lowv); if(lowv>dfn[u]) { iscut[u]=1; } } else { if(v!=fa&&dfn[v]<dfn[u]) { lowu=min(lowu,dfn[v]); } } } if(fa<0&&child==1) { iscut[u]=false; } low[u]=lowu; return lowu; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } for(int i=1;i<=n;i++) { if(!dfn[i]) { root=i; tarjan(i,-1); } } for(int i=1;i<=n;i++) { if(iscut[i]) { ans++; } } printf("%d\n",ans); for(int i=1;i<=n;i++) { if(iscut[i]) { printf("%d ",i); } } }

橋的求法也差很少

 

 

並無找到模板題目,因此只好把沒檢驗過的代碼放着了......若有錯誤還請留言指正

#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
#define hi printf("hi!");
using namespace std; vector<pair<int,int> >bridge; vector<int> g[10010]; int dfn[10010],low[10010]; int deep,root,n,m,ans; int tarjan(int u,int fa) { int lowu; lowu=dfn[u]=++deep; int sz=g[u].size(); for(int i=0;i<sz;i++) { int v=g[u][i]; if(!dfn[v]) { int lowv=tarjan(v,u); lowu=min(lowu,lowv); if(lowv>dfn[u]) { int from,to; from=u; to=v; if(from>to) { swap(from,to); } bridge.push_back(make_pair(from,to)); } } else { if(v!=fa&&dfn[v]<dfn[u]) { lowu=min(lowu,dfn[v]); } } } low[u]=lowu; return lowu; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } for(int i=1;i<=n;i++) { if(!dfn[i]) { root=i; tarjan(i,-1); } } for(int i=0;i<bridge.size();i++) { printf("%d %d\n",bridge[i].first,bridge[i].second); } }

 

 

 

 

おわり

相關文章
相關標籤/搜索