摘要圖片來源:http://www.javashuo.com/article/p-hzuafiqx-vc.htmlhtml
Tarjan這東西,最基礎的用法就是求強連通份量和環,而後還能夠求割點、割邊,以及能夠縮點。看題目:數組
首先,你須要知道:強連通、強連通圖、強連通份量、dfs序、dfs樹
那麼,這都是些啥?在一個有向圖中,點\([u,v]\)能夠互相到達,那麼咱們就稱\([u,v]\)是強連通。而強連通圖,就是對於任意兩個\([u,v]\)都能互相到達。而強連通份量,就是在一個子圖中,知足對於任意\([u,v]\)都是強連通。
而dfs序,則是在dfs的過程當中,第一次被遍歷到的時間戳。dfs樹就是在dfs過程當中所造成的樹,顯然,從\(u\)dfs到\(v\),那\(u\)就是\(v\)的父節點,\(v\)就是\(u\)的子節點。不過這棵樹還可能從一個結點回到本身的祖先。
那接下來就講如何用Tarjan求強連通份量吧。首先,咱們從任意一個點開始dfs,而後咱們須要兩個數組:\(low\)和\(dfn\)。\(low_i\)表示點\(i\)所在的強連通份量的根(咱們把dfs樹中的一個強連通份量當作一棵子樹,那這個強連通份量的根就是在這個強連通份量中第一個被遍歷到的點)。而\(dfn_i\)則表示點\(i\)的dfs序。接着,咱們每遍歷到一個點\(u\),就記錄她的\(dfn\),而後讓\(low_u\)的初值等於\(dfn_u\),再把她放進棧裏。接着遍歷她所鏈接的點\(v\),\(v\)有兩種狀況,一種狀況是不在棧中,也就是沒被遍歷過,也就是兒子結點,那麼就再作dfs(v),而後更新\(low_u = min(low_u,low_v)\)。第二種是在棧中,也就是是她的祖先,那麼\(low_u = min(low_u,dfn_v)\)。最後,若是\(low_u\)仍是等於\(dfn_u\),那就說明\(u\)是這個強連通份量的根,那麼就讓\(u\)和\(u\)上面的點所有出棧。你覺得這樣就行了嗎?不是的,因爲這個圖不必定連通,因此最好循環\(1~n\),若是\(dfn\)沒有更新,那麼就是沒走過,那就要作Tarjan。
接下來來看上面給的模板,明顯是要求點的個數大於1的強連通份量的個數。(注意,只有1個點也是強連通份量)那咱們只要在出棧是記錄數量就能夠了,由於在\(u\)上面的點都是\(u\)所在的強連通份量中的。
code:spa
#include<cstdio> #include<stack> using namespace std; int n,m,ans; int index,low[10005],dfn[10005],vis[10005]; //index用來記錄當前搜了幾個點,也就是當前dfn應該取幾,而vis[i]表示以i爲根的強連通份量有幾個點 stack<int>s; bool f[10005];//用來判斷是否在棧裏 struct graph { int tot,hd[10005]; int nxt[50005],to[50005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; return ; } }g;//鏈式前向星 void Tarjan(int x) { dfn[x]=low[x]=++index; s.push(x); f[x]=true; //初始化 for(int i=g.hd[x];i;i=g.nxt[i])//遍歷全部連通的點 if(!dfn[g.to[i]])//子節點 { Tarjan(g.to[i]);//繼續Tarjan low[x]=min(low[x],low[g.to[i]]);//更新答案 } else if(f[g.to[i]])//祖先 low[x]=min(low[x],dfn[g.to[i]]);//更新答案 if(dfn[x]==low[x])//若是是根 { vis[x]=1;//更新vis while(s.top()!=x) { f[s.top()]=false; s.pop(); vis[x]++;//更新vis }//出棧 f[x]=false; s.pop(); //出棧 } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); } for(int i=1;i<=n;i++) { if(!dfn[i]) Tarjan(i); if(vis[i]>1) ans++; } printf("%d",ans); return 0; }
什麼是縮點呢?縮點,就是把一堆點縮成一個點。固然不能隨便縮。若是有一堆點,她們能互相到達,也就是說這是一個強連通份量,那麼把她們縮成一個點,是否是對題目自己沒有影響?但縮完點以後他就變成了一個DAG,並且點和邊的數量也大大減小了。這就是縮點。那麼明白什麼是縮點以後,那就很好寫了,咱們記錄\(scc_i\),表示i所在的強連通份量的編號。而後對每一個點遍歷,對於點\(i\),把本身的權值賦給\(scc_i\),而後枚舉全部出邊,若是終點不在同一個強連通份量中,那麼把這條邊記錄下來。注意,不能在原圖上作,也就是咱們要新建一個圖。接下來,咱們就能夠根據題目要求作了。
接下來看模板,那這個就是縮完點以後,作一遍拓撲排序,而後求最大值。
code:code
#include<cstdio> #include<stack> #include<queue> using namespace std; int mx,n,m,vv[10005]; int index,low[10005],dfn[10005]; stack<int>s; int f[10005]; int scc_cnt,scc[10005]; queue<int>q; int in[10005],vis[10005],dp[10005]; int max(int x,int y){return x>y?x:y;} struct graph { int tot,hd[10005]; int nxt[100005],to[100005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; } }g,sg; void Tarjan(int x)//求強連通份量 { dfn[x]=low[x]=++index; s.push(x); f[x]=true; for(int i=g.hd[x];i;i=g.nxt[i]) if(!dfn[g.to[i]]) { Tarjan(g.to[i]); low[x]=min(low[x],low[g.to[i]]); } else if(f[g.to[i]]) low[x]=min(low[x],dfn[g.to[i]]); if(dfn[x]==low[x])//出棧並記錄scc { scc[x]=++scc_cnt; while(s.top()!=x) { scc[s.top()]=scc_cnt; f[s.top()]=false; s.pop(); } f[x]=false; s.pop(); } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&vv[i]); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); } for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i); for(int i=1;i<=n;i++) { vis[scc[i]]+=vv[i]; for(int j=g.hd[i];j;j=g.nxt[j]) { int u=scc[i],v=scc[g.to[j]]; if(v!=u) { sg.add(u,v); in[v]++; } } }//建新圖 for(int i=1;i<=scc_cnt;i++) { dp[i]=vis[i]; if(!in[i]) q.push(i); } while(!q.empty()) { int x=q.front(); q.pop(); for(int i=sg.hd[x];i;i=sg.nxt[i]) { int y=sg.to[i]; dp[y]=max(dp[y],dp[x]+vis[y]); if(!--in[y]) q.push(y); } } //拓撲排序 for(int i=1;i<=scc_cnt;i++) mx=max(mx,dp[i]);//求得答案 printf("%d",mx); return 0; }
割點,又叫割頂,這個東西的定義就是,在一個無向圖中,若是有一個點\(u\),把她和鏈接她的邊去掉後,連通數增長了,那麼這個點\(u\)就是割點。
上面講到的連通數,你能夠理解爲在無向圖中的強連通份量的個數。
那這個割點怎麼求呢?很簡單,仍是作Tarjan,而後,若一個點\(u\)的子節點中存在一個\(v\),使得\(low_v \ge dfn_u\),那麼\(u\)就是割點。緣由很簡單,由於這樣就表示有至少一棵子樹不能到達她上面。但這隻對不是根結點的點有效。那麼根節點怎麼判斷呢?那就更簡單了,只要有兩棵及以上的子樹,那她就必定是割點,緣由本身想。
最後再提醒一下,並非一個點鏈接的全部點都是她的子節點。
code:htm
#include<cstdio> #include<stack> #include<queue> #include<vector> using namespace std; int n,m,ans,visit[20005]; int index,low[20005],dfn[20005]; stack<int>s; bool f[20005]; vector<int>son[20005]; struct graph { int tot,hd[20005]; int nxt[500005],to[500005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; return ; } }g; void Tarjan(int x)//作Tarjan { dfn[x]=low[x]=++index; s.push(x); f[x]=true; for(int i=g.hd[x];i;i=g.nxt[i]) if(!dfn[g.to[i]]) { Tarjan(g.to[i]); low[x]=min(low[x],low[g.to[i]]); son[x].push_back(g.to[i]);//記錄子節點 } else if(f[g.to[i]]) low[x]=min(low[x],dfn[g.to[i]]); if(dfn[x]==low[x]) { while(s.top()!=x) { f[s.top()]=false; s.pop(); } f[x]=false; s.pop(); } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); g.add(v,u); } for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i); for(int i=1;i<=n;i++) { if(dfn[i]==low[i])//特判根結點 { if(son[i].size()>1) visit[++ans]=i; continue; } for(int j=0;j<son[i].size();j++) if(low[son[i][j]]>=dfn[i])//知足條件 { visit[++ans]=i; break; } } printf("%d\n",ans); for(int i=1;i<=ans;i++) printf("%d ",visit[i]); //輸出答案 return 0; }
割邊又叫割橋,割邊的定義和割點類似,就是若是去掉一條邊使得連通數增長,那麼這條邊就是割邊。
割邊的求法也和割點類似,就是對於一條邊\([u,v]\),若\(low_v > dfn_u\),那麼這條邊就是割邊。這裏不取等於的緣由是,若\(v\)能夠經過另外一條邊到達\(u\),那麼這條邊天然不是割邊了。
割邊不用判斷根節點(畢竟找的是邊不是點),但須要特判通往父節點的那條邊(緣由顯然)。
接下來看模板,這題就是在求完割邊以後要再排一遍序,確保輸出的是有序的。
code:blog
#include<cstdio> #include<stack> #include<queue> #include<vector> #include<algorithm> using namespace std; int n,m,ans; int index,low[20005],dfn[20005]; stack<int>s; bool f[20005]; vector<int>son[20005]; struct srt { int u,v; }a[20005]; bool cmp(srt x,srt y) { return x.u<y.u||(x.u==y.u&&x.v<y.v); } struct graph { int tot,hd[20005]; int nxt[500005],to[500005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; return ; } }g; void Tarjan(int x,int dad) { dfn[x]=low[x]=++index; s.push(x); f[x]=true; for(int i=g.hd[x];i;i=g.nxt[i]) if(!dfn[g.to[i]]) { Tarjan(g.to[i],x); low[x]=min(low[x],low[g.to[i]]); son[x].push_back(g.to[i]); } else if(f[g.to[i]]&&g.to[i]!=dad)//特判父節點 low[x]=min(low[x],dfn[g.to[i]]); if(dfn[x]==low[x]) { while(s.top()!=x) { f[s.top()]=false; s.pop(); } f[x]=false; s.pop(); } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); g.add(v,u); } for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i,0); for(int i=1;i<=n;i++) { for(int j=0;j<son[i].size();j++)//上面兩重循環是枚舉每條邊 if(low[son[i][j]]>dfn[i])//符合條件 { ans++; a[ans].u=i; a[ans].v=son[i][j]; } } sort(a+1,a+ans+1,cmp);//注意要按順序輸出哦 for(int i=1;i<=ans;i++) printf("%d %d\n",a[i].u,a[i].v); return 0; }