Kruskal算法简介:
(1)将所有边按权值从小到大排序;
(2)选出权值最小的边,如果这条边的两个端点不属于同一个集合,就将这条边添加到最小生成树里面;
(3)重复(2)直到选出了
节
点
数
−
1
节点数-1
节点数−1 条边;
如果最后选出的边数小于 节 点 数 − 1 节点数-1 节点数−1 ,说明该无向图不能构建出最小生成树。
查并集操作简介:
这里我们假设集合是用树来表示的,也就是每个节点都有一个父节点(根节点的父节点是自身)。
因此通过不断往上一个集合里面的点的父节点,最后都可以找到根节点,也就意味着每个集合是可以用父节点来唯一表示的。
查操作: 查询某个节点属于哪个集合,基于上述的假设,该问题可以转化为查询该节点的根节点。
并操作: 将两个集合合并,自然而然的做法就是将其中一个集合的根节点设置成另一个集合的根节点的子节点。注意,通常情况下我们会将小集合的根节点设置成大节点的子节点,因为小集合的树的深度一般较浅,添加到大集合之后对查询操作比较友好。
查询优化: 基于上述的查操作,我们当然希望每个子节点的父节点就是根节点,这样一次就能得到查询结果。因此,在进行查操作之后,我们可以将查询节点的父节点、查询节点的父节点的父节点等一系列点的父节点设置成根节点。
下面给出一份具有详细注释的源代码,可以很好地帮助大家理解上述算法。
//
// main.cpp
// KruskalAlalgorithm
//
// Created by 胡昱 on 2021/12/22.
//
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
// 定义所需用到的全局变量
const int MAX_N = 500; // 顶点的最大数量
int father[MAX_N + 1]; // 记录每一个顶点的父节点,用以查并集
// 边类
struct Edge {
int u; // 一个顶点
int v; // 另一个顶点
int w; // 边的权重
};
// 比较两条边的权重大小
int compareEdgesByW(const Edge& a, const Edge& b) {
return a.w < b.w;
}
// 判断两条边是否为重边
bool operator == (const Edge&a, const Edge& b) {
return (a.u == b.u) && (a.v == b.v);
}
// 查某个点舒服哪个集合(也就是根节点是哪个),同时顺便进行路径压缩
int find(int x) {
// 子节点的父节点肯定不是自己,根节点的父节点肯定是自己,可以利用这个来寻找根节点
int nowFather = x;
while(father[nowFather] != nowFather) {
nowFather = father[nowFather];
}
// 路径压缩,也就是让刚才查询中涉及到的点的父节点都设置成根节点
int i = x, j;
while(father[i] != nowFather) {
// 记录下一个需要更新的节点
j = father[i];
// 将当前节点的父节点设置成根节点
father[i] = nowFather;
// 处理下一个节点
i = j;
}
return nowFather;
}
// 合并两个集合
// 讲道理将小树(小集合)添加到大树(大集合)里面会比较好,但是在这边我们不做考虑
void unionSets(int u, int v) {
int uFather = find(u);
int vFather = find(v);
// 如果这两个点所属集合的根节点是一致的,说明已经同属一个集合
// 否则将这两个节点所属集合合并
if(uFather != vFather) {
// 将u所属集合的根节点变成v所属集合的根节点的子节点
father[uFather] = vFather;
}
}
int main(int argc, const char * argv[]) {
// 共T组测试用例
int T;
cin >> T;
while((T--) > 0) {
// 输入顶点数n、边数E
int n, E;
cin >> n >> E;
// 创建边数组并输入边
vector<Edge> edges;
for(int ei = 0; ei < E; ++ei) {
Edge edge;
cin >> edge.u >> edge.v >> edge.w;
edges.push_back(edge);
}
// 先对边数组排序,再进行去重,最后更新
sort(edges.begin(), edges.end(), compareEdgesByW);
edges.erase(unique(edges.begin(), edges.end()), edges.end());
E = (int)edges.size();
// 初始化父节点集合,此时所有顶点的父节点都是自身,即每个顶点都是单独的一棵树
// 注意:顶点是从1开始编号的
for(int vi = 1; vi <= n; ++vi) {
father[vi] = vi;
}
// 开始Kruskal算法
// 用于记录结果,也就是最小生成树权值
int result = 0;
// K用于记录已经添加的边数
int k = 0;
// 每次都从边数组中取出最小的边(因为我们已经排好序了,所以直接取出即可)
for(Edge edge: edges) {
// 如果k等于n - 1,说明已经取了足够的边了,因为n个点需要n-1条边连接
if(k >= n - 1) {
break;
}
// 如果这条边的两个点不在同一个集合,那么这条边需要添加到最小生成树里面
// 添加指的就是将这两个点的集合合并
if(find(edge.u) != find(edge.v)) {
unionSets(edge.u, edge.v);
result += edge.w;
++k;
}
}
// Kruskal算法结束
// 如果k小于n - 1,说明不能生成最小生成树
// 也可以判断所有点是否在同一个集合,但时间复杂度较高
if(k < n - 1) {
result = -1;
}
// 输出结果
cout << result << endl;
}
return 0;
}