Prim算法是解决加权无向连通图最小生成树问题的经典贪心算法。与Kruskal算法不同,Prim算法采用"逐步生长"的策略构建最小生成树,其核心思想可以概括为:从任意顶点开始,每次选择连接树与非树顶点且权重最小的边,将对应顶点加入生成树,直到所有顶点都被包含。
算法执行过程可分为以下关键步骤:
这个过程中,算法始终保持当前生成树是最优局部解,通过贪心选择逐步扩展,最终得到全局最优解。Prim算法的时间复杂度取决于实现方式,使用普通数组时为O(V²),采用优先队列可优化至O(E log V)。
Prim算法的正确性基于以下两个关键性质:
通过数学归纳法可以证明:算法每一步选择的边都满足切割性质,因此最终构建的生成树必然是最小生成树。这也是Prim算法与Kruskal算法虽然策略不同但都能得到正确解的根本原因。
实现Prim算法需要合理选择数据结构来高效执行以下操作:
我们推荐使用以下数据结构组合:
cpp复制struct Edge {
int to;
int weight;
Edge(int t, int w) : to(t), weight(w) {}
};
vector<vector<Edge>> adj; // 邻接表存储图
vector<int> minWeight; // 记录各顶点到生成树的最小权重
vector<bool> inMST; // 标记顶点是否在生成树中
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq; // 小顶堆
以下是使用优先队列优化的Prim算法实现:
cpp复制#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
int primMST(const vector<vector<Edge>>& graph, int start) {
int n = graph.size();
minWeight.assign(n, INT_MAX);
inMST.assign(n, false);
minWeight[start] = 0;
pq.push({0, start});
int totalWeight = 0;
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (inMST[u]) continue;
inMST[u] = true;
totalWeight += minWeight[u];
for (const Edge& e : graph[u]) {
int v = e.to, w = e.weight;
if (!inMST[v] && w < minWeight[v]) {
minWeight[v] = w;
pq.push({minWeight[v], v});
}
}
}
return totalWeight;
}
根据图的特点,Prim算法有多种实现变体:
| 场景特征 | 推荐实现方式 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 稠密图(V²≈E) | 数组实现 | O(V²) | O(V) |
| 稀疏图(E≈V) | 二叉堆优先队列 | O(E log V) | O(V+E) |
| 边权重范围较小 | 桶优先队列 | O(E+V log V) | O(V+E) |
| 需要频繁查询 | 斐波那契堆 | O(E+V log V) | O(V+E) |
cpp复制vector<Edge> edges;
vector<size_t> offsets;
// 替代vector<vector<Edge>>提高缓存命中率
cpp复制edges.reserve(2*E); // 无向图每条边存储两次
offsets.reserve(V+1);
cpp复制bitset<MAX_V> inMST; // 节省内存空间
cpp复制// 错误示例:未初始化为INT_MAX导致选择错误
minWeight.assign(n, 0);
// 正确做法:
minWeight.assign(n, INT_MAX);
cpp复制// 错误示例:默认大顶堆导致选择最大边
priority_queue<pair<int, int>> pq;
// 正确做法:
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
cpp复制// 错误示例:重复累加边权重
totalWeight += e.weight;
// 正确做法:
totalWeight += minWeight[u];
cpp复制cout << "Add vertex " << u << " via edge weight " << minWeight[u] << endl;
cpp复制assert(minWeight[u] != INT_MAX && "Unreachable vertex added to MST");
交叉验证:与Kruskal算法结果对比验证正确性
边界测试:
在实现优化过程中,我发现两个值得注意的实践细节:一是优先队列的实现选择会显著影响实际性能,在边数超过10^5时,斐波那契堆虽然理论复杂度更优,但由于常数因子较大,实际表现可能不如二叉堆;二是对于固定拓扑的网络结构,可以预先计算并缓存生成树,避免重复计算。