深度優先生成樹及其應用

在上一篇博客判斷有向圖是否有圈中從遞歸的角度簡單感性的介紹瞭如何修改深度優先搜索來判斷一個有向圖是否有圈。事實上, 它的實質是利用了深度優先生成樹(depth-first spanning tree)的性質。那麼什麼是深度優先生成樹?顧名思義,這顆樹由深度優先搜索而生成的,因爲無向圖與有向圖的深度優先生成樹有差異,下面將分別介紹。html

一. 無向圖的深度優先生成樹

無向圖的深度優先生成樹的生成步驟:node

  1. 深度優先搜索第一個被訪問的頂點爲該樹的根結點。
  2. 對於頂點v,其相鄰的邊w若是未被訪問,則邊(v, w)爲該樹的樹邊,用實線表示;若w已經被訪問,則邊(v, w)爲該樹的回退邊(back edge),用虛線表示(表明這條邊實際上不是樹的一部分)。

下面是一個無向圖和它對應的深度優先生成樹:c++

 

不難發現,該樹的先序遍歷過程就是DFS過程,利用該樹咱們能夠更好的理解DFS。而對無向圖而言,深度優先生成樹一個重要的應用是解決算法

雙連通性問題(該問題在通信網絡,運輸網絡等有重要應用)。固然,咱們首先須要瞭解雙連通性問題的相關概念。網絡

  1. 若是一個連通的無向圖中的任一頂點被刪除後,剩下的圖仍然連通,那麼這樣的無向連通圖就稱做是雙連通的(biconnected)。(上圖的無向圖是雙連通的)
  2. 若是一個圖不是雙連通的,也就是說存在一些頂點,將其刪除後圖將不在連通,咱們把那些頂點稱爲割點或者關節點(articulation point)

下圖是一個不是雙連通的圖,其中頂點C和D爲割點。數據結構

利用深度優先生成樹求連通圖中的全部割點算法以下:ide

  1. 經過先序遍歷深度優先生成樹得到每一個頂點的先序編號(也是深度優先編號),不妨把頂點v的先序編號記爲num(v);
  2. 計算深度優先生成樹上的每個頂點的最小編號,所謂最小編號是取頂點v和w的先序編號的較小者,其中的w是從v點沿着零條或多條樹邊到v的後代x(多是v自己),以及可能沿着任意一條回退邊(x,w)所能達到w的全部頂點,記爲low(v)。由low(v)的定義可知low(v)是:(1). num(v);(2). 全部回退邊(v, w)中的最小num(w);(3). 全部樹邊(v, w)中的最小low(w)三者中的最小值。由(3)可知咱們必須先求出v的全部孩子的最小編號,故須要用後序遍歷計算low(v)。
  3. 求出全部割點:
    1. 第一類割點:根節點是割點當且僅當他有兩個或兩個以上的孩子。由於若是根節點有多個孩子時,刪除根使得其餘的節點分佈在不一樣的子樹上,而每一棵子樹就對應一個連通圖,因此整個圖就不連通了;而但根只有一個孩子時,刪除它仍是隻有一棵子樹。
    2. 第二類割點:對於除根節點之外的節點v,它是割點當且僅當它有某個孩子使得low(w) >= num(v),即以v爲根節點的子樹中的全部節點均沒有指向v的祖先的背向邊,這樣若刪除v,其子樹就和其餘部分分開了。(注意:節點v必定不是葉節點由於刪除葉節點仍是一棵樹,而根節點之全部單獨拿出來是由於任何狀況下若v爲根節點,必定知足low(w) >= num(v),由於num(v)是最小先序編號)。

下面是分別從A與C開始遍歷上圖生成的樹:測試

 

c++實現代碼以下:spa

/* 數據結構:鄰接表存儲圖 程序說明:爲簡單起見,設節點的類型爲整型,設visited[],num[].low[],parent[]爲全局變量, 爲求得先序編號num[],設置全局變量counter並初始化爲1。爲便於單獨處理根節點設置root變量。 */ #include <cstdio> #include <vector> #include <algorithm>

using namespace std; const int MAX_N = 100; vector<int> graph[MAX_N]; vector<int> artPoint; int num[MAX_N], low[MAX_N], parent[MAX_N]; int counter = 1; int root; bool visited[MAX_N]; void Init();           //初始化圖
void FindArt(int v);   //找到第二類割點
void PrintArtPoint();  //打印全部割點(第一類割點在此單獨處理)

