割點(Tarjan算法)

本文可轉載,轉載請註明出處:www.cnblogs.com/collectionne/p/6847240.html 。本文未完,若是不在博客園(cnblogs)發現此文章,請訪問以上連接查看最新文章。html

 

前言:以前翻譯過一篇英文的關於割點的文章(英文原文翻譯),可是本身還有一些不明白的地方,這裏就再次整理了一下。有興趣能夠點我給的兩個連接。算法

 

割點的概念

 

無向連通圖中,若是將其中一個點以及全部鏈接該點的邊去掉,圖就再也不連通,那麼這個點就叫作割點(cut vertex / articulation point)。數組

 

例如,在下圖中,0、3是割點,由於將0和3中任意一個去掉以後,圖就再也不連通。若是去掉0,則圖被分紅一、2和三、4兩個連通份量;若是去掉3,則圖被分紅0、一、2和4兩個連通份量。優化

 

 

怎麼求割點

 

直接DFS

 

最容易想到的方法就是依次刪除每一個割點,而後DFS,但這種方法效率過低,這裏不作討論。spa

 

DFS樹

 

首先須要瞭解一些關於DFS樹(DFS tree)的概念。如下圖爲例:翻譯

 

 

從點1開始搜索整個圖, 對於每一個點相鄰的頂點,按照頂點編號從小到大搜索(也能夠按其它順序)。所以上圖的搜索順序以下:code

 

第1步,與1相鄰的點有{2, 4},選2。htm

 

第2步,與2相鄰的點有{1, 3, 4},1訪問過,選3。blog

 

第3步,與3相鄰的點有{2, 5},2訪問過,選5。get

 

第4步,與5相鄰的點有{3},訪問過,退出。

 

退回第3步,與3相鄰的點有{2, 5},都訪問過,退出。

 

退回第2步,與2相鄰的點有{1, 3, 4},一、3訪問過,選4。

 

第5步,與4相鄰的點有{1, 2},都訪問過,退出。

 

退回第2步,與2相鄰的點有{1, 3, 4},都訪問過,退出。

 

退回第1步,與1相鄰的點有{2, 4},都訪問過,退出。

 

至此,訪問結束。

 

把訪問頂點的路徑表示出來就是這樣的(訪問已訪問過的頂點時加上刪除線並再也不訪問,end表示與某個頂點相鄰的頂點遍歷完畢,{}裏是與一個頂點相鄰的全部頂點)。

 

1 {2,4}
  2 {1,3,4}
    1
    3 {2,5}
      2
      5 {3}
        3
        end
      end
    4 {1,2}
      1
      2
      end
    end
  4
  end

 

訪問路徑能夠繪製成下圖(綠邊爲訪問未訪問頂點時通過的邊,紅邊爲訪問已訪問節點是通過的邊):

 

 

咱們把上圖稱爲DFS搜索樹(DFS tree),上圖中的綠邊稱爲樹邊(tree edge),紅邊稱爲回邊(back edge)。經過回邊能夠從一個點返回到之間訪問過的頂點

 

你可能會有疑問,「訪問已訪問節點時所通過的邊叫回邊」,咱們上面不是沒有訪問嗎?實際上是有的,可是爲方便就不寫了,並且遇到已訪問的邊(在後面的算法裏)只是簡單計算一下,再也不繼續DFS了。

 

注意,在上圖中,若是與一個頂點相鄰A的頂點B是A的父節點,不表示出來,接下來的算法遇到這種狀況也不計算

 

Tarjan算法

 

可使用Tarjan算法求割點(注意,還有一個求連通份量的算法也叫Tarjan算法,與此算法相似)。(Tarjan,全名Robert Tarjan,美國計算機科學家。)

 

首先選定一個根節點,從該根節點開始遍歷整個圖(使用DFS)。

 

對於根節點,判斷是否是割點很簡單——計算其子樹數量,若是有2棵即以上的子樹,就是割點。由於若是去掉這個點,這兩棵子樹就不能互相到達。

 

對於非根節點,判斷是否是割點就有些麻煩了。咱們維護兩個數組dfn[]和low[],dfn[u]表示頂點u第幾個被(首次)訪問,low[u]表示頂點u及其子樹中的點,經過非父子邊(回邊),可以回溯到的最先的點(dfn最小)的dfn值(但不能經過鏈接u與其父節點的邊)。對於邊(u, v),若是low[v]>=dfn[u],此時u就是割點。

 

但這裏也出現一個問題:怎麼計算low[u]。

 

假設當前頂點爲u,則默認low[u]=dfn[u],即最先只能回溯到自身。

 

有一條邊(u, v),若是v未訪問過,繼續DFS,DFS完以後,low[u]=min(low[u], low[v]);

 

若是v訪問過(且u不是v的父親),就不須要繼續DFS了,必定有dfn[v]<dfn[u],low[u]=min(low[u], dfn[v])。

 

代碼

 

DFS

 

