最小生成树(MST)算法
完整版万字原文见史上最全详解图数据结构
加权无向图中,最小生成树是一个包含图中所有节点的子图
树 ———> 包含图中所有节点
最小 ———> 树中的边权之和最小
1. Prim算法(最小生成树)
算法原理:
1. 贪心算法
2. 从一个起始点开始,逐步选择与当前生成树相连的、权重最小的边,直到所有节点都被包含在生成树中(贪心策略保证了每次选择的边都是当前情况下的最佳选择)
3. 时间复杂度为 O(V^2)(稠密图)
4. 应用:适用于求解加权无向图的最小生成树,尤其是稠密图。
提要 :
Prim算法 过程非常类似于 Dijkstra 算法
只有 distance 数组和 weight 数组保存的值的意义不同 :
Dijkstra 保存从该点到起始点最短路径长度,Prim 保存已选集合和未选集合之间最短的边权
复习 dijkstra算法
代码实现(未优化版本)
void MyGraph::Prim()
{
vector<bool> visited(n, false);
// 创建一个向量 weight,记录每个节点与生成树的最小连接权重,初始值为无穷大
vector<int> weight(n, numeric_limits<int>::max());
// prev 向量记录最小生成树中每个节点的父节点,初始值为 -1
vector<int> prev(n, -1);
// 设置起始节点的权重为 0,表示从第一个节点开始构建生成树。
weight[0] = 0;
for (int i = 0; i < n; i++)
{
int minVertex = -1;
int minWeight = numeric_limits<int>::max();
// 从尚未加入生成树的节点中,找到权重最小的节点 minVertex
for (int i = 0; i < n; i++)
if (!visited[i] && weight[i] < minWeight)
minWeight = weight[i], minVertex = i;
// 将该节点加入最小生成树,并增加已加入节点的计数。
visited[minVertex] = true;
// 遍历 minVertex 的邻接节点,更新连接权重,如果当前邻接节点的权重小于之前记录的最小权重,则更新权重并记录其父节点
for (const auto &pair : adjList[minVertex])
if (!visited[pair.first] && pair.second < weight[pair.first])
weight[pair.first] = pair.second, prev[pair.first] = minVertex;
// 打印输出
for (int i = 1; i < n; i++)
cout << "Edge " << prev[i] << " - " << i << " with weight " << weight[i] << endl;
}
}
代码实现(优化版本)
void MyGraph::Prim()
{
vector<bool> visited(n, false);
vector<int> weight(n, numeric_limits<int>::max());
vector<int> prev(n, -1);
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, startVertex});
weight[startVertex] = 0;
//for (int i = 0; i < n; i++)
while (!pq.empty())
{
// int minVertex = -1;
// int minWeight = numeric_limits<int>::max();
int now_minWeight = pq.top().first;
int now_minVertex = pq.top().second;
pq.pop();
// for (int i = 0; i < n; i++)
// if (!visited[i] && weight[i] < minWeight)
// minWeight = weight[i], minVertex = i;
visited[now_minVertex] = true;
for (const auto &neighbor : adjList[now_minVertex])
{
if (!visited[neighbor.first] && neighbor.second < weight[neighbor.first])
{
weight[neighbor.first] = neighbor.second;
prev[neighbor.first] = now_minVertex;
pq.push({neighborWeight, neighbor.first});
}
}
}
for (int i = 0; i < n; i++)
if (i != startVertex)
cout << "vertex " << i << " distance from " << startVertex << " is " << weight[i] << endl;
}
2. Kruskal算法(最小生成树)
算法原理:
1. Kruskal 算法按边的权重升序排序,然后通过并查集判断边是否形成环,逐步选择不形成环的边,构建最小生成树。
2. 时间复杂度为 O(E log E)
3. 应用:适用于稀疏图的最小生成树。
算法步骤
1. 排序边:
将所有边按照权重升序排序。
2. 初始化并查集:
每个节点自成一个集合,即每个节点都是其自己的根。
3. 构造最小生成树:
从排序后的边中,从最小权重开始顺序处理。
4. 对于每条边 (u, v):
使用并查集检查 u 和 v 是否在同一个集合中。
(1)如果不在同一个集合中,那么这条边可以安全地加入到生成树中,因为没有形成环。
(2)如果在同一个集合中,那么这条边会导致生成环,因此跳过这条边。
5. 重复步骤:直到生成树包含 V-1 条边,其中 V 是图中节点的数量。
为了有效地检测边是否会形成环,Kruskal 算法使用 并查集(Union-Find) 数据结构。
建议先去了解 并查集
代码实现
void MyGraph::Kruskal()
{
// 创建一个存储边的向量 edges,每个边由权重和两个顶点组成
vector<pair<int, pair<int, int>>> edges;
for (int i = 0; i < n; i++)
for (const auto &edge : adjList[i])
edges.push_back({edge.second, {i, edge.first}});
// 1. 按边的权重升序排序:首先将图中的所有边按权重从小到大排序。
sort(edges.begin(), edges.end());
//2. 初始化并查集:
// 初始化一个并查集 forest,用于判断不同的节点是否属于同一个连通分量(是否形成环)。
// forest 保存每个顶点和其父节点
vector<pair<int, int>> forest(n);
// 每个节点自成一个集合,即每个节点都是其自己的根
for (int i = 0; i < n; i++)
forest[i] = {i, i};
// 3. 构造最小生成树
// 遍历所有边,如果两个节点不属于同一个集合(不形成环),则将该边加入生成树,并合并这两个集合。当加入的边数达到 (V-1) 时,最小生成树构建完成
int numEdges = 0;
for (const auto &edge : edges)
{
int root1 = find(forest, edge.second.first);
int root2 = find(forest, edge.second.second);
if (root1 != root2)
{
forest[root1] = {root1, root2};
numEdges++;
cout << "Edge " << edge.second.first << " - " << edge.second.second << " with weight " << edge.first << endl;
if (numEdges == n - 1)
break;
}
}
}
// 用于并查集(Union-Find)算法中的“路径压缩”查找操作。它递归地查找某个顶点的根,并且在查找过程中把该路径上的所有顶点直接连接到根,提高后续查找效率
// 找到 vertex 所属集合的根节点
int find(vector<pair<int, int>> &forest, int vertex)
{
// forest[vertex].second 表示顶点 vertex 的父节点。如果当前顶点的父节点不是它自己,说明它不是根节点。
// 在递归返回后,路径压缩的核心操作发生:将 vertex 直接连接到根节点。这一步显著提高了未来查找的效率,因为路径被压缩了
if (forest[vertex].second != vertex)
forest[vertex].second = find(forest, forest[vertex].second);
return forest[vertex].second;
/*假设
forest = {
{0, 0}, // 顶点 0 的父节点是 0 (根节点)
{1, 0}, // 顶点 1 的父节点是 0
{2, 1}, // 顶点 2 的父节点是 1
{3, 2}, // 顶点 3 的父节点是 2
{4, 3}, // 顶点 4 的父节点是 3
}
这个结构表示顶点 0 是根节点,其他顶点通过不同的路径连接到根节点 0。
调用 find(forest, 4) 来查找顶点 4 的根节点。
forest[4].second != 4:顶点 4 的父节点是 3,而不是它自己,所以继续递归查找。
递归调用 find(forest, 3):
forest[3].second != 3:顶点 3 的父节点是 2,继续递归查找。
递归调用 find(forest, 2):
forest[2].second != 2:顶点 2 的父节点是 1,继续递归查找。
递归调用 find(forest, 1):
forest[1].second != 1:顶点 1 的父节点是 0,继续递归查找。
递归调用 find(forest, 0):
forest[0].second == 0:顶点 0 的父节点是它自己,返回 0 作为根节点。
接着路径压缩开始生效:
路径压缩应该是从递归返回时,按递归调用的顺序依次更新父节点。
forest[1].second = 0(顶点 1 的父节点更新为根节点 0)
forest[2].second = 0(顶点 2 的父节点更新为根节点 0)
forest[3].second = 0(顶点 3 的父节点更新为根节点 0)
forest[4].second = 0(顶点 4 的父节点更新为根节点 0)
最终,find(forest, 4) 返回 0,并且路径压缩后,forest 变为:
forest = {
{0, 0}, // 顶点 0 的父节点是 0
{1, 0}, // 顶点 1 的父节点是 0
{2, 0}, // 顶点 2 的父节点是 0
{3, 0}, // 顶点 3 的父节点是 0
{4, 0}, // 顶点 4 的父节点是 0
}
*/
}
标签:Prim,weight,int,Kruskal,second,算法,forest,顶点,节点 From: https://blog.csdn.net/2301_81373791/article/details/144213766