1. 程式人生 > >割點(Tarjan算法)

割點(Tarjan算法)

stat 退回 target 以及 之間 tar logs 討論 關於

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

前言:之前翻譯過一篇英文的關於割點的文章(英文原文、翻譯),但是自己還有一些不明白的地方,這裏就再次整理了一下。有興趣可以點我給的兩個鏈接。

割點的概念

無向連通圖中,如果將其中一個點以及所有連接該點的邊去掉,圖就不再連通,那麽這個點就叫做割點(cut vertex / articulation point)。

例如,在下圖中,0、3是割點,因為將0和3中任意一個去掉之後,圖就不再連通。如果去掉0,則圖被分成1、2和3、4兩個連通分量;如果去掉3,則圖被分成0、1、2和4兩個連通分量。

技術分享

怎麽求割點

直接DFS

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

DFS樹

首先需要了解一些關於DFS樹(DFS tree)的概念。以下圖為例:

技術分享

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

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

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

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

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

退回第3步,與3相鄰的點有{2

, 5},都訪問過,退出。

退回第2步,與2相鄰的點有{1, 3, 4},1、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],即v即其子樹能夠(通過非父子邊)回溯到的最早的點,最早也只能是u,要到u前面就需要u的回邊或u的父子邊。也就是說這時如果把u去掉,u的回邊和父子邊都會消失,那麽v最早能夠回溯到的最早的點,已經到了u後面,無法到達u前面的頂點了,此時u就是割點。

但這裏也出現一個問題:怎麽計算low[u]。假設當前頂點為u,則默認low[u]=dfn[u],即最早只能回溯到自身。有一條邊(u, v),如果v未訪問過,繼續DFS,DFS完之後,low[u]為low[u]和low[v]中的最小值,即low[u]=min(low[u], low[v]);如果v訪問過(且u不是v的父親),就不需要繼續DFS了,一定有dfn[v]<dfn[u],low[u]為low[u]和dfn[v]中的最小值,即low[u]=min(low[u], dfn[v])。(哎,好難啊~)

代碼

DFS

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

bool vis[N];

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

Tarjan算法

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

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

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

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

// 默認u不能回溯到任何前面的點
low[u] = dfn[u];
for (int v: edgesOf(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 = 50;    // 可以將V改成其它值
const int E = 50;    // 可以將E改成其它值
const int NIL = -1;  // 根節點的parent值

vector<int> e[E];    // e[u]為與u相鄰的所有頂點
int dfn[V], low[V], parent[V];
bool vis[V];

// 在其它函數中調用tarjan()前需將parent[u]設為NIL
void tarjan(int u)
{
    static int count = 0;
    int children = 0;

    count++;
    dfn[u] = low[u] = count;
    vis[u] = true;

    for (int v: e[u])
    {
        if (!vis[v])
        {
            children++;
            parent[v] = u;
            tarjan(v);
            low[u] = min(low[u], low[v]);
            if (parent[u] != NIL && low[u] >= dfn[v])
                cout << "AP: " << u << endl;
        }
        else if (v != parent[u])
            low[u] = min(low[u], dfn[v]);
    }
    if (parent[u] == NIL && children >= 2)
        cout << "AP: " << u << endl;
}

Todo

對算法的詳細理解

割點(Tarjan算法)