一、最小生成树的概念
生成树:所有顶点均由边连接在一起,但不存在回路的图叫该图的生成树。
一个图可以有许多棵不同的生成树。
所有生成树具有以下共同特点:
生成树的顶点个数与图的顶点个数相同
生成树是图的极小连通子图
最小生成树:生成树的每条边上的权值之和最小。
构成最小生成树可以有多种算法。其中多数算法利用了最小生成树的下列一种简称为MST的性质:假设N=(V,{E})是一个连通图,U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值(代价)的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树。
普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法是两个利用MST性质构造最小生成树的算法。
二、Prim算法
假设N=(V,{E})是连通网,TE是N上最小生成树中边的集合。
①:算法从U={u0}(u0∈V),TE={}开始。
②:在所有u∈U,v∈V-U的边(u,v)∈E中找一条代价最小的边(u0,v0)
③:(u0,v0)并入集合TE,同时v0并入U
④:重复②和③,直到U=V为止。此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树。
为实现这个算法需要附设一个辅助数组closedge,以记录从U到V-U具有最小代价的边,对每个顶点ui∈V-U,在辅助数组中存在一个相应分量closedge[i-1],它包括两个域,其中lowcost存储该边上的权。显然,
closedge[i-1].lowcost = Min{cost(u,vi) | u∈U}
vex域存储该边依附在U中的顶点。上图为按Prim算法构造网的一棵最小生成树,在构造过程中辅助数组中各分量值的变化如下:
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | #include <iostream> #include <fstream> using namespace std; ifstream fin("prim.txt"); #define MAX_VERTEX_NUM 20 #define ERROR -1 #define INFINITY 0x7fff //图的邻接矩阵存储结构 typedef struct { char *vexs; int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; int vexnum, arcnum; }Graph; //记录从顶点集U到V-U的代价最小的边的辅助数组定义: typedef struct { char adjvex; int lowcost; }closedge; //图G中查找顶点c的位置 int LocateVex(Graph G, char c) { for(int i = 0; i < G.vexnum; ++i) { if(G.vexs[i] == c) return i; } return ERROR; } int minimum(closedge cs[MAX_VERTEX_NUM]); //创建无向网 void CreateUDN(Graph &G){ //采用数组(邻接矩阵)表示法,构造无向网G fin >> G.vexnum >> G.arcnum; G.vexs = (char *) malloc((G.vexnum+1) * sizeof(char)); //需要开辟多一个空间存储'/0' //构造顶点向量 for(int i = 0; i < G.vexnum; i++) fin >> G.vexs[i]; G.vexs[G.vexnum] = '\0'; //初始化邻接矩阵 for(i = 0; i < G.vexnum; ++i) for( int j = 0; j < G.vexnum; j++) G.arcs[i][j] = INFINITY; char a, b; int s1, s2, w; for(i = 0; i < G.arcnum; ++i) { fin >> a >> b >> w; //输入依附于弧的权值 s1 = LocateVex(G,a); //找到a和b在顶点向量中的位置 s2 = LocateVex(G,b); G.arcs[s1][s2] = G.arcs[s2][s1] = w; } } class myerr{}; void MiniSpanTree_PRIN(Graph G, char u) { //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边 closedge cl[MAX_VERTEX_NUM]; int k = LocateVex(G,u); //返回顶点u在图G中的位置 for(int j = 0; j < G.vexnum; ++j) { //辅助数组初始化 if(j != k) { cl[j].adjvex = u; cl[j].lowcost = G.arcs[k][j]; } } cl[j].adjvex = '\0'; cl[j].lowcost = INFINITY; cl[k].adjvex = u; //初始,U = {u} cl[k].lowcost = 0; for(int i = 1; i < G.vexnum; ++i) { //此处更改!!!! 只需运行G.vexnum-1次即可 k = minimum(cl); //求出T的下一个结点:第k顶点 if (k==-1) throw myerr(); //如果明明没搜索完,minimum函数却返回-1,那么肯定出问题了,抛个异常玩玩 cout << cl[k].adjvex << "连接" << G.vexs[k] << endl; //输出生成树的边 cl[k].lowcost = 0; //第k顶点并入U集 for(j = 0; j < G.vexnum; ++j) if(G.arcs[k][j] < cl[j].lowcost) { //新顶点并入U后重新选择最小边,将所有变化的值都更新 cl[j].adjvex = G.vexs[k]; cl[j].lowcost = G.arcs[k][j]; } } } //MiniSpanTree /* 算法简要: 1、找到开始的结点 2、对辅助数组初始化 3、更新变化每一步,使得每次都选择最小边 4、调整结束 */ int minimum(closedge cs[MAX_VERTEX_NUM]) { int i = 0, j = -1, min; min = INFINITY; while(cs[i].adjvex) { if(cs[i].lowcost < min && cs[i].lowcost != 0) { //确保已经选择过的顶点不会再被选择 min = cs[i].lowcost; j = i; } i++; } return j; } //主函数 void main(){ Graph G; CreateUDN(G); cout << "result:" << endl; MiniSpanTree_PRIN(G,'A'); cout << endl; } |
prim.txt中的输入信息为:
6 10
A
B
C
D
E
F
A B 6
A C 1
A D 5
B C 5
B E 3
C D 5
C F 4
C E 6
D F 2
E F 6
运行结果:
三、Kruskal算法
Prim算法时间复杂度为O(n平方),与网中的边数无关,因此适用于求边稠密的网的最小生成树。
而克鲁斯卡尔算法恰恰相反,它的时间复杂度为O(eloge)(e为网中边的数目),因此它相对于普里姆算法而言,适合于求边稀疏的网的最小生成树。
假设连通网N=(V,{E}),则令最小生成树:
①:初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。
②:在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。
③:以此类推,直到T中所有顶点都在同一连通分量上为止。
下图为依照克鲁斯卡尔算法构造一棵最小生成树的过程:
代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | #include <stdio.h> #include <stdlib.h> #define MAX 100 /* 定义边(x,y),权为w */ typedef struct { int x, y; int w; }edge; edge e[MAX]; /* rank[x]表示x的秩 */ int rank[MAX]; /* father[x]表示x的父节点 */ int father[MAX]; int sum; /* 比较函数,按权值(相同则按x坐标)非降序排序 */ int cmp(const void *a, const void *b) { if ((*(edge *)a).w == (*(edge *)b).w) { return (*(edge *)a).x - (*(edge *)b).x; } return (*(edge *)a).w - (*(edge *)b).w; } /* 初始化集合 */ void Make_Set(int x) { father[x] = x; rank[x] = 0; } /* 查找x元素所在的集合,回溯时压缩路径 */ int Find_Set(int x) { if (x != father[x]) { father[x] = Find_Set(father[x]); } return father[x]; } /* 合并x,y所在的集合 */ void Union(int x, int y, int w) { if (x == y) return; /* 将秩较小的树连接到秩较大的树后 */ if (rank[x] > rank[y]) { father[y] = x; } else { if (rank[x] == rank[y]) { rank[y]++; } father[x] = y; } sum += w; } /* 主函数 */ int main() { int i, n; int x, y; char chx, chy; /* 读取边的数目 */ scanf("%d", &n); getchar(); /* 读取边信息并初始化集合 */ for (i = 0; i < n; i++) { scanf("%c %c %d", &chx, &chy, &e[i].w); getchar(); e[i].x = chx - 'A'; e[i].y = chy - 'A'; Make_Set(i); } /* 将边排序 */ qsort(e, n, sizeof(edge), cmp); sum = 0; for (i = 0; i < n; i++) { x = Find_Set(e[i].x); y = Find_Set(e[i].y); if (x != y) { printf("%c - %c : %d\n", e[i].x + 'A', e[i].y + 'A', e[i].w); Union(x, y, e[i].w); } } printf("Total:%d\n", sum); //system("pause"); return 0; } |
运行结果: