常见的最小生成树算法包括:
Prim算法:基于顶点的贪婪算法,从一个初始顶点开始,逐步选择与当前生成树相连的权值最小的边所连接的顶点,直到所有顶点都被包含在生成树中。
Kruskal算法:基于边的贪婪算法,首先将所有边按权值从小到大排序,然后逐步选择权值最小的边,若加入该边不构成环,则将其加入生成树中,直到生成树包含了所有顶点。
Boruvka算法:一种并行算法,通过每次选择每个连通分量的最小边来构建生成树,直到只剩下一个连通分量为止。
这些算法都是用来寻找一个图中的最小生成树,即连接所有顶点且总权值最小的树。
Prim算法:
- 时间复杂度: Prim算法的时间复杂度通常是O(n^2),其中n是顶点的数量。这是因为算法需要遍历所有的边来找到最小权重的边,而在一个稠密图中,边的数量接近于n^2。如果使用优先队列(如二叉堆)来优化,时间复杂度可以降低到O(m log n),其中m是边的数量。
- 空间复杂度: Prim算法的空间复杂度主要取决于存储图的数据结构和并查集的大小。如果使用邻接矩阵存储图,则空间复杂度为O(n^2);如果使用邻接表,则为O(n + m)。并查集的空间复杂度为O(n)。
Kruskal算法:
- 时间复杂度: Kruskal算法的时间复杂度为O(m log m),因为它需要对所有的边进行排序,然后遍历这些边来构建最小生成树。这里的m是边的数量,排序通常使用快速排序或归并排序,它们的时间复杂度都是O(m log m)。
- 空间复杂度: Kruskal算法的空间复杂度同样取决于图的表示方法和并查集的大小。如果使用邻接矩阵,空间复杂度为O(n^2);使用邻接表则为O(n + m)。并查集的空间复杂度为O(n)。
综上所述,Prim算法更适合求解稠密图的最小生成树问题,而Kruskal算法更适合求解稀疏图的最小生成树问题。
基于Kruskal算法的最小生成树算法:
Kruskal算法是一种贪心算法,用于求解最小生成树问题。它的基本步骤如下:
- 将所有边按照权值从小到大排序。
- 初始化一个空的森林,用于存储最小生成树的边。
- 遍历排序后的边,对于每一条边,检查它连接的两个顶点是否属于同一个连通分量。如果不属于,则将这条边加入森林中,并将两个顶点所在的连通分量合并。
- 当森林中的边数等于顶点数减一时,停止遍历。此时得到的森林就是最小生成树。
另外注意一件事,所谓的并查集查找,可以通俗地将其看作从某个结点开始一路查找到根,而这个根节点就是当下节点所在树的集合的代表。
因此,在这个最小生成树里,每次找到的边要通过查看点是否与已经纳入到正在构成的最小树里(即查看当前点的根与最小树的根是否一致)来避免形成环。- 算法的时间复杂度为:O(ElogE)或者O(ElogV),其中E代表图中的边的数目,V代表图中的顶点数目。对图中的边按照非降序排列需要O(ElogE)的时间。排序后遍历所有的边并判断添加边是否构成环,判断添加一条边是否构成环最坏情况下需要O(logV),关于这个复杂度等到景禹给你们谈并查集的时候再分析;因此,总的时间复杂度为O(ElogE + ElogV),其中E的值最大为V(V-1)/2,因此O(logV) 等于 O(logE)。因此,总的时间复杂度为O(ElogE) 或者O(ElogV)。
参考:算法学习笔记(1) : 并查集 - 知乎 (zhihu.com)- Kruskal算法的基本思想是维护一个森林,查询两个结点是否在同一棵树中,并连接两棵树。在实际的算法过程中,我们需要对边集进行排序,复杂度O ( mlogm ) O(m\log m)O(mlog m),并使用O ( mlog n ) O(m \log n)O(mlogn)并查集维护集合。总复杂度O ( m log m ) O(m \log m)O(mlogm)。因此对于稠密图,算法性能就会变差。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//边,u、v代表边的起点和重点,w代表权重
struct Edge {
int u, v, w;
bool operator<(const Edge& other) const {
return w < other.w;
}
};
class UnionFind {
public:
UnionFind(int n) {
parent.resize(n);
rank.resize(n, 0);
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
bool unite(int x, int y) {
int root_x = find(x);
int root_y = find(y);
if (root_x == root_y) {
return false;
}
if (rank[root_x] < rank[root_y]) {
parent[root_x] = root_y;
} else {
parent[root_y] = root_x;
if (rank[root_x] == rank[root_y]) {
++rank[root_x];
}
}
return true;
}
private:
vector<int> parent;
vector<int> rank;
};
vector<Edge> kruskal(const vector<Edge>& edges, int n) {
sort(edges.begin(), edges.end());
UnionFind uf(n);
vector<Edge> mst;
for (const Edge& e : edges) {
if (uf.unite(e.u, e.v)) {
mst.push_back(e);
}
}
return mst;
}
int main() {
int n, m;
cin >> n >> m;
vector<Edge> edges(m);
for (int i = 0; i < m; ++i) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
--edges[i].u;
--edges[i].v;
}
vector<Edge> mst = kruskal(edges, n);
cout << "Minimum Spanning Tree:" << endl;
for (const Edge& e : mst) {
cout << e.u + 1 << " - " << e.v + 1 << " : " << e.w << endl;
}
return 0;
}
基于Prim算法的最小生成树算法:
普里姆算法在找最小生成树时,将顶点分为两类,一类是在查找的过程中已经包含在生成树中的顶点(假设为 A 类),剩下的为另一类(假设为 B 类)。
对于给定的连通网,起始状态全部顶点都归为 B 类。在找最小生成树时,选定任意一个顶点作为起始点,并将之从 B 类移至 A 类;然后找出 B 类中到 A 类中的顶点之间权值最小的顶点,将之从 B 类移至 A 类,如此重复,直到 B 类中没有顶点为止。所走过的顶点和边就是该连通图的最小生成树。
该算法的时间复杂度是O(n^2)
Prim算法的基本思想是每次需要寻找距离最小的一个结点(与Dijkstra’s Algorithm相似),以及新的边来更新其它结点的距离。在寻找距离最小点的过程中,可以暴力查找,也可以采用堆维护进行优化。在使用二叉堆优化的加持下,复杂度O ( ( n + m ) log n ) O((n + m)\log n)O((n+m)logn)。相比于K r u s k a l KruskalKruskal,P r i m PrimPrim更适用于稠密图
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
const int V = 5; // 顶点个数
int minKey(int key[], bool mstSet[]) {
int min = INT_MAX, min_index;
for (int v = 0; v < V; v++) {
if (mstSet[v] == false && key[v] < min) {
min = key[v];
min_index = v;
}
}
return min_index;
}
void printMST(vector<int> parent, vector<vector<int>> graph) {
cout << "边\t权值" << endl;
for (int i = 1; i < V; i++) {
cout << parent[i] << " - " << i << "\t" << graph[i][parent[i]] << endl;
}
}
void primMST(vector<vector<int>> graph) {
vector<int> parent(V); // 存储最小生成树的父节点
vector<int> key(V); // 存储每个顶点到最小生成树的最小权值
bool mstSet[V]; // 记录顶点是否已经加入最小生成树
for (int i = 0; i < V; i++) {
key[i] = INT_MAX;
mstSet[i] = false;
}
key[0] = 0; // 将第一个顶点作为起始点
parent[0] = -1; // 第一个顶点没有父节点
for (int count = 0; count < V - 1; count++) {
int u = minKey(key, mstSet);
mstSet[u] = true;
for (int v = 0; v < V; v++) {
if (graph[u][v] && mstSet[v] == false && graph[u][v] < key[v]) {
parent[v] = u;
key[v] = graph[u][v];
}
}
}
printMST(parent, graph);
}
int main() {
vector<vector<int>> graph = {
{0, 2, 0, 6, 0},
{2, 0, 3, 8, 5},
{0, 3, 0, 0, 7},
{6, 8, 0, 0, 9},
{0, 5, 7, 9, 0}
};
primMST(graph);
return 0;
}
基于Boruvka算法的最小生成树算法:
一种并行算法,通过每次选择每个连通分量的最小边来构建生成树,直到只剩下一个连通分量为止。
对于BoruvkaBoruvka算法,一个比较笼统的表述是,一个多路增广版本的 Kruskal。它的思想是一开始所有点看做独立子集,每次遍历边找到两个集合(连通块)之间连接的最短边,不断扩大集合(连通块)直到所有点合并为一个集合(连通块)。
在并查集算法中,初始状态下我们将每个点视为一个独立的点集,并不断地合并集合。在B r o u v k a BrouvkaBrouvka算法中,我们在一开始将所有点视为独立子集,每次我们找到两个集合(即为连通块)之间的最短边,然后扩展连通块进行合并。基本思路是:生成树中所有顶点必然是连通的,所以两个不相交集必须连接起来才能构成生成树,而且所选择的连接边的权重必须最小,才能得到最小生成树。
首先将所有点视为各自独立的集合,初始化一个空的M S T MSTMST;
当子集个数大于1 11的时候,对各个子集和执行以下操作:
- 找到与当前集合有边的集合,选出权值最小的边;
- 如果该权值最小的边不在M S T MSTMST中;
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
// 定义边的结构体
struct Edge {
int src, dest, weight;
};
// 定义并查集的结构体
struct Subset {
int parent, rank;
};
// 查找顶点的根节点
int find(Subset subsets[], int i) {
if (subsets[i].parent != i)
subsets[i].parent = find(subsets, subsets[i].parent);
return subsets[i].parent;
}
// 合并两个子集
void Union(Subset subsets[], int x, int y) {
int xroot = find(subsets, x);
int yroot = find(subsets, y);
if (subsets[xroot].rank < subsets[yroot].rank)
subsets[xroot].parent = yroot;
else if (subsets[xroot].rank > subsets[yroot].rank)
subsets[yroot].parent = xroot;
else {
subsets[yroot].parent = xroot;
subsets[xroot].rank++;
}
}
// Boruvka算法实现最小生成树
void boruvkaMST(vector<Edge>& edges, int V) {
vector<Edge> result; // 存储最小生成树的边
vector<Subset> subsets(V); // 并查集数组
// 初始化并查集
for (int v = 0; v < V; ++v) {
subsets[v].parent = v;
subsets[v].rank = 0;
}
// 迭代直到所有顶点都在同一个连通分量中
while (result.size() < V - 1) {
int minWeight = INT_MAX;
int minIndex = -1;
// 遍历所有的边,找到权重最小的边
for (int i = 0; i < edges.size(); ++i) {
int set1 = find(subsets, edges[i].src);
int set2 = find(subsets, edges[i].dest);
if (set1 != set2) {
if (edges[i].weight < minWeight) {
minWeight = edges[i].weight;
minIndex = i;
}
}
}
// 将找到的边加入结果中,并合并两个顶点所在的集合
result.push_back(edges[minIndex]);
Union(subsets, edges[minIndex].src, edges[minIndex].dest);
}
// 输出最小生成树的边
cout << "最小生成树的边为:" << endl;
for (const auto& edge : result) {
cout << edge.src << " -- " << edge.dest << " == " << edge.weight << endl;
}
}
int main() {
int V = 4; // 顶点数
vector<Edge> edges = {
{0, 1, 10},
{0, 2, 6},
{0, 3, 5},
{1, 3, 15},
{2, 3, 4}
}; // 边的列表,每个元素包含源顶点、目标顶点和权重
boruvkaMST(edges, V);
return 0;
}
运行结果如下:
最小生成树的边为:
2 -- 3 == 4
0 -- 3 == 5
0 -- 1 == 10
Boruvka算法是一种基于并查集的贪心算法,用于求解最小生成树问题。算法的基本思想是每次从图中选择一条权重最小的边,将其加入到最小生成树中,并将这条边的两个顶点所在的集合合并。重复这个过程,直到所有顶点都在同一个连通分量中,此时得到的最小生成树就是最终结果。