最小生成树
给定一个无向图, 是子图中任意两个顶点相互连通, 而且是一个树, 那么这是生成树(Spanning Tree); 倘若边上有权值, 那么使得边权和最小的生成树就是最小生成树(MST)!
生活最小生成树的问题十分常见, 比如说要在几个城市之间修建铁路, 问如何修建铁路, 可以使得城市之间相互连通同时成本最小? 这就是一个MST 问题!
最小生成树有着两种算法, 一个是 Kruskal 算法 以及 Prim 算法!
最小生成树的 Prim 算法(点(集)出发) :是从某一个仅仅只有一个定点的树出发, 不断地添加最短边的算法,进而不断地扩大树的规模, 最终将所有的点都扩充进去。
那么在此我们使用反证法来证明一下
首先我们在算法运行过程中, 得到了一个半成品 T , 他是包含各 节点 X , 剩下的节点是 V \ X; 同时, 我们假设这两点点的集合当中, 有一个最小的连接边 是 e; 假设存在一个最小生成树, 包括了我们如今算法运行的半成品T, 下面有着两种情况,
1. e 也是在这个最小生成树中, 那么算法成立;
2. 那么 e 不在这个最小生成树当中, 我们不妨 把 e给添加进去, 因为这是一个最小生成树,所以他会形成一个回路, 那么由之前我们对 e 这条边的定义来看, 可以把那个回路边去掉, 添加上这个新的边, 最小生成树仍然成立;
那么我们可以根据这个从一个节点, 不断地进行向外扩张, 最终得到了最小生成树;(这个类似于数学归纳法!!)
算法中我们需要先确定一个起点, 然后再不断地进行扩张哦, 还需要不断地进行其他点到这个半成品树距离的更新。
这个数组分为了 mincost 数组 和 cost 数组, 一个是进行更新距离, 一个是记录初始输入的边权。
最小生成树的Kruskal算法 (边出发) :
该算法是按照边的权值顺序, 从大到小的进行排序, 如果不产生圈(重边)的话, 我们就把他放进去 , 否者下一个边;
特点是使用了并查集, 高效的进行了点与点直接连通关系的维护;
下面是两个算法的具体实施代码:
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <cctype> #include <algorithm> #include <string> #include <vector> #include <queue> #include <list> #include <map> #include <stack> #include <set> using namespace std; const int MAX_V = 105; const int MAX_E = 1000; const int INF = 1e7; int V, E; //点的数量, 边的数量 int cost[MAX_V][MAX_V]; //边权 int mincost[MAX_V]; //到现有生成树的距离 bool used[MAX_V]; //标记这个点是否被使用了 int par[MAX_V], ran[MAX_V]; //并查集的要素 struct node{ int from, to, cost; //边的三种属性 }; node edge[MAX_E]; //对边属性的存储 //Initial void Initial(void){ for(int i = 0; i < V; i++){ ran[i] = 1, par[i] = i; } } //Find int Find(int x){ if(x == par[x]) return x; else return par[x] = Find(par[x]); } //Same bool Same(int x, int y){ return Find(x) == Find(y); } //Unite void Unite(int x, int y){ x = Find(x), y = Find(y); if(x == y) return; else if(ran[x] < ran[y]) par[x] = y; else{ par[y] = x; if(ran[x] == ran[y]) ran[x]++; } } // sort cmp; int cmp(const void *a, const void *b){ node *aa = (node *)a; node *bb = (node *)b; return aa->cost - bb->cost; } int main() { /* cin >> V >>E; //输入 vertax edge number! for(int i = 0; i < E; i++){ int a, b, c; scanf("%d%d%d", &a, &b, &c); cost[a][b] = cost[b][a] = c; edge[i].from = a, edge[i].to = b, edge[i].cost = c; } qsort(edge, E, sizeof(edge[0]), cmp); int res = 0; printf("THE SHOW : \n"); for(int i = 0; i < E; i++){ printf("%d %d %d\n", edge[i].from, edge[i].to, edge[i].cost); } printf("END OF SHOW\n"); // KRUSKAL 算法, 进行运算最小生成树 Initial(); for(int i = 0; i < E; i++){ int a = edge[i].from, b = edge[i].to, c = edge[i].cost; if(Same(a, b)) continue; else Unite(a, b), res += c; } printf("THE MINCOST IS : %d\n", res);*/ //prime 算法 while(~scanf("%d", &V)){ for(int i = 0; i < V; i++){ for(int j = 0; j < V; j++) scanf("%d", &cost[i][j]); } //数据初始化操作 for(int i = 0; i < V; i++){ used[i] = false; mincost[i] = INF; } mincost[0] = 0; //这个没大要求, 只需要设置一个点作为最初的最小生成树就OK; int res = 0; while(true){ int v = -1; for(int i = 0; i < V; i++){ //寻找最短距离点 if(!used[i]&&(v == -1 || mincost[i] < mincost[v])) v = i; } if(v == -1) break; //循环完毕; res += mincost[v]; used[v] = true; //进行数的更新 for(int i = 0; i < V; i++){ mincost[i] = min(mincost[i], cost[v][i]); } } // printf("THE RESULT IS : "); printf("%d\n", res); } return 0; } /* KRUSKAL DATA: 7 9 0 1 2 0 3 10 1 4 7 3 4 5 1 6 1 1 2 3 2 4 1 2 5 5 4 5 8 PRIM DATA : 4 0 4 9 21 4 0 8 17 9 8 0 16 21 17 16 0 */
注意事项:
- mincost 数组的更新, 和我们之前提到的最短路数组的更新不同, mincost 数组的更新, 仅仅是 min( mincost [j], cost [i] [j] ) ;
- 注意边长的编号是从哪里开始的? 如果是 1 的话需要进行减减, 否则的话直接输入就可以了。
- 并查集一定要记得初始化!!! Initial 别只写函数, 不初始化
上面的是目前网上主流的算法, 在这里,我们介绍其他两种不是很常见的方法:
破圈法:
我们知道, 上面的两种算法, 一个是 Prim 算法, 是从一个点为一棵树出发, 不断地更新树, 往里面加顶点, 最后得到最小生成树, 也可以说他是加点法; Kruskal 的算法是不断地往里面加边,更新树, 最后得到最小生成树, 也可以说的上是加边法; 但是我们都可以看出, 这两种算法都不会形成圈, 也就是说, 他们都是避免圈的存在的前提下, 更新树的点、边; 然而我们这种算法破圈法, 思路是从一个完整的图进行出发, 不断地删去回路上面的最大边, 直到图中不存在回路, 算法完成, 这个思路就是我们所说的破圈法。
Sollin算法:
这个算法是一个十分古老的算法