int main() { Init(); FindArt(root); PrintArtPoint(); return 0; } void PrintArtPoint() { int rootChild = 0;  //根節點的孩子個數
    for (int i = 0; i < graph[root].size(); i++) //計算根節點的孩子個數
 { if (parent[graph[root][i]] == root) rootChild++; } if (rootChild > 1)            //根節點孩子個數大於1則爲割點
 artPoint.push_back(root); for (int i = 0; i < artPoint.size(); i++) printf("%d\n", artPoint[i]); } void Init() { int a, b; root = 1; while (scanf("%d%d", &a, &b) != EOF) { graph[a].push_back(b); graph[b].push_back(a); visited[a] = false; visited[b] = false; } } void FindArt(int v) { visited[v] = true; low[v] = num[v] = counter++;          //狀況(1)
    for (int i = 0; i < graph[v].size(); i++) { int w = graph[v][i]; if (!visited[w])           //樹邊
 { parent[w] = v; FindArt(w); if (low[w] >= num[v] && v != root) artPoint.push_back(v); low[v] = min(low[v], low[w]);  //狀況(3)
 } else if (parent[v] != w)           //回退邊
 { low[v] = min(low[v], num[w]);  //狀況(2)
 } } }

測試運行結果以下:設計

 二. 有向圖的深度優先生成樹

咱們知道有向圖一樣能夠和無向圖同樣進行深度優先搜索。可是,因爲有向圖的特色:邊的方向性致使即便兩個頂點有邊相連也不必定是可達的,有向圖的深度優先生成樹的邊有了更多的狀況,包括樹邊(tree edges), 回退邊(back edges),向前邊(forward edges), 橫邊(cross edges)。其中後三者是樹實際不存在的邊,通向的是已經被訪問過的點。下面用一張圖來直觀感覺一下這幾種狀況:

事實上,有如下結論(其中num[]保存的是樹節點的先序序列即DFS序列):

一、若num[v] < num[w],即v在w以後被訪問,則(v,w)是樹邊或向前邊;

      此時,若visited[v]= true, visited[w] = false,(v,w)爲 樹邊;

              若visited[v]= true, visited[w] = true,(v,w) 爲 向前邊;好比上圖的第2種狀況,訪問到節點3時,節點1已經被訪問,且num[1]<num[3],故邊(1, 3)是向前邊。

二、若num[v] > num[w],即v在w以後被訪問,故visited[v] = true則visited[w] = true,則(v,w)是回退邊或橫邊;

    當產生樹邊(i,j) 時,同時記下j的父節點:parent[j] = i, 因而對圖中任一條邊(v,w),由結點v沿着樹邊向上(parent中)查找w(可能直到根);

    若找到w,則(v,w)是回退邊,不然是橫邊。好比上圖第一種狀況parent[3] = 1,故邊(3, 1)爲回退邊,而第3種狀況節點3無父節點,故爲橫邊。

到此咱們就知道了以下法則:一個有向圖是無圈圖當且僅當它沒有回退邊。

查找強連通份量(SCC: Strong Connected Components)

有向圖的深度優先生成樹除了能夠用於判斷有向圖是否有邊,還能夠用來查找強連通份量。首先給出相關概念:

強連通圖:一個有向圖中任意兩個頂點是能夠互達的。

強連通份量:對於一個非強連通圖,咱們可獲得頂點的一些子集,使得它們到自身是強連通的。

查找強連通份量的算法:

1. Kosaraju-Sharir算法

  1. 首先對輸入的圖G進行一次DFS:後序遍歷深度優先生成森林,將圖G的頂點標號。而後將圖G全部邊反向,獲得Gr。
  2. 每次在圖Gr中還未訪問的頂點中從編號最大的頂點開始對Gr進行DFS,每進行一次DFS獲得的深度優先生成樹中的全部頂點就是一個強連通份量;如此直到全部點被訪問。

    理解該算法:若是兩個頂點v和w都在一個強連通分支中,則原圖G中就存在v到w和w到v的路徑,因此Gr也存在。  而兩個頂點互達與這兩個頂點在Gr中                        的同一棵深度優先生成樹等價。因此步驟2每次DFS都能獲得一個強連通份量。

    代碼以下:

/* 數據結構;鄰接表 程序說明:1. 每對Gr進行一次DFS,生成一個強連通份量,topSort++, 因此topSort相同的頂點即在同一個強連通份量中。 2. 爲便於獲得最大編號對應的頂點,設置node[],其下標爲後序編號,值爲對應頂點 */ #include <cstdio> #include <vector> #include <cstring> #include <cstdlib>
using namespace std; const int MAX_N = 100; vector<int> G[MAX_N];  //原圖
vector<int> Gr[MAX_N]; //反轉後的圖
vector<int> topSort[MAX_N];    //下標爲所屬強分支的拓撲序
int counter = 0;       //用於編號
int node[MAX_N];       //後序遍歷標號,下標爲編號
bool visited[MAX_N]; int vNum;              //圖的頂點數
void DFS(int v); void RDFS(int v, int k);  //參數k爲v所在的強連通份量的拓撲序
int SCC();                //返回強連通份量的個數
void Init();              //初始化圖G和Gr
int main() { Init(); int sccNum = SCC(); printf("%d\n", sccNum); for (int i = 0; i < sccNum; i++) { int j; printf("{"); for (j = 0; j < topSort[i].size()-1; j++) printf("%d, ", topSort[i][j]); printf("%d}\n", topSort[i][j]); } return 0; } void Init() { scanf("%d", &vNum); int u, v; while (scanf("%d%d", &u, &v) != EOF) { G[u].push_back(v); Gr[v].push_back(u); //反向
 } } void DFS(int v) { visited[v] = true; for (int i = 0; i < G[v].size(); i++) { if (!visited[G[v][i]]) DFS(G[v][i]); } node[counter++] = v;     //後序遍歷
} void RDFS(int v, int k) { visited[v] = true; topSort[k].push_back(v); //將屬於同一強連通份量放一塊兒
    for (int i = 0; i < Gr[v].size(); i++) { if (!visited[Gr[v][i]]) RDFS(Gr[v][i], k); } } int SCC() { memset(visited, false, sizeof(visited)); for (int v = 1; v <= vNum; v++) { if (!visited[v]) DFS(v); } memset(visited, false, sizeof(visited)); int k = 0;           //初始化第一個強連通份量的拓撲序爲1
    for (int i = --counter; i >= 0; i--) //從編號最大開始
 { if (!visited[node[i]]) RDFS(node[i], k++); } return k; }

測試運行結果:

2. Tarjan算法

Tarjan算法和上文所說的雙連通性問題的算法很是類似。它也是經過求出深度優先生成樹的先序編號num[]和low[]。利用的性質是當num[v] == low[v]時,則以v爲根節點的深度優先生成樹中全部的節點爲一個強連通份量,而爲了得到強連通份量,咱們須要用一個棧來記錄。

Tarjan算法的僞碼描述以下:

Tarjan(u) { num[u]=low[u] = counter              //狀況(1)
    Stack.push(u)                        // 將節點u壓入棧中
    for each (u, v) in E                  // 枚舉每一條邊
        if (v is not visted)              // 若是節點v未被訪問過
            Tarjan(v)                    // 繼續向下找
            low[u] = min(low[u], low[v]) //狀況(3)
        else if (v in Stack)             // 若是節點v還在棧內
            Low[u] = min(low[u], num[v]) //狀況(2)
    if (num[u] == low[u])                // 若是節點u是強連通份量的根
 repeat v = Stack.pop                // 將v退棧,爲該強連通份量中一個頂點
 print v until (u== v) }

c++代碼:

/* 數據結構:鄰接表存儲圖 */ #include <cstdio> #include <vector> #include <algorithm> #include <stack> #include <cstring>

using namespace std; const int MAX_N = 100; vector<int> graph[MAX_N]; vector<int> topSort[MAX_N];    //下標爲所屬強分支的拓撲序
stack<int> scc; int num[MAX_N], low[MAX_N]; int counter = 1; int numSCC = 0;          //強連通份量個數
int vNum;                //頂點個數
bool inStack[MAX_N];     //判斷頂點是否在棧中
bool visited[MAX_N]; void Init();          //初始化圖
void Tarjan(int v);   //tarjan算法查找SCC
void PrintSCC();      //打印全部SCC
void SCC(); int main() { Init(); SCC(); PrintSCC(); return 0; } void SCC() { memset(visited, false, sizeof(visited)); for (int i = 1; i <= vNum; i++) { if (!visited[i]) Tarjan(i); } } void PrintSCC() { for (int i = 0; i < numSCC; i++) { int j; printf("{"); for (j = 0; j < topSort[i].size() - 1; j++) printf("%d, ", topSort[i][j]); printf("%d}\n", topSort[i][j]); } } void Init() { int u, v; scanf("%d", &vNum); while (scanf("%d%d", &u, &v) != EOF) { graph[u].push_back(v); } } void Tarjan(int v) { low[v] = num[v] = ++counter;          //狀況(1)
    inStack[v] = true; visited[v] = true; scc.push(v); for (int i = 0; i < graph[v].size(); i++) { int w = graph[v][i]; if (!visited[w]) { Tarjan(w); low[v] = min(low[v], low[w]);  //狀況(3)
 } else if (inStack[w]) { low[v] = min(low[v], num[w]);  //狀況(2)
 } } if (num[v] == low[v]) { int w; do { w = scc.top(); scc.pop(); inStack[w] = false; topSort[numSCC].push_back(w); } while (w != v); numSCC++; } }
View Code

參考資料:《數據結構與算法分析-C語言描述》

              《挑戰程序設計競賽》

               博客:https://www.byvoid.com/blog/scc-tarjan/

相關文章
相關標籤/搜索