資料結構學習(C++)——圖【3】(無向圖)(下) (轉)

amyz發表於2007-11-25
資料結構學習(C++)——圖【3】(無向圖)(下) (轉)[@more@]

最小生成樹

說人是最難伺候的,真是一點不假。上面剛剛為了“提高可靠性”新增了幾條多餘的邊,這會兒又來想辦法怎麼能以最小的代價把所有的頂點都連起來。可能正是人的這種精神才使得人類能夠進步吧——看著現在3GHz的真是眼紅啊,我還在受500MHz的煎熬,然後再想想8086……

正如圖的基本元素是頂點和邊,從這兩個方向出發,就能得到兩個演算法——Kruskal演算法(從邊出發)、Prim演算法(從頂點出發)。據說還有別的方法,恕我參考資料有限,不能詳查。

最小生成樹的儲存

顯然用常用的樹的儲存方法來儲存沒有必要,雖然名曰“樹”,實際上,這裡誰是誰的“祖先”、“子孫”並不重要。因此,用如下的MSTedge結構陣列來儲存就可以了。

template

class MSTedge

{

public:

  MSTedge() {}

  MSTedge(int v1, int v2, dist cost) : v1(v1), v2(v2), cost(cost) {}

  int v1, v2;

  dist cost;

  bool operator > (const MSTedge& v2) { return (cost > v2.cost); }

  bool operator < (const MSTedge& v2) { return (cost < v2.cost); }

  bool operator == (const MSTedge& v2) { return (cost == v2.cost); }

};

Kruskal演算法

最小生成樹直白的講就是,挑選N-1條不產生迴路最短的邊。Kruskal演算法算是最直接的表達了這個思想——在剩餘邊中挑選一條最短的邊,看是否產生迴路,是放棄,不是選定然後重複這個步驟。說起來倒是很簡單,做起來就不那麼容易了——判斷是否產生迴路需要並查集,在剩餘邊中找一條最短的邊需要最小堆(並不需要對所有邊排序,所以堆是最佳選擇)。

Kruskal演算法的複雜度是O(eloge),當e接近N^2時,可以看到這個演算法不如O(N^2)的Prim演算法,因此,他適合於稀疏圖。而作為稀疏圖,通常用鄰接表來儲存比較好。另外,對於鄰接矩陣儲存的圖,Kruskal演算法比Prim演算法佔不到什麼便宜(初始還要掃描N^2條“邊”)。因此,最好把Kruskal演算法放在Link類裡面。

template int Link::MinSpanTree(MSTedge* a)

{

  MinHeap > E; int i, j, k, l = 0;

  MFSets V(vNum); list::iterator iter;

  for (i = 0; i < vNum; i++)

  for (iter = vertices[i].e->begin(); iter != vertices[i].e->end(); iter++)

    E.insert(MSTedge(i, iter->vID, iter->cost));//建立邊的堆:namespace prefix = o ns = "urn:schemas--com::office" />

  for (i = 0; i < eNum && l < vNum; i++)//Kruskal Start

  {

  j = V.find(E.top().v1); k = V.find(E.top().v2);

  if (j != k) { V.merge(j, k); a[l] = E.top(); l++; }

    E.pop();

  }

  return l;

}

下面是堆和並查集的實現

#ifndef Heap_H

#define Heap_H

#include

using namespace std;

#define minchild(i) (heap[i*2+1]

template

class MinHeap

{

public:

  void insert(const T& x) { heap.push_back(x); FilterUp(heap.size()-1); }

  const T& top() { return heap[0]; }

  void pop() { heap[0] = heap.back(); heap.pop_back(); FilterDown(0); }

private:

  void FilterUp(int i)

  {

  for (int j = (i - 1) / 2; j >= 0 && heap[j] > heap[i]; i = j, j = (i - 1) / 2)

    s(heap[i], heap[j]);

  }

  void FilterDown(int i)

  {

  for (int j = minchild(i); j < heap.size() && heap[j] < heap[i]; i = j, j = minchild(i))

    swap(heap[i], heap[j]);

  }

