☰
Prim算法求解最小生成树:公路修建问题实战
2026/11/2 8:25:26 网站建设 项目流程

1. 问题背景与题目解析

洛谷P1265《公路修建》是一道经典的图论与最小生成树算法练习题。题目描述了一个需要连接多个城市的高速公路网络建设场景:给定N个城市的坐标位置,要求设计一个总长度最短的公路网络,使得任意两个城市之间都可以直接或间接到达。

这道题的核心在于将实际问题抽象为图论模型:

  • 每个城市代表图中的一个顶点
  • 每两个城市之间潜在的公路连接代表图中的一条边
  • 边的权值就是两个城市之间的欧几里得距离
  • 目标就是找到一个边的子集,使得所有顶点连通且边的总权值最小

这正符合最小生成树(MST, Minimum Spanning Tree)的定义。在实际工程中,类似的场景非常常见,比如通信基站布线、电网架设、输油管道规划等,都需要解决这种"用最小成本实现全局连通"的问题。

2. 算法选型与复杂度分析

对于最小生成树问题,主要有两种经典算法:

2.1 Prim算法

Prim算法从一个顶点开始,逐步扩展生成树。每次选择连接树与非树顶点的最小权边加入生成树。对于稠密图(边数接近完全图),Prim算法更为高效。

在本题中:

  • 城市数量N ≤ 5000
  • 每对城市之间都存在潜在连接,属于完全图
  • 边数E = N(N-1)/2 ≈ 1.25×10⁷(当N=5000时)

使用邻接矩阵存储的Prim算法时间复杂度为O(N²),空间复杂度也是O(N²)。对于N=5000,N²=25×10⁶,这在现代计算机上是可以接受的。

2.2 Kruskal算法

Kruskal算法按边权从小到大排序,依次选择不形成环的边加入生成树。通常使用并查集来高效判断环的存在。

对于完全图:

  • 排序阶段复杂度O(E log E) = O(N² log N²) = O(N² log N)
  • 并查集操作近似O(α(N)),总体O(N² log N)

当N=5000时,N² log N ≈ 2.5×10⁸,明显比Prim算法慢。

2.3 决策依据

基于上述分析:

  • 本题是完全图,边数远大于顶点数
  • Prim算法在稠密图中表现更优
  • 题目N≤5000,Prim的O(N²)复杂度可接受
  • 不需要存储所有边,节省空间

因此选择Prim算法更为合适。特别是可以使用堆优化的Prim算法,但在完全图中优势不明显。

3. 具体实现细节

3.1 距离计算

城市坐标(x,y)给出,两点a(x₁,y₁)和b(x₂,y₂)之间的距离d为: d = √[(x₂-x₁)² + (y₂-y₁)²]

注意:

  1. 实际计算时不需要开平方,直接比较距离平方即可
  2. 但最终总长度需要真实距离而非平方值

3.2 Prim算法实现步骤

  1. 初始化:

    • 选定起始城市(任意)
    • 维护一个数组dist,记录各城市到当前生成树的最小距离
    • 初始化dist[0] = 0,其他dist[i] = ∞
    • 维护一个数组visited标记是否已加入生成树
  2. 主循环:

    • 每次选择dist最小的未访问城市u
    • 将u标记为已访问
    • 总长度累加dist[u]
    • 更新u的所有邻接城市v的dist值: dist[v] = min(dist[v], distance(u,v))
  3. 终止条件:

    • 所有城市都被访问
    • 累计的总长度即为答案

3.3 优化技巧

  1. 距离缓存:

    • 不需要预先计算所有城市对的距离
    • 在Prim过程中按需计算,节省空间
  2. 堆优化:

    • 使用优先队列快速获取最小dist城市
    • 但在完全图中,简单的线性查找可能更优(常数因子小)
  3. 精度处理:

    • 使用double类型存储距离
    • 输出时保留两位小数

4. 完整代码实现(C++)

#include <iostream> #include <vector> #include <cmath> #include <climits> #include <iomanip> using namespace std; double calculateDistance(pair<int, int> a, pair<int, int> b) { long long dx = a.first - b.first; long long dy = a.second - b.second; return sqrt(dx*dx + dy*dy); } double primMST(const vector<pair<int, int>>& cities) { int n = cities.size(); vector<double> dist(n, 1e18); vector<bool> visited(n, false); double total = 0.0; dist[0] = 0.0; for (int i = 0; i < n; ++i) { int u = -1; // 线性查找最小dist for (int j = 0; j < n; ++j) { if (!visited[j] && (u == -1 || dist[j] < dist[u])) { u = j; } } if (dist[u] == 1e18) return -1; // 不连通(本题保证连通) visited[u] = true; total += dist[u]; // 更新邻接城市 for (int v = 0; v < n; ++v) { if (!visited[v]) { double d = calculateDistance(cities[u], cities[v]); if (d < dist[v]) { dist[v] = d; } } } } return total; } int main() { int n; cin >> n; vector<pair<int, int>> cities(n); for (int i = 0; i < n; ++i) { cin >> cities[i].first >> cities[i].second; } double totalLength = primMST(cities); cout << fixed << setprecision(2) << totalLength << endl; return 0; }

5. 算法正确性证明

Prim算法能得到最小生成树的证明基于贪心选择性质:

  1. 安全边:每次选择的dist最小的边(u,v)(u在树中,v不在)必然是某棵最小生成树的边。

    • 假设存在不包含(u,v)的MST T
    • 将(u,v)加入T会形成环,环中必有另一条边(u',v')连接树与非树
    • 由于(u,v)是dist最小的边,w(u,v) ≤ w(u',v')
    • 用(u,v)替换(u',v')得到T',总权值不增加
    • 因此(u,v)属于某棵MST
  2. 归纳法:

    • 初始时,空集是某棵MST的子集
    • 每次加入安全边后,仍保持是某棵MST的子集
    • 最终得到完整的MST

6. 性能优化与边界情况

6.1 进一步优化方向

  1. 堆优化Prim:

    • 使用优先队列存储(dist, city)对
    • 每次取最小dist城市时间复杂度降为O(log N)
    • 总体复杂度O(N log N)
    • 但完全图中更新邻接点仍需O(N),实际可能更慢
  2. 并行计算:

    • 距离计算可以并行化
    • 但算法本身有顺序依赖,难以完全并行

6.2 边界情况处理

  1. 单城市情况:

    • 总长度应为0
    • 算法中不进入主循环,直接返回0
  2. 浮点精度:

    • 坐标范围未给出,但距离计算可能溢出
    • 使用long long存储中间结果
    • 最终用double存储总长度
  3. 重复城市:

    • 两个城市坐标相同,距离为0
    • 不影响算法正确性

7. 实际应用与扩展

最小生成树在实际中有广泛应用:

  1. 网络设计:

    • 通信网络布线
    • 电网架设
    • 交通网络规划
  2. 聚类分析:

    • 基于MST的层次聚类
    • 图像分割
  3. 近似算法:

    • 旅行商问题(TSP)的近似解
    • Steiner树问题

对于本题的扩展方向:

  1. 部分城市已有连接:

    • 预处理时将这些边加入生成树
    • 剩余部分继续使用Prim
  2. 地形因素:

    • 距离计算考虑地形阻碍
    • 可能需要A*等更复杂的路径规划
  3. 多目标优化:

    • 同时考虑建设成本、环境影响等
    • 转化为多目标优化问题

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询