先回憶一下怎麼用DFS遍歷一個圖,代碼以下:

 

bool vis[N];            // 頂點是否訪問過
vector<int> g[N];     // 鄰接表表示的圖

// 調用dfs()前需將整個vis[]設爲false
void dfs(int u)
{
    vis[u] = true;
    for (int v: g[u])
    {
        if (!vis[v])
            dfs(v);
    }
}

 

Tarjan算法

 

首先假設u是根節點。若是u有兩棵以上的子樹,則u爲割點。代碼:

 

int children = 0;
for (int v: g[u])
{
    if (!vis[v])
    {
        children++;
        dfs(v);   // 繼續DFS
    }
}
if (children >= 2)
    // u是割點

 

非根節點呢?按照前面的描述,代碼以下:

 

// 默認u不能回溯到任何前面的點
low[u] = dfn[u];
for (int v: g[u])
{
    // (u, v)爲樹邊
    if (!vis[v])
    {
        // 設置v的父親爲u
        parent[v] = u;
        // 繼續DFS,遍歷u的子樹
        dfs(v);
        // u子樹遍歷完畢,low[v]已求出,low[u]取最小值
        low[u] = min(low[u], low[v]);

        if (low[v] >= dfn[u])
            // u是割點
    }
    // (u, v)爲回邊,且v不是u的父親
    else if (v != parent[u])
        low[u] = min(low[u], dfn[v]);
}

 

綜合起來,加上一些其它部分,Tarjan算法的代碼以下:

 

const int V = 20;
int dfn[V], low[V], parent[V];
bool vis[V], ap[V];
vector<int> g[V];

void dfs(int u)
{
    static int count = 0;
    // 子樹數量
    int children = 0;

    // 默認low[u]等於dfn[u]
    dfn[u] = low[u] = ++count;
    vis[u] = true;

    // 遍歷與u相鄰的全部頂點
    for (int v: g[u])
    {
        // (u, v)爲樹邊
        if (!vis[v])
        {
            // 遞增子樹數量
            children++;
            // 設置v的父親爲u
            parent[v] = u;
            // 繼續DFS
            dfs(v);
            // DFS完畢,low[v]已求出,若是low[v]<low[u]則更新low[u]
            low[u] = min(low[u], low[v]);

            // 若是是根節點且有兩棵以上的子樹則是割點
            if (parent[u] == -1 && children >= 2)
                cout << "Articulation point: " << u << endl;
            // 若是不是根節點且low[v]>=dfn[u]則是割點
            else if (parent[u] != -1 && low[v] >= dfn[u])
                cout << "Articulation point: " << u << endl;
        }
        // (u, v)爲回邊,且v不是u的父親
        else if (v != parent[u])
            low[u] = min(low[u], dfn[v]);
    }
}

 

不過有一個問題:可能會重複輸出一個割點。例如一個圖裏有(1, 2)、(1, 3)、(1, 4)和(1, 5)四條邊(取1爲根節點),發現(1, 3)時就已經輸出了1,但發現(1, 4)和(1, 5)時就又輸出了兩遍。因此須要使用一個數組ap[]來記錄割點。

 

還有一個能夠優化的地方:咱們使用vis[]來記錄一個點是否訪問過。可是咱們想一下,不是隻有訪問過的點纔會分配dfn嗎?固然,沒有訪問過的頂點,dfn[]裏也有值,但這裏dfn[]是全局的,所以它的每一個元素最初都是0。所以徹底能夠取消vis[]數組並把!vis[v]改爲!dfn[v]。

 

最後一個點:下面的代碼:

 

if (parent[u] == -1 && children >= 2)
    cout << "Articulation point: " << u << endl;
else if (parent[u] != -1 && low[v] >= dfn[u])
    cout << "Articulation point: " << u << endl;

 

能夠合起來寫成:

 

if (parent[u] == -1 && children >= 2 || parent[u] != -1 && low[v] >= dfn[u])
    cout << "Articulation point: " << u << endl;

 

固然,還須要加上對ap[]的檢查。

 

對Tarjan算法的詳細理解

 

1. 

 

Todo

 

對算法的詳細理解

 

首先,「根節點有n棵子樹」這句話,是說這n棵子樹是獨立的,沒有根節點不能互相到達。所以n不必定等於與根節點相鄰的頂點數。所以加入了vis[v]爲false的條件,由於若是(u, v1)和(u, v2)在一棵子樹裏,對v1進行DFS,必定能去到v2,vis[v2]就會爲true,此時就不會children++了。

 

對於邊(u, v),若是low[v]>=dfn[u],即v即其子樹可以(經過非父子邊)回溯到的最先的點,最先也只能是u,要到u前面就須要u的回邊或u的父子邊。也就是說這時若是把u去掉,u的回邊和父子邊都會消失,那麼v最先可以回溯到的最先的點,已經到了u後面,沒法到達u前面的頂點了,此時u就是割點。

相關文章
相關標籤/搜索