Tarjan Algorithm

Tarjan Algorithm


List

Knowledge

基本知識

Tarjan 主要是用來求有向圖的強連通分量(縮點)和無向圖的橋和割頂。首先都是要求出 DFN 和 LOW 值:DFN 是指一個點被搜索的次序編號,LOW 是指一個點的子樹中最小編號。

基本概念

  1. 割點:若刪掉某點後,原連通圖分裂爲多個子圖,則稱該點爲割點。
  2. 割點集合:在一個無向連通圖中,如果有一個頂點集合,刪除這個頂點集合,以及這個集合中所有頂點相關聯的邊以後,原圖變成多個連通塊,就稱這個點集爲割點集合。
  3. 點連通度:最小割點集合中的頂點數。
  4. 割邊(橋):刪掉它之後,圖必然會分裂爲兩個或兩個以上的子圖。
  5. 割邊集合:如果有一個邊集合,刪除這個邊集合以後,原圖變成多個連通塊,就稱這個點集爲割邊集合。
  6. 邊連通度:一個圖的邊連通度的定義爲,最小割邊集合中的邊數。
  7. 縮點:把沒有割邊的連通子圖縮爲一個點,此時滿足任意兩點之間都有兩條路徑可達。
    注:求塊<>求縮點。縮點後變成一棵k個點k-1條割邊連接成的樹。而割點可以存在於多個塊中。
  8. 雙連通分量:分爲點雙連通和邊雙連通。它的標準定義爲:點連通度大於1的圖稱爲點雙連通圖,邊連通度大於1的圖稱爲邊雙連通圖。通俗地講,滿足任意兩點之間,能通過兩條或兩條以上沒有任何重複邊的路到達的圖稱爲雙連通圖。無向圖G的極大雙連通子圖稱爲雙連通分量。

複雜度

O(n+m)

有向圖

首先搜索一個點,賦 DFN 和 LOW 初值爲它的 DFN,把它丟進棧裏,然後
遍歷它的每個相鄰點:
if 如果該點沒被搜到過
then 搜索該點 然後用該點的 LOW 更新現在點的 LOW
else if 下個點還在棧裏
then 用下個點的 DFN/LOW 更新這個點的 LOW (爲什麼 DFN 和 LOW 都可以:因爲它在棧中說明它還不是強連通分量,所以 LOW 還沒被更新)
最後判斷一下它的 LOW 值和 DFN 值是否還相等,如果相等,說明有兩種情況:
1.它沒有出邊
2.它的子節點最終回到了它
兩種情況都說明以它爲根的樹爲一個強連通分量。這個時候就只要不斷退棧到這個點爲止,退出來的所有元素爲一個強連通分量。

Code

struct Tarjan{
    static const int N=500010,M=500010;
    int n,m,tot,_clock,scc;
    int ne[M],to[M];bool in[N];
    int fr[N],low[N],dfn[N],f[N],st[N];
    inline void add(int u,int v){
        to[++tot]=v;ne[tot]=fr[u];fr[u]=tot;
    }
    void Init(){
        n=gi(),m=gi();
        for(int i=1;i<=m;i++){
            int u=gi(),v=gi();add(u,v);
        }
    }
    inline void dfs(int x){
        dfn[x]=low[x]=++_clock;
        st[++tot]=x;in[x]=true;
        for(int o=fr[x];o;o=ne[o])
            if(!dfn[to[o]])dfs(to[o]),low[x]=min(low[x],low[to[o]]);
            else if(in[to[o]])low[x]=min(low[x],low[to[o]]);
        if(dfn[x]==low[x]){
            scc++;
            while(st[tot]!=x){
                f[st[tot]]=scc;in[st[tot]]=false;tot--;
            }
            f[x]=scc;in[x]=false;tot--;
        }
    }
    void Work(){
        _clock=tot=scc=0;
        memset(dfn,0,sizeof(dfn));
        memset(in,false,sizeof(in));
        for(int i=1;i<=n;i++)
            if(!dfn[i])dfs(i);
    }
}Tar;

縮點

上面代碼中f[]代表沒個點屬於那個塊,只需把每一塊縮成一個點即可,如果兩個塊中的點有相連,那麼將這個塊連起來

Code

//在上面結構體中加上這幾行就行
vector<int>p[N]
void Rebuild(){
    for(int i=1;i<=n;i++)
        for(int o=fr[i];o;o=ne[o]){
            if(f[i]==f[to[o]])continue;
            p[f[i]].push_back(f[to[o]]);
        }    
}

用途

縮完點非常好,dfs隨便跑,有很多方面應用,比如下面Practice的第一題
這個圖會變得非常優美,有向無環圖→DAG
DAG

無向圖

在無向圖中因爲一條邊可以來回走,所以要保證不能用父親更新,但是直接
記父親又不行,因爲可能會有重邊,所以就要記錄邊的編號。更新的過程還是一
樣 的 。

Articulation Point-割頂與連通度

