文章目录
前言
本篇文章将通过LeetCode例题,让读者可以通过知识点+实战的方式来领悟什么是最小生成树,最小生成树的两种算法Prim算法和Kruscal算法。
一、最小生成树是什么?
**情景引入:**假设在n个城市之间建立通信联络网,则连通n个城市只需要 n - 1 条线路。这时,自然会考虑这样一个问题,如何在最节省经费的前提下建立这个通信网。
对于n个顶点的连接网可以建立许多不同的生成树。每一棵生成树都可以是一个通信网。现在,要选择这样一棵生成树,也就是使总的耗费最少。这个问题就是构造连通网的最小代价生成树(Minimum Cost Spanning Tree)(简称为最小生成树)的问题。一棵生成树的代价就是树上各边的代价之和。
二、最小生成树算法
1、MST性质
假设N = (V, {E})是一个连通网,U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值(代价)的边,其中u ∈ U, v ∈ V - U,则必存在一棵包含边(u,v)的最小生成树。
2、普里姆(Prim)算法
2.1 算法思路
假设N = (V, {E})是连通网,TE是N上最小生成树中边的集合。算法从U = {u0} (u0 ∈ V),TE = { }开始,重复执行下述操作:在所有的u ∈ U,v ∈ V - U 的边(u,v)∈ E中找一条代价最小的边
(u0, v0)并入集合TE,同时v0并入U,直至U = V 为止。此时TE中必然有n - 1条边,则T = (V, {TE})为N的最小生成树。
2.2 算法的时间复杂度
假设网中有n个顶点,则第一个进行初始化的循环语句的频度为n,第二个循环语句的频度是n-1。其中有两个是内循环(在后续给出的代码中体现):其一是求出最小值,其频度为n-1,其二是重新选择具有最小代价的边,其频度为n。由此,Prim算法的时间复杂度为O( n n n2),与网中的边数无关,因此适用于求边稠密的网的最小生成树。
3、克鲁斯卡尔(Kruscal)算法
3.1 算法思路
假设N = (V, {E})是连通网,则令最小生成树的初始状态只有n个顶点而无边的非连通图
T = (V, { }),图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上。在E中选择代价最小的边,若该依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。
3.2 算法的时间复杂度
算法至多对e条边各扫描一次,如果用“堆“来存放网中的边,则每次选择最小代价的边仅需O( e l o g e eloge eloge)的时间(第一次需要O( e e e))。又生成树T的每个连通分量可看成是一个等价类,则构造T加入新的边的过程类似于求等价类的过程,由此可以用并查集来描述。使得构造T的过程仅需O( e l o g e eloge eloge)的时间。由此Kruscal算法的时间复杂度为O( e l o g e eloge eloge)。
三、实战演练
1、题目编号
- 连接所有点的最小费用
2、题目链接
3、题目描述
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
4、解题代码
本道题目提供两种不同的代码,分别对应上面的Prim算法和Kruscal算法。
4.1 Prim算法解题代码
// prim 算法
class Solution {
#define maxn 1010
#define inf -1
int dist[maxn][maxn];
public:
int lessthan(int a,int b){
if(a == inf){
return b;
}
if(b == inf){
return a;
}
return a<b ? a:b;
}
//定义顶点编号 0 n-1
int minSpanningTree(int n, int dist[maxn][maxn]){
//由于是生成树,所以0一定是在树上
//现在就是想办法吧n-1条边找出来
//首先,找距离0这个点最近的点
int u, dis, ret=0;
int cost[maxn];
memset(cost, 0, sizeof(cost));
//0一定在最小生成树集合中
for(int i = 0; i < n; ++i){
cost[i]=(i==0) ? 0:dist[0][i];
}
//cost[i] 表示最小生成树集合和当前点的距离
//(0,u)这条边一定是距离最小的边,这条边一定在最小生成树
//假设这条边不在最小生成树上,并且其他n-2条边都已经求出来了
//并且0还是个孤立点
//那么为了让0和其他n-1个点连通
//势必要去找一条边能让它和其他点连通
//所以一定是找权值最小的边才是最优的
//更新 最小生成树 和 非最小生成树 之间的距离
while(1){
dis = inf;
for(int i = 0; i < n; ++i){
if(cost[i] && lessthan(cost[i],dis)==cost[i]){
u = i;
dis=cost[i];
}
}
if(dis == inf){
return ret;
}
ret += dis;
cost[u] = 0;
for(int i = 0;i < n; ++i){
if(cost[i] && lessthan(dist[u][i], cost[i]) == dist[u][i]){
cost[i] = dist[u][i];
}
}
}
return inf;
}
int minCostConnectPoints(vector<vector<int>>& points) {
int n = points.size();
memset(dist, inf, sizeof(dist));
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
dist[i][j] = dist[j][i] = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]);
}
}
return minSpanningTree(n, dist);
}
};
4.2 Kruscal算法解题代码
class Solution {
struct Edge {
int start; // 顶点1
int end; // 顶点2
int len; // 长度
};
public:
int Find(vector<int>& pre, int index){
while(pre[index] != index){
index = pre[index];
}
return index;
}
void Join(vector<int>& pre, int index1, int index2){
index1 = Find(pre, index1);
index2 = Find(pre, index2);
if(index1 != index2){
pre[index1] = index2;
}
}
int minCostConnectPoints(vector<vector<int>>& points) {
vector<int> pre(1010);
int n = points.size();
for(int i = 1; i <= n; ++i){
pre[i] = i;
}
vector<Edge> edges;
// 建立点-边式数据结构
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
Edge edge = {i, j, abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])};
edges.emplace_back(edge);
}
}
// 按边长度排序
sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
return a.len < b.len;
});
// 连通分量合并
int num = 0;
int ret = 0;
for(int i = 0; i < edges.size(); ++i){
int x = edges[i].start;
int y = edges[i].end;
int dis = edges[i].len;
if(Find(pre, x) != Find(pre, y)){
ret += dis;
Join(pre, x, y);
}
if(num == n){
break;
}
}
return ret;
}
};
5、解题思路
本道题目提供两种不同的解题思路,分别对应上面的Prim算法和Kruscal算法。
5.1 Prim算法解题思路
(1) 首先先用邻接矩阵来存储图。
(2) 接着先将点0放在选的点的集合当中。
(3) 接着更新集合中的点到不在集合中的点的最小距离,然后选取最小的边,将新的点加入集合中,依次重复上述的事情直到所有的点都在集合中。
(4) 最后返回出和即为最小生成树的权值和。
5.2 Kruscal算法解题思路
(1) 首先将所有的边按照长度从小到大排序。
(2) 因为总共有n个点,所以要选n-1个边。利用并查集来选取符合条件的n-1条长度最短的边即可。
(3) 最后返回出和即为最小生成树的权值和。
总结
通过本篇文章,相信读者已经对最小生成树有所理解,如果觉得不熟练的话,建议读者独立完成LeetCode上的这道题目,分别使用Prim算法和Kruscal算法来解决。