1. 程式人生 > >(資料結構)第六章 圖

(資料結構)第六章 圖

直觀顯示圖結構的方法:用小圓圈或小方塊代表頂點,用連線於其間的直線段或者曲線弧表示對應的邊。

圖:無向圖、有向圖及混合圖

深度優先搜尋實質功能:先將當前節點v標記為DISCOVERED(已發現)狀態,再逐一核對其各鄰居u的狀態並做相應處理。待其所有鄰居均以處理完畢之後,將頂點v置為VISITED(訪問完畢)狀態,便可回溯。

深度優先搜尋

從圖的某個頂點出發,訪問圖中的所有頂點且使每個頂點僅被訪問一次。這一過程叫做圖的遍歷

    深度優先搜尋的思想:

      ①訪問頂點v;
      ②依次從v的未被訪問的鄰接點出發,對圖進行深度優先遍歷;直至圖中和v有路徑相通的頂點都被訪問;
      ③若此時圖中尚有頂點未被訪問,則從一個未被訪問的頂點出發,重新進行深度優先遍歷,直到圖中所有頂點均被訪問過為止。

    比如:

    

    在這裡為了區分已經訪問過的節點和沒有訪問過的節點,我們引入一個一維陣列bool visited[MaxVnum]用來表示與下標對應的頂點是否被訪問過,

流程:
☐ 首先輸出 V1,標記V1的flag=true;
☐ 獲得V1的鄰接邊 [V2 V3],取出V2,標記V2的flag=true;
☐ 獲得V2的鄰接邊[V1 V4 V5],過濾掉已經flag的,取出V4,標記V4的flag=true;
☐ 獲得V4的鄰接邊[V2 V8],過濾掉已經flag的,取出V8,標記V8的flag=true;
☐ 獲得V8的鄰接邊[V4 V5],過濾掉已經flag的,取出V5,標記V5的flag=true;
☐ 此時發現V5的所有鄰接邊都已經被flag了,所以需要回溯。(左邊黑色虛線,回溯到V1,回溯就是下層遞迴結束往回返)
☐ 


☐ 回溯到V1,在前面取出的是V2,現在取出V3,標記V3的flag=true;
☐ 獲得V3的鄰接邊[V1 V6 V7],過濾掉已經flag的,取出V6,標記V6的flag=true;
☐ 獲得V6的鄰接邊[V3 V7],過濾掉已經flag的,取出V7,標記V7的flag=true;
☐ 此時發現V7的所有鄰接邊都已經被flag了,所以需要回溯。(右邊黑色虛線,回溯到V1,回溯就是下層遞迴結束往回返)

廣度優先搜尋

所謂廣度,就是一層一層的,向下遍歷,層層堵截,還是這幅圖,我們如果要是廣度優先遍歷的話,我們的結果是V1 V2 V3 V4 V5 V6 V7 V8。

      

      廣度優先搜尋的思想:

         ① 訪問頂點vi ;

         ② 訪問vi 的所有未被訪問的鄰接點w1 ,w2 , …wk ;

         ③ 依次從這些鄰接點(在步驟②中訪問的頂點)出發,訪問它們的所有未被訪問的鄰接點; 依此類推,直到圖中所有訪問過的頂點的鄰接點都被訪問;

   說明:

      為實現③,需要儲存在步驟②中訪問的頂點,而且訪問這些頂點的鄰接點的順序為:先儲存的頂點,其鄰接點先被訪問。 這裡我們就想到了用標準模板庫中的queue佇列來實現這種先進現出的服務。

      老規矩我們還是走一邊流程:

   說明: 

     ☐將V1加入佇列,取出V1,並標記為true(即已經訪問),將其鄰接點加進入佇列,則 <—[V2 V3] 

     ☐取出V2,並標記為true(即已經訪問),將其未訪問過的鄰接點加進入佇列,則 <—[V3 V4 V5]

☐取出V3,並標記為true(即已經訪問),將其未訪問過的鄰接點加進入佇列,則 <—[V4 V5 V6 V7]

☐取出V4,並標記為true(即已經訪問),將其未訪問過的鄰接點加進入佇列,則 <—[V5 V6 V7 V8]

☐取出V5,並標記為true(即已經訪問),因為其鄰接點已經加入佇列,則 <—[V6 V7 V8]

☐取出V6,並標記為true(即已經訪問),將其未訪問過的鄰接點加進入佇列,則 <—[V7 V8]

☐取出V7,並標記為true(即已經訪問),將其未訪問過的鄰接點加進入佇列,則 <—[V8]

☐取出V8,並標記為true(即已經訪問),將其未訪問過的鄰接點加進入佇列,則 <—[]

 

拓撲排序

有向無環圖:頂點A和B互換之後依然是一個拓撲排序,所以同一有向圖的拓撲排序未必唯一。

不含環路的有向圖------有向無環圖一定存在拓撲結構嗎?答案是肯定的。

