[筆記]tarjan+縮點
阿新 • • 發佈:2020-09-07
[筆記]tarjan+縮點
演算法用途:
tarjan可以求強連通分量,縮點則是將一個強連通分量縮成一個點
tarjan
概念
1.有向圖的強連通分量:再有向圖G中,如果兩個頂點V_i,V_j間有一條從V_i到V_j的有向路徑同時還有一條從V_j到V_i的有向路徑,則稱這兩個點強連通,在一個圖的子圖中,任意兩點可以相互到達則稱這組成了一個強連通分量。
2.一個單獨的點也是一個強連通分量。
演算法描述
變數:
1.dfn陣列:dfn[i]表示i這個點再dfs時是第幾個被搜到的 2.low[i]:表示的是i這個節點和它的子孫節點中dfn最小的值 3.stack:表示所有可能構成強連通分量的點,其實就是一個棧
演算法過程
一 畫圖理解
tarjan的第一步是對整個圖進行dfs,搜完後會得到一棵dfs樹,舉個例子.這個樹時有向的,顯然在這個樹上是不會存在環的,因此能產生環的只有可能是一條指向已經訪問(搜尋)過的邊,也就是圖中的紅邊和藍邊。經過觀察發現,紅邊可以產生一個強連通分量,而藍邊不行,因為紅邊是由6號節點指向它的祖先4號節點的,這種紅邊稱為後向邊,而藍邊則指向兩個沒有父子關係的點,這種邊稱為橫叉邊,橫叉邊不一定產生環,而後向邊一定產生環。 知道了以上結論後就開始深搜首先會搜到這樣的圖,此時stack = {1,2,3},而3沒有多餘的指向其它點的邊,因此將3彈出棧,單獨作為一個強連通分量,繼續深搜,會搜到這樣一個圖,此時stack = {1,2,7} 發現節點7指向已經搜尋過的節點3,是上述兩種可能存在環的情況,而此時3不再stack中,因此不存在環,將7,2依次彈出棧中,單獨作為一個強連通分量,再次深搜,會搜到這樣一個圖,此時stack = {1,4,5,6} 發現節點6有一條連向其他節點的邊,即紅邊,指向的是節點4,而這個點已經搜尋過了,符合上面說的產生環的條件,而此時發現節點4已經在stack中說明這是一條後向邊,可以產生環,因此4~6號節點中的所有點組成一個強連通分量。演算法結束 但實際情況可能更復雜,這裡出現了大環套小環的問題,我們需要對dfs過程稍作修改(見下)。
二 演算法完整步驟
1.首先初始化dfn[u] = low[u] = 第幾個被搜尋到,但並不是一開始就先跑一遍深搜,而是邊做邊賦值,具體見程式碼 2.將當前搜尋的節點u存入stack中 3.遍歷每一個與節點u相連的點,如果遍歷到的點的dfn值為0則說明這個點之前沒有被訪問過,那麼就對這個沒有被訪問過的點(假設為v)進行深搜並且更新low陣列的值:low[u] = min(low[u],low[v]),如果與u相連的點(假設為g)已經被搜尋過了,也就是說dfn[g]!=0,此時就更新low[u]的值:low[u] = min(low[u],dfn[g]),這樣就可以保證low[u]儲存的是最先被dfs到的點,也就保證了找的環是最大的。但為什麼是用dfn[g]來更新呢?因為節點g可能是另一個強連通分量裡的節點,只是還沒有出棧,因此節點u可能不能到達low[g],但u一定可以到達dfn[g]。 4.那麼什麼時候說明找到了一個完整的最大的環呢?當我們找到一個點u滿足low[u] == dfn[u]時,說明這個點的子樹中不存在比這個點先搜到的點,則節點u為它所在的強連通分量裡的根節點,所以將stack中u及它之後的點全部彈出,這就是一個強連通分量。
縮點
演算法描述
縮點其實很好理解,也很好實現,只需要新建一個col陣列,用來將同一個連通塊內的點染成一個顏色。具體實現看程式碼。縮完點後的圖是一個有向無環圖,各個縮完的點由跨越不同強連通分量的邊來連線。
例題應用
原題鏈
題目分析
首先建圖,如果A認為B受歡迎,則連一條從B到A的有向邊,這樣更方便求強連通分量。然後用tarjan求出所有的強連通分量,再縮點,找到一個出度為0的縮完後的點,這就是答案,因為縮完點後的圖是一個有向無環圖,如果一個縮點有出度,則它一定不能被它連出去的那個縮點的牛所喜歡;而一個縮完點後的圖中,也不能出現兩個沒有出度的縮點,因為這樣它們就不能被對方縮點裡的牛所喜歡,因此我們的答案是隻有一個縮點出度為0的圖中那個縮點的大小
程式碼(tarjan+縮點 & AC code)
#include <bits/stdc++.h>
using namespace std;
struct node{
int to,next;
}edge[100010];
int fir[100010],dfn[100010],low[100010],col[100010],num,tot,coln,size[100010],degree[100010];
stack < int > s;
void add(int x,int y){
tot++;
edge[tot].to = y;
edge[tot].next = fir[x];
fir[x] = tot;
}
void tarjan(int k){
low[k] = dfn[k] = ++num;
s.push(k);
for(int i = fir[k];i;i = edge[i].next){
int x = edge[i].to;
if(!dfn[x]){
tarjan(x);
low[k] = min(low[k],low[x]);
}
else if(!col[x]){//發現後向邊
low[k] = min(low[k],dfn[x]);
}
}
if(low[k] == dfn[k]){//找到了一個強連通分量的根節點
col[k] = ++coln;//縮到一個點裡
++size[coln];//更新縮點的大小
while(s.top() != k){
col[s.top()] = coln;//縮點
++size[coln];//更新縮點的大小
s.pop();
}
s.pop();//千萬不要忘記這一步,要將k節點彈出
}
return;
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int x,y;
scanf("%d%d",&x,&y);
add(y,x);
}
for(int i = 1;i <= n;i++){
if(!dfn[i])
tarjan(i);
}
for(int i = 1;i <= n;i++){
for(int j = fir[i];j;j = edge[j].next){
int x = edge[j].to;
if(col[x] != col[i]){//顏色不同說明不在一個塊中,所以出度增加
++degree[col[x]];//因為建的是反向邊,所以其實是從x連到i
}
}
}
int flag = 0;//記錄有多少出度為0的縮點,如果大於1個則答案為0
int ans = 0;
for(int i = 1;i <= coln;i++){
if(degree[i] == 0){
++flag;
ans = size[i];
}
}
if(flag != 1){
printf("0\n");
}
else printf("%d\n",ans);
return 0;
}