tarjan求強連通分量+縮點+割點/割橋(點雙/邊雙)
轉載自 這裡
一、tarjan求強連通分量
1、什麼是強連通分量?
引用來自度孃的一句話:
“有向圖強連通分量:在有向圖G中,如果兩個頂點vi,vj間(vi>vj)有一條從vi到vj的有向路徑,同時還有一條從vj到vi的有向路徑,則稱兩個頂點強連通(strongly connected)。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱為強連通分量(strongly connected components)。”
一臉懵逼......不過倒也不難理解。
反正就是在圖中找到一個最大的圖,使這個圖中每個兩點都能夠互相到達。這個最大的圖稱為強連通分量,同時一個點也屬於強連通分量。
如圖中強連通分量有三個:1-2-3,4,5
2、強連通分量怎麼找?
噫......當然,通過肉眼可以很直觀地看出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);
}
二、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);
}
}
}
}
三、tarjan求割點、橋
1、什麼是割點、橋
再來引用一遍度娘:
在一個無向圖中,如果有一個頂點集合,刪除這個頂點集合以及這個集合中所有頂點相關聯的邊以後,圖的連通分量增多,就稱這個點集為割點集合。
又是一臉懵逼。。。。
總而言之,就是這個點維持著雙聯通的繼續,去掉這個點,這個連通分量就無法在維持下去,分成好幾個連通分量。
比如說上圖紅色的即為一個割點。
橋:
如果一個無向連通圖的邊連通度大於1,則稱該圖是邊雙連通的 (edge biconnected),簡 稱雙連通或重連通。一個圖有橋,當且僅當這個圖的邊連通度為 1,則割邊集合的唯一元素 被稱為橋(bridge),又叫關節邊(articulationedge)。一個圖可能有多個橋。(該資料同樣來自百度)
對於連通圖有兩種雙聯通,邊雙和點雙,橋之於邊雙如同割點之於點雙
如圖則是一個橋。
2、割點和橋怎麼求?
與之前強連通分量中的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);
}
}