  vector heap;

};

#endif

 

#ifndef MFSets_H

#define MFSets_H

class MFSets

{

public:

  MFSets(int maxsize) : size(maxsize)

  {

  parent = new int[size + 1];

  for (int i = 0; i <= size; i++) parent[i] = -1;

  }

  ~MFSets() { delete []parent; }

  void merge(int 1, int root2)//root1!=root2

  {

    parent[root2] = root1;

  }

  int find(int n)

  {

  if (parent[n] < 0) return n;

  return find(parent[n]);

  }

private:

  int size;

  int* parent;

};

#endif

Prim演算法

如果從頂點入手,就能得到另一種方法。從只含有一個頂點的集合開始,尋找集合外面的頂點到這個集合裡的頂點最近的一條邊,然後將這個頂點加入集合,修改因為這個頂點的加入而使得集合外面的頂點到集合裡的頂點的最短距離產生變化的分量。因為需要對每個頂點掃描,鄰接矩陣儲存的圖是最合適Prim演算法的。

template int AdjMatrix::MinSpanTree(MSTedge* a)

{

  dist* lowC = new dist[vNum]; int* nearV = new int[vNum];

  int i, j, k;

  for (i = 0; i < vNum; i++) { lowC[i] = edge[0][i]; nearV[i] = 0; } nearV[0] = -1;

  for (k = 0; k < vNum-1; k++)//Prim Start

  {

  for (i = 1, j = 0; i < vNum; i++)

  if (nearV[i] != -1 && lowC[i] < lowC[j]) j = i;//find low cost

  a[k] = MSTedge(nearV[j], j, lowC[j]); nearV[j] = -1; //insert MST

  if (a[k].cost == NoEdge) return k - 1;//no edge then return

  for (i = 1; i < vNum; i++)//modify low cost

  if (nearV[i] != -1 && edge[i][j] < lowC[i]) { lowC[i] = edge[i][j]; nearV[i] = j; }

  }

  return k;

}

【附註】這裡需要說明一下,對於edge[I][I]這樣的是應該是0呢還是NoEdge呢?顯然0合理,但是不好用。並且,從有權圖無權圖統一的角度來說,是NoEdge更好。因此,在我的有權圖的鄰接矩陣中,主對角線上的元素是NoEdge,而不是書上的0。

測試

儲存和操作分離,沒想到得到了一個有趣的結果——對於最後的無向圖而言,最小生成樹的演算法對外表現不知道是採用了那個演算法。

template

bool Graph::MinSpanTree()

{

  MSTedge* a = new MSTedge[vNum() - 1];

  int n = data.MinSpanTree(a); dist sum = dist();

  if (n < vNum() - 1) return false;//不夠N-1條邊,不是生成樹

  for (int i = 0; i < n; i++)

  {

  cout << '(' << getV(a[i].v1) << ',' << getV(a[i].v2) << ')' << a[i].cost << ' ';

  sum += a[i].cost;

  }

  cout << endl << "MinCost: " << sum << endl;

  delete []a;

  return true;

}

最後的測試圖的資料取自殷版(C++)——不知是這組資料好是怎麼的,殷版居然原封不動的照抄了《資料結構演算法與應用-C++語言描述》(中文譯名)

#include

using namespace std;

#include "Graph.h"

int main()

{

  Graph > a(100);//改為Link儲存為Kruskal演算法

  a.insertV('A'); a.insertV('B');

  a.insertV('C');  a.insertV('D');

  a.insertV('E'); a.insertV('F');

  a.insertV('G');

  a.insertE('A', 'B', 28); a.insertE('A', 'F', 10);

  a.insertE('B', 'C', 16); a.insertE('C', 'D', 12);

  a.insertE('D', 'E', 22); a.insertE('B', 'G', 14);

  a.insertE('E', 'F', 25); a.insertE('D', 'G', 18);

  a.insertE('E', 'G', 24);

  a.MinSpanTree();

  return 0;

}


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-984553/,如需轉載,請註明出處,否則將追究法律責任。

相關文章