天天看點

圖的深度與廣度周遊及最小生成樹

上一篇文章講了圖的有關概念以及圖的兩種存儲方式,

​​點選打開連結​​

接下來我們一起學習圖的兩種周遊及最小生成樹的實作。

一、圖的周遊

1、廣度優先周遊(Breadth First Search, BFS)

廣度優先搜尋類似于樹的層序周遊,看一個例子:

圖的深度與廣度周遊及最小生成樹

假設我們都從節點0開始周遊,無向圖周遊順序為0,3,4,1,2,,有向圖周遊順序為0,3,4,1,2。

下面是實作的代碼,需要借助隊列來完成:

// 圖的廣度優先周遊 
  void BFS(const V& v)
  {
    queue<int> q;
    //标記是否已經周遊,周遊過為true
    vector<bool> visited(_v.size(), false);
    size_t index = GetIndexOfV(v);
    q.push(index);
    _BFS(q, visited);

    //避免漏掉與其他頂點無關的點
    for (size_t i = 0; i < _v.size(); i++)
    {
      if (visited[i] == false)
      {
        q.push(i);
        _BFS(q, visited);
      }
    }
    cout << endl;
  }
       void _BFS(queue<int>& q, vector<bool>& visited)
  {
    while (!q.empty())
    {
      size_t index = q.front();
      q.pop();
      if (visited[index] == true)
        continue;
      visited[index] = true;
      cout << _v[index] << " ";

      pNode pCur = _linkEdges[index];
      while (pCur)
      {
        if (visited[pCur->_dst] == false)
          q.push(pCur->_dst);
        pCur = pCur->_pNext;
      }
    }
  }      

2、深度優先周遊

深度優先周遊相當于一條路走到黑的道理,還是上面的例子:

圖的深度與廣度周遊及最小生成樹

假設我們都從節點0開始周遊,無向圖周遊順序為0,3,1,4,2,有向圖周遊順序為0,3,1,2,4。

代碼如下:

// 圖的深度優先周遊 
  void DFS(const V& v)
  {
    size_t index = GetDevOfV(v);
    vector<bool> visited(_v.size(), false);
    _DFS(index, visited);

    for (size_t i = 0; i < _v.size(); i++)
    {
      if (visited[i] == false)
        _DFS(i, visited);
    }
    cout << endl;
  }
        void _DFS(int index, vector<bool>& visited)
  {
    cout << _v[index] << " ";
    visited[index] = true;

    pNode pCur = _linkEdges[index];
    while (pCur)
    {
      if (visited[pCur->_dst] == false)
        _DFS(pCur->_dst, visited);
      pCur = pCur->_pNext;
    }
  }      

二、最小生成樹

連通圖中的每一棵生成樹,都是原圖的一個極大無環子圖,即:從其中删去任何一條邊,生成樹就不在連通;反之,在其中引入任何一條新邊,都會形成一條回路。

若連通圖由n個頂點組成,則其生成樹必含n個頂點和n-1條邊。是以構造最小生成樹的準則有三 條:

(1). 隻能使用圖中的邊來構造最小生成樹

(2). 隻能使用恰好n-1條邊來連接配接圖中的n個頂點

(3). 選用的n-1條邊不能構成回路

構造最小生成樹的方法:Kruskal算法和Prim算法。這兩個算法都采用了逐漸求解的貪心政策。

注:貪心算法:是指在問題求解時,總是做出目前看起來最好的選擇。也就是說貪心算法做出的不是整體最優的的選擇,而是某種意義上的局部最優解。貪心算法不是對所有的問題都能得到整體最優解。

我們了解一下兩種算法。

1、Kruskal算法

 任給一個有n個頂點的連通網絡N={V, E},首先構造一個由這n個頂點組成、不含任何邊的圖G= {V, NU LL },其中每個頂點自成一個連通分量,不斷從E中取出權值最小的一條邊(若有多條任取其一) ,若該邊的兩個頂點來自不同的連通分量,則将此邊加入到G中。如此重複,直到所有頂點在同一個連通分量上為止。

