笔者从基本储存方法、DFS和BFS、无向图、最小生成树、最短路径以及活动网络(AOV、AOE)六个方面详细介绍C++图的应用。这篇我们该介绍最小生成树了。
最小生成树
说人是最难伺候的,真是一点不假。上面刚刚为了“提高可靠性”添加了几条多余的边,这会儿又来想办法怎么能以最小的代价把所有的顶点都连起来。可能正是人的这种精神才使得人类能够进步吧——看着现在3GHz的CPU真是眼红啊,我还在受500MHz的煎熬,然后再想想8086……
正如图的基本元素是顶点和边,从这两个方向出发,就能得到两个算法——Kruskal算法(从边出发)、Prim算法(从顶点出发)。据说还有别的方法,恕我参考资料有限,不能详查。
最小生成树的储存
显然用常用的树的储存方法来储存没有必要,虽然名曰“树”,实际上,这里谁是谁的“祖先”、“子孙”并不重要。因此,用如下的MSTedge结构数组来储存就可以了。
- 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条不产生回路最短的边。Kruskal算法算是最直接的表达了这个思想——在剩余边中挑选一条最短的边,看是否产生回路,是放弃,不是选定然后重复这个步骤。说起来倒是很简单,做起来就不那么容易了——判断是否产生回路需要并查集,在剩余边中找一条最短的边需要最小堆(并不需要对所有边排序,所以堆是最佳选择)。
Kruskal算法的复杂度是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;
- }
下面是堆和并查集的实现
- #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算法
如果从顶点入手,就能得到另一种方法。从只含有一个顶点的集合开始,寻找集合外面的顶点到这个集合里的顶点最近的一条边,然后将这个顶点加入集合,修改因为这个顶点的加入而使得集合外面的顶点到集合里的顶点的最短距离产生变化的分量。因为需要对每个顶点扫描,邻接矩阵储存的图是最合适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]这样的是应该是0呢还是NoEdge呢?显然0合理,但是不好用。并且,从有权图无权图统一的角度来说,是NoEdge更好。因此,在我的有权图的邻接矩阵中,主对角线上的元素是NoEdge,而不是书上的0。
测试程序
储存和操作分离,没想到得到了一个有趣的结果——对于最后的无向图而言,最小生成树的算法对外表现不知道是采用了那个算法。
- 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;
- }
最后的测试图的数据取自《数据结构算法与应用-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;
- }
【编辑推荐】