在無向連通圖中,刪除一個頂點v及其相連的邊後,原圖從一個連通分量變成了兩個或多個連通分量,則稱頂點v爲割點,同時也稱關節點(Articulation Point)。一個沒有關節點的連通圖稱爲重連通圖(biconnected graph)。若在連通圖上至少刪去k 個頂點才能破壞圖的連通性,則稱此圖的連通度爲k。

關節點和重連通圖在實際中較多應用。顯然,一個表示通信網絡的圖的連通度越高,其系統越可靠,無論是哪一個站點出現故障或遭到外界破壞,都不影響系統的正常工作;又如,一個航空網若是重連通的,則當某條航線因天氣等某種原因關閉時,旅客仍可從別的航線繞道而行;再如,若將大規模的集成電路的關鍵線路設計成重連通的話,則在某些元件失效的情況下,整個片子的功能不受影響,反之,在戰爭中,若要摧毀敵方的運輸線,僅需破壞其運輸網中的關節點即可。

割頂求法
1. 對根節點u,若其有兩棵或兩棵以上的子樹,則該根結點u爲割點;
2. 若low[v]>=dfn[u],則u爲割點,u和它的子孫形成一個塊。因爲這說明u的子孫不能夠通過其他邊到達u的祖先,這樣去掉u之後,圖必然分裂爲兩個子圖。Analysis:對非葉子節點u(非根節點),若其子樹的節點均沒有指向u的祖先節點的回邊,說明刪除u之後,根結點與u的子樹的節點不再連通;則節點u爲割點。

Code


struct ArticulationPoint{
    static const int N=500010,M=500010;
    int n,m,tot,_clock;
    int ne[M],to[M];bool cut[N];
    int fr[N],low[N],dfn[N],father[N];
    inline void add(int u,int v){
        to[++tot]=v;ne[tot]=fr[u];fr[u]=tot;
    }
    void Init(){
        n=gi(),m=gi();
        for(int i=1;i<=m;i++){
            int u=gi(),v=gi();add(u,v);add(v,u);
        }
    }
    inline void Tarjan(int x,int fa){
        dfn[x]=low[x]=++_clock;father[x]=fa;
        for(int o=fr[x];o;o=ne[o])
            if(!dfn[to[o]]){
                Tarjan(to[o],x);
                low[x]=min(low[x],low[to[o]]);
            }
            else if(to[o]>>1!=fa>>1)low[x]=min(low[x],low[to[o]]);
    }
    void Work(){
        _clock=0;
        memset(dfn,0,sizeof(dfn));
        for(int i=1;i<=n;i++)
            if(!dfn[i])Tarjan(i,-1);
        tot=0;int k=0;//這裏1爲根節點
        for(int i=2;i<=n;i++)
            if(father[i]==1)tot++;
            else if(low[i]>=dfn[father[i]])cut[father[i]]=1,++k;
        if(tot>1)cut[1]=true,++k;
        printf("%d\n",k);//割頂個數
        for(int i=1;i<=n;i++)if(cut[i])printf("%d ",i);
    }
}Tar;

Bridge-橋

橋 的 判 定 方 法 是 , 如 果 DFN(u) < LOW(v) 則 邊 (u,v) 爲 橋 。( 因 爲
DFN(u) < LOW(v)說明 v 沒有回到 u 之前的節點,即 u 和 v 不是一個塊(雙連通
分量,顧名思義要有兩條路)的。)

Code

struct Bridge{
    static const int N=500010,M=500010;
    int n,m,tot,_clock,ans;//ans 統計橋的個數
    int ne[M],to[M];
    int fr[N],low[N],dfn[N];
    inline void add(int u,int v){
        to[++tot]=v;ne[tot]=fr[u];fr[u]=tot;
    }
    void Init(){
        n=gi(),m=gi();
        for(int i=1;i<=m;i++){
            int u=gi(),v=gi();add(u,v);add(v,u);
        }
    }
    inline void Tarjan(int x,int fa){
        dfn[x]=low[x]=++_clock;
        for(int o=fr[x];o;o=ne[o])
            if(!dfn[to[o]]){
                Tarjan(to[o],x);
                low[x]=min(low[x],low[to[o]]);
                if(low[to[o]]>dfn[x])ans++;
            }
            else if(to[o]>>1!=fa>>1)low[x]=min(low[x],low[to[o]]);//重邊不爲橋
    }
    void Work(){
        _clock=ans=0;
        memset(dfn,0,sizeof(dfn));
        for(int i=1;i<=n;i++)
            if(!dfn[i])Tarjan(i,-1);
        printf("%d",ans);
    }
}Tar;

一些有用的定理

  1. 有向無環圖中唯一出度爲0的點,一定可以由任何點出發均可達(由於無環,所以從任何點出發往前走,必然終止於一個出度爲0的點)
  2. 有向無環圖中所有入度不爲0的點,一定可以由某個入度爲0的點出發可達。(由於無環,所以從任何入度不爲0的點往回走,必然終止於一個入度爲0的點)

Practice

Bzoj1179 Apio2009 Atm(2016-09-20 19:54)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章