最小生成树Prim算法和Kruskal算法
最小生成树(Minimum Cost Spanning Tree)
首先,最小生成树是一副连通加权无向图中一棵权值最小的生成树。
主要可以使用Prim和Kruskal算法实现,对于稀疏图来说,用Kruskal写最小生成树效率更好,加上并查集,可对其进行优化。
Kruskal算法(并查集实现)
在使用Kruskal实现最小生成树之前,先来看下并查集需要注意的两点:
1. 针对树可能会退化为链表的解决方案是,每次合并树时,总是将矮的树挂到高的树下,这种方式称为按秩合并。
2. 为了得到的树将更加扁平,加速以后直接或者间接引用节点的速度,Find时改变每一个节点的引用到根节点,这叫路径压缩。
并查集的初始化:
并查集的路径压缩:
并查集的按秩合并
Kruskal算法的步骤包括:
1. 对所有权值进行从小到大排序(这里对边排序时还需要记录边的索引,这样以边的权值排完序后只改变了权值的索引位置)
2. 然后每次选取最小的权值,如果和已有点集构成环则跳过,否则加到该点集中。最终有所有的点集构成的树就是最佳的。
代码实现如下所示:
1 #include <iostream> 2 #include <vector> 3 #include <algorithm> 4 using namespace std; 5 6 //并查集实现最小生成树 7 vector<int> u, v, weights, w_r, father; 8 int mycmp(int i, int j) 9 { 10 return weights[i] < weights[j]; 11 } 12 int find(int x) 13 { 14 return father[x] == x ? x : father[x] = find(father[x]); 15 } 16 void kruskal_test() 17 { 18 int n; 19 cin >> n; 20 vector<vector<int> > A(n, vector<int>(n)); 21 for(int i = 0; i < n; ++i) { 22 for (int j = 0; j < n; ++j) { 23 cin >> A[i][j]; 24 } 25 } 26 27 int edges = 0; 28 // 共计n*(n - 1)/2条边 29 for (int i = 0; i < n - 1; ++i) { 30 for (int j = i + 1; j < n; ++j) { 31 u.push_back(i); 32 v.push_back(j); 33 weights.push_back(A[i][j]); 34 w_r.push_back(edges++); 35 } 36 } 37 for (int i = 0; i < n; ++i) { 38 father.push_back(i); // 记录n个节点的根节点,初始化为各自本身 39 } 40 41 sort(w_r.begin(), w_r.end(), mycmp); //以weight的大小来对索引值进行排序 42 43 int min_tree = 0, cnt = 0; 44 for (int i = 0; i < edges; ++i) { 45 int e = w_r[i]; //e代表排序后的权值的索引 46 int x = find(u[e]), y = find(v[e]); 47 //x不等于y表示u[e]和v[e]两个节点没有公共根节点,可以合并 48 if (x != y) { 49 min_tree += weights[e]; 50 father[x] = y; 51 ++cnt; 52 } 53 } 54 if (cnt < n - 1) min_tree = 0; 55 cout << min_tree << endl; 56 } 57 58 int main(void) 59 { 60 61 kruskal_test(); 62 63 return 0; 64 }
这里只用到了路径压缩,在合并的时候直接将后一个节点的根节点指到前一个节点的。
Prim算法(使用visited数组实现)
Prim算法求最小生成树的时候和边数无关,和顶点树有关,所以适合求解稠密网的最小生成树。
Prim算法的步骤包括:
1. 将一个图分为两部分,一部分归为点集U,一部分归为点集V,U的初始集合为{V1},V的初始集合为{ALL-V1}。
2. 针对U开始找U中各节点的所有关联的边的权值最小的那个,然后将关联的节点Vi加入到U中,并且从V中删除(注意不能形成环)。
3. 递归执行步骤2,直到V中的集合为空。
4. U中所有节点构成的树就是最小生成树。
代码实现如下所示:
1 #include <iostream> 2 #include <vector> 3 using namespace std; 4 5 //Prim算法实现 6 void prim_test() 7 { 8 int n; 9 cin >> n; 10 vector<vector<int> > A(n, vector<int>(n)); 11 for(int i = 0; i < n ; ++i) { 12 for(int j = 0; j < n; ++j) { 13 cin >> A[i][j]; 14 } 15 } 16 17 int pos, minimum; 18 int min_tree = 0; 19 //lowcost数组记录每2个点间最小权值,visited数组标记某点是否已访问 20 vector<int> visited, lowcost; 21 for (int i = 0; i < n; ++i) { 22 visited.push_back(0); //初始化为0,表示都没加入 23 } 24 visited[0] = 1; //最小生成树从第一个顶点开始 25 for (int i = 0; i < n; ++i) { 26 lowcost.push_back(A[0][i]); //权值初始化为0 27 } 28 29 for (int i = 0; i < n; ++i) { //枚举n个顶点 30 minimum = max_int; 31 for (int j = 0; j < n; ++j) { //找到最小权边对应顶点 32 if(!visited[j] && minimum > lowcost[j]) { 33 minimum = lowcost[j]; 34 pos = j; 35 } 36 } 37 if (minimum == max_int) //如果min = max_int表示已经不再有点可以加入最小生成树中 38 break; 39 min_tree += minimum; 40 visited[pos] = 1; //加入最小生成树中 41 for (int j = 0; j < n; ++j) { 42 if(!visited[j] && lowcost[j] > A[pos][j]) lowcost[j] = A[pos][j]; //更新可更新边的权值 43 } 44 } 45 46 cout << min_tree << endl; 47 } 48 49 int main(void) 50 { 51 prim_test(); 52 53 return 0; 54 }
注意:Prim算法实质就是每在最小生成树集合中加入一个点就需要把这个点与集合外的点比较,不断的寻找两个集合之间最小的边。
Kruskal VS Prim
方法上:Kruskal在所有边中不断寻找最小的边,Prim在U和V两个集合之间寻找权值最小的连接,共同点是构造过程都不能形成环。
时间上:Prim适合稠密图,复杂度为O(n * n),因此通常使用邻接矩阵储存,复杂度为O(e * loge),而Kruskal多用邻接表,稠密图 Prim > Kruskal,稀疏图 Kruskal > Prim。
空间上: Prim适合点少边多,Kruskal适合点多边少。