有向無環圖的拓撲結構一定存在。因為有向無環圖對應於偏序關係,而拓撲排序則對應於全序關係。

DFS搜尋善於檢測環路的特性,恰好可以用來判別輸入是否為有向無環圖。

在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的所有頂點的線性序列。且該序列必須滿足下面兩個條件:

第一,每個頂點出現且只出現一次。
第二,若存在一條從頂點 A 到頂點 B 的路徑,那麼在序列中頂點 A 出現在頂點 B 的前面。

有向無環圖(DAG)才有拓撲排序,非DAG圖沒有拓撲排序一說。

例如,下面這個圖:


 
它是一個 DAG 圖,那麼如何寫出它的拓撲排序呢?這裡說一種比較常用的方法:

第一,從 DAG 圖中選擇一個 沒有前驅(即入度為0)的頂點並輸出。
第二,從圖中刪除該頂點和所有以它為起點的有向邊。
第三,重複 1 和 2 直到當前的 DAG 圖為空或當前圖中不存在無前驅的頂點為止。後一種情況說明有向圖中必然存在環。

 
於是,得到拓撲排序後的結果是 { 1, 2, 4, 3, 5 }。

通常,一個有向無環圖可以有一個或多個拓撲排序序列。

二、拓撲排序的應用

拓撲排序通常用來“排序”具有依賴關係的任務。

比如,如果用一個DAG圖來表示一個工程,其中每個頂點表示工程中的一個任務,用有向邊<A,B>表示在做任務 B 之前必須先完成任務 A。故在這個工程中,任意兩個任務要麼具有確定的先後關係,要麼是沒有關係,絕對不存在互相矛盾的關係(即環路)。

三、拓撲排序的實現

根據上面講的方法,我們關鍵是要維護一個入度為0的頂點的集合。

圖的儲存方式有兩種:鄰接矩陣和鄰接表。這裡我們採用鄰接表來儲存圖,C++程式碼如下:

#include<iostream>
#include <list>
#include <queue>
using namespace std;

/************************類宣告************************/
class Graph
{
    int V;             // 頂點個數
    list<int> *adj;    // 鄰接表
    queue<int> q;      // 維護一個入度為0的頂點的集合
    int* indegree;     // 記錄每個頂點的入度
public:
    Graph(int V);                   // 建構函式
    ~Graph();                       // 解構函式
    void addEdge(int v, int w);     // 新增邊
    bool topological_sort();        // 拓撲排序
};

/************************類定義************************/
Graph::Graph(int V)
{
    this->V = V;
    adj = new list<int>[V];

    indegree = new int[V];  // 入度全部初始化為0
    for(int i=0; i<V; ++i)
        indegree[i] = 0;
}

Graph::~Graph()
{
    delete [] adj;
    delete [] indegree;
}

void Graph::addEdge(int v, int w)
{
    adj[v].push_back(w); 
    ++indegree[w];
}

bool Graph::topological_sort()
{
    for(int i=0; i<V; ++i)
        if(indegree[i] == 0)
            q.push(i);         // 將所有入度為0的頂點入隊

    int count = 0;             // 計數,記錄當前已經輸出的頂點數 
    while(!q.empty())
    {
        int v = q.front();      // 從佇列中取出一個頂點
        q.pop();

        cout << v << " ";      // 輸出該頂點
        ++count;
        // 將所有v指向的頂點的入度減1,並將入度減為0的頂點入棧
        list<int>::iterator beg = adj[v].begin();
        for( ; beg!=adj[v].end(); ++beg)
            if(!(--indegree[*beg]))
                q.push(*beg);   // 若入度為0,則入棧
    }

    if(count < V)
        return false;           // 沒有輸出全部頂點,有向圖中有迴路
    else
        return true;            // 拓撲排序成功
}

測試如下DAG圖

int main()
{
    Graph g(6);   // 建立圖
    g.addEdge(5, 2);
    g.addEdge(5, 0);
    g.addEdge(4, 0);
    g.addEdge(4, 1);
    g.addEdge(2, 3);
    g.addEdge(3, 1);

    g.topological_sort();
    return 0;
}

 輸出結果是 4, 5, 2, 0, 3, 1。這是該圖的拓撲排序序列之一。

每次在入度為0的集合中取頂點,並沒有特殊的取出規則,隨機取出也行,這裡使用的queue。取頂點的順序不同會得到不同的拓撲排序序列,當然前提是該圖存在多個拓撲排序序列。

由於輸出每個頂點的同時還要刪除以它為起點的邊,故上述拓撲排序的時間複雜度為O(V+E)。

雙連通域分解

可行演算法:DFS樹中的葉節點,絕不可能是原圖中的關節點。因為此類頂點的刪除既不影響DFS的連通性,也不影響原圖的連通性。DFS樹的根節點若至少擁有兩個分支,則必是一個關節點。

 

參考文獻;《Topological Sorting