核心:每次疊代時,選出一條具有最小權值,且兩端點不在同一連通分量上的邊,加入生成樹。

舉例說明一下:

圖的深度與廣度周遊及最小生成樹

步驟如下:

圖的深度與廣度周遊及最小生成樹

(1)建立一個圖,隻有圖的頂點,沒有邊,且 我們有5個頂點,是以需要4(n-1)條邊

(2)将原圖中邊的權值,從小到大排序

(3)利用之前寫過的 ​​并查集​​,如果需要連接配接的頂點不在已經連通的集合裡,那麼将該邊加到新圖中;

       否則,繼續看下一條邊。 

可以參考之前文章的并查集。

具體實作:

// 最小生成樹---克魯斯卡爾算法;
  typedef Graph<V, W, IsDirect> Self;
  typedef Node LinkEdge;
  Self Kruskal()
  {
    Self g;  //建立一個圖
    g._v = _v; //
    g._linkEdges.resize(_v.size());
    vector<LinkEdge*> edges;

    for (size_t i = 0; i > _v.size(); i++)
    {
      LinkEdge* pCur = _linkEdges[i];
      while (pCur)
      {
        if (IsDirect || (!IsDirect&&pCur->_src < pCur->_dst))  //儲存邊的權值
          edges.push_back(pCur);
        pCur = pCur->_pNext;
      }
    }
    class Compare
    {
    public:
      bool operator()(const LinkEdge* left, const LinkEdge* right)
      {
        return left->_weight < right->_weight;
      }
    };

    sort(edges.begin(), edges.end(), Compare());

    size_t count = _v.size() - 1;//從前往後取n-1條邊
    UnionFind u(_v.size());
    for (size_t i = 0; i < edges.size(); i++)
    {
      LinkEdge* pCur = edges[i];
      size_t srcRoot = u.FindRoot(pCur->_src);
      size_t dstRoot = u.FindRoot(pCur->_dst);
      if (srcRoot != dstRoot)  //兩個節點不在一個集合,加邊
      {
        g.AddEdge(pCur->_src, pCur->_dst, pCur->_weight);
        if (!IsDirect)        //有向圖
          g.AddEdge(pCur->_dst, pCur->_src, pCur->_weight);

        u.Union(pCur->_src, pCur->_dst);//合并
        count--;

        if (count == 0)
          break;
      }
    }
    if (count > 0)
      cout << "最小生成樹非法" << endl;
    return g;
  }      

2、Prim算法

普裡姆算法

(Prim算法),圖論中的一種算法,可在權重連通圖裡搜尋

​​最小生成樹​​ 。

即由此算法搜尋到的邊子集所構成的樹中,不但包括了連通圖裡的所有頂點,且其所有邊的權值之和亦為最小。該算法于1930年由捷克數學家沃伊捷赫·亞爾尼克(英語:Vojtěch Jarník)發現;并在1957年由美國計算機科學家羅伯特·普裡姆(英語:Robert C. Prim)獨立發現;1959年, ​​艾茲格·迪科斯徹​​ 再次發現了該算法。是以,在某些場合,普裡姆算法又被稱為DJP算法、亞爾尼克算法或普裡姆-亞爾尼克算法。

1).輸入:一個權重連通圖,其中頂點集合為V,邊集合為E;

2).初始化:V

new = {x},其中x為集合V中的任一節點(起始點),E new = {},為空;

3).重複下列操作,直到V

new = V。

   a.在集合E中選取權值最小的邊<u, v>,其中u為集合V

new中的元素,而v不在V new​​集合​​當中,并且v∈V(如果存在有多條滿足前  述條件即具有相同權值的邊,則可任意選取其中之一);

    b.将v加入集合V

new中,将<u, v>邊加入集合E new中;