六講貫通C++圖的應(yīng)用之四 最小生成樹
筆者從基本儲存方法、DFS和BFS、無向圖、最小生成樹、最短路徑以及活動網(wǎng)絡(luò)(AOV、AOE)六個方面詳細介紹C++圖的應(yīng)用。這篇我們該介紹最小生成樹了。
最小生成樹
說人是最難伺候的,真是一點不假。上面剛剛為了“提高可靠性”添加了幾條多余的邊,這會兒又來想辦法怎么能以最小的代價把所有的頂點都連起來??赡苷侨说倪@種精神才使得人類能夠進步吧——看著現(xiàn)在3GHz的CPU真是眼紅啊,我還在受500MHz的煎熬,然后再想想8086……
正如圖的基本元素是頂點和邊,從這兩個方向出發(fā),就能得到兩個算法——Kruskal算法(從邊出發(fā))、Prim算法(從頂點出發(fā))。據(jù)說還有別的方法,恕我參考資料有限,不能詳查。
最小生成樹的儲存
顯然用常用的樹的儲存方法來儲存沒有必要,雖然名曰“樹”,實際上,這里誰是誰的“祖先”、“子孫”并不重要。因此,用如下的MSTedge結(jié)構(gòu)數(shù)組來儲存就可以了。
- template <class dist>
- 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條不產(chǎn)生回路最短的邊。Kruskal算法算是最直接的表達了這個思想——在剩余邊中挑選一條最短的邊,看是否產(chǎn)生回路,是放棄,不是選定然后重復(fù)這個步驟。說起來倒是很簡單,做起來就不那么容易了——判斷是否產(chǎn)生回路需要并查集,在剩余邊中找一條最短的邊需要最小堆(并不需要對所有邊排序,所以堆是最佳選擇)。
Kruskal算法的復(fù)雜度是O(eloge),當e接近N^2時,可以看到這個算法不如O(N^2)的Prim算法,因此,他適合于稀疏圖。而作為稀疏圖,通常用鄰接表來儲存比較好。另外,對于鄰接矩陣儲存的圖,Kruskal算法比Prim算法占不到什么便宜(初始還要掃描N^2條“邊”)。因此,最好把Kruskal算法放在Link類里面。
- template <class name, class dist> 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)); //建立邊的堆- 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;
- }
下面是堆和并查集的實現(xiàn)
- #ifndef Heap_H
- #define Heap_H
- #include
- using namespace std;
- #define minchild(i) (heap[i*2+1]
- template <class T>
- 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)
- swap(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 root1, 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
#p#
Prim算法
如果從頂點入手,就能得到另一種方法。從只含有一個頂點的集合開始,尋找集合外面的頂點到這個集合里的頂點最近的一條邊,然后將這個頂點加入集合,修改因為這個頂點的加入而使得集合外面的頂點到集合里的頂點的最短距離產(chǎn)生變化的分量。因為需要對每個頂點掃描,鄰接矩陣儲存的圖是最合適Prim算法的。
- template <class name, class dist> 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]這樣的是應(yīng)該是0呢還是NoEdge呢?顯然0合理,但是不好用。并且,從有權(quán)圖無權(quán)圖統(tǒng)一的角度來說,是NoEdge更好。因此,在我的有權(quán)圖的鄰接矩陣中,主對角線上的元素是NoEdge,而不是書上的0。
測試程序
儲存和操作分離,沒想到得到了一個有趣的結(jié)果——對于最后的無向圖而言,最小生成樹的算法對外表現(xiàn)不知道是采用了那個算法。
- template <class name, class dist, class mem>
- 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;
- }
最后的測試圖的數(shù)據(jù)取自《數(shù)據(jù)結(jié)構(gòu)算法與應(yīng)用-C++語言描述》(中文譯名)
- #include
- using namespace std;
- #include "Graph.h"
- int main()
- {
- Graph<char, int, AdjMatrix<char, int> > 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;
- }
【編輯推薦】