1. 程式人生 > >圖論初步-Tarjan演算法及其應用

圖論初步-Tarjan演算法及其應用

暑假刷了一堆Tarjan題到頭來還是忘得差不多。
這篇部落格權當複習吧。

一些定義

無向圖

割頂與橋 (劃重點)

圖G是連通圖,刪除一個點表示刪除此點以及所有與其相連的邊。
若刪除某點u後G不再連通,那麼u是G的一個割頂(割點)。
若刪除某邊e後G不再連通,那麼e是G的一個

雙連通

一個圖為雙連通,意思是說任一點對(u,v),從u到v都有兩條路徑。

廣義雙連通有兩種:點雙連通(狹義的雙連通)、邊雙連通。

  • 點雙連通:就是這兩條路徑除了起點和終點外無重複點。
  • 邊雙連通:就是這兩條路徑無重複邊。

例如,兩個簡單環共一個點的圖是邊雙連通的,但不是點雙連通的。

雙連通圖的性質

點雙連通圖刪除任意一個點仍然連通。
邊雙連通圖刪除任意一條邊仍然連通。

雙連通分量

雙連通分量也分為兩種。
圖G的雙連通分量也是G的子圖,且保證新增任意其他點、邊後都不再是雙連通的。
一個頂點可以屬於多個點雙連通分量,此時這個頂點必定是割頂。
顯然,點雙連通分量必定是邊雙連通分量。

哈密頓迴路和歐拉回路

哈密頓路是經過所有頂點一次的路徑。若起點和終點相同則稱哈密頓迴路。
尤拉路是經過所有邊一次的路徑。

有向圖

有向圖的連通性

  • 強連通:任意點對(u,v),存在從u到v的路徑從v到u的路徑。
  • 單連通:任意點對(u,v),存在從u到v的路徑從v到u的路徑。亦稱單向連通。
  • 弱聯通:忽略邊的方向得到的無向圖是連通的。此無向圖稱為該有向圖的底圖。

單連通圖一定是弱連通圖。反之則不然。

強連通分量(SCC)(劃重點)

亦如上面的定義。有向圖G的一個強連通分量是G的一個子圖,且任意新增其他點、邊都不能再滿足強連通的要求。

Tarjan演算法

Tarjan求割頂

首先從根節點開始dfs遍歷全圖,對於每個點我們定義兩個陣列:\(dfn[],low[]\),其中\(dfn[]\)是這個點的時間戳,用來記錄首次訪問這個點的時刻;而\(low[]\)則是用來記錄從這個點出發所能到達點的最小時間戳。可以配合註釋食用。

void tarjan(int u,int fath){
    dfn[u]=low[u]=++tmp;
        int child=0;
    for (int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if (!dfn[v]){//如果在此之前點v沒有被訪問過
            tarjan(v,u);
            low[u]=Min(low[u],low[v]);//更新最小時間戳
                        child++;
            if (!fath&&child>=2) rec[v]=1;//如果u為根節點且u下有兩棵子樹那麼u一定是割頂
            else (fath&&(low[v]>=dfn[u])) rec[v]=1;//如果u有子樹的最小時間戳不小於u的時間戳,那麼u一定是割頂
        }else{
            if (u!=fath) low[u]=Min(low[u],dfn[v]);//v的時間戳可能比u小,並且回邊不能算
        }
    }
}

Tarjan求橋

和求割頂非常類似

void tarjan(int u,int fath){
    dfn[u]=low[u]=++tmp;
        int child=0;
    for (int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if (!dfn[v]){
            tarjan(v,u);
            low[u]=Min(low[u],low[v]);
                        child++;
            if (dfn[u]<low[v]){e[++cnt].u=u;e[cnt].v=v;}
        }else {if (u!=fath) low[u]=Min(low[u],dfn[v]);}
    }
}

Tarjan求強連通分量

在Tarjan求強連通分量的過程中,low的定義有所改變,並且引入一個棧,low不是能到達的最前的dfn值,而是能到達的仍在棧中的dfn的最前值。

void tarjan(int u){
    dfn[u]=low[u]=++timer;
    st.push(u); instack[u]=true;
    for (int i=0;i<e[u].size();i++) {
        int v=e[u][i];
        if (dfn[v]==0) {//如果訪問過v
            tarjan(v);
            low[u]=min(low[u],low[v]);//更新最小時間戳
        } else if (instack[v]) {//如果在棧內(在同一個強連通分量內)
            low[u]=min(low[u],dfn[v]);
                        /*同low[u]=min(low[u],low[v]);
            LXQ:求scc應該都可以-.-因為你一旦找到了上一個scc那它們就都出棧了-.-所以後面更新判斷是否在棧中就保證了更新的值都是可行的
                        求scc不存在割頂的問題所以應該都可以*/
        }
    }
    if (dfn[u]==low[u]) {//這就是一個強連通分量的根節點
        int m;
        printf("SCC: ");
        do {
            m=st.top();
            st.pop();/*使棧頂出棧,把該強連通分量內除根節點外的
                       所有點彈出*/
            instack[m]=false;
            printf("%d ",m);
        } while (m!=u);
        printf("\n");
    }
}

縮點

一般,縮點是針對有向圖而言的。
找出了一個有向圖的強連通分量後我們可以把每個強連通分量縮為一個點。
如此一來我們便得到一個有向無環圖(DAG)。

有了有向無環圖,我們就可以在上面跑拓撲。能跑拓樸就能跑dp,因為狀態轉移的無後效性已經在DAG建立的同時保證了。