尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

图论最小生成树(MST):Kruskal 算法与 Prim 算法的贪心本质与工程选型

图论最小生成树(MST):Kruskal 算法与 Prim 算法的贪心本质与工程选型 图论最小生成树MSTKruskal 算法与 Prim 算法的贪心本质与工程选型在图论算法与网络拓扑规划中“最小生成树Minimum Spanning TreeMST”是解决在保证图内所有顶点完全连通的前提下使得所选边的权重之和达到最小的经典问题。典型工业应用包括分布式集群机房之间的低成本光纤布线拓扑设计微服务跨机房网络广播树构建电路芯片布线与图像分割聚类。解决 MST 最著名的两大贪心算法分别是Kruskal 算法克鲁斯卡尔算法与Prim 算法普里姆算法。虽然两者都基于贪心选择性质Greedy-Choice Property与割边定理Cut Property但一个立足于“按边的权重全局排序”另一个立足于“从顶点的局部生长”。今天我们系统拆解这两种算法的数学本质、代码模板与工程选型权衡。割性质Cut Property最小生成树贪心正确性的终极数学基石在证明 MST 贪心算法的正确性时割性质是最核心的定理对于图 $G (V, E)$ 的任意一个割 $(S, V \setminus S)$横跨这个割的所有边中权重最小的那条轻量边Light Edge必然属于图的某棵最小生成树Kruskal 与 Prim 正是从不同角度不断寻找并加入这样的轻量边。graph TD subgraph Kruskal 算法: 边的全局贪心视角 A1[将全图所有边按权重从小到大严格排序] -- B1[依次遍历每条边 (u, v)] B1 -- C1{通过并查集检查 u 和 v 是否已连通?} C1 --|未连通| D1[将边加入生成树, 并查集 union(u, v), 计数器 count] C1 --|已连通| E1[丢弃该边 (防成环)] D1 -- F1{已选够 V-1 条边?} end subgraph Prim 算法: 顶点的局部生长视角 A2[选定任意起点加入已访问集合 S] -- B2[将与集合 S 相邻的所有割边放入最小堆] B2 -- C2[弹出当前最短的割边 (u, v)] C2 -- D2{顶点 v 是否已在集合 S 中?} D2 --|否| E2[将 v 纳入集合 S, 累加边权, 将 v 的新邻边推入堆] D2 --|是| C2 end一、Kruskal 算法边贪心 并查集稀疏图的绝对首选Kruskal 算法的逻辑极其清晰纯粹将图中的所有边按照权重 $w$ 从小到大排序初始时所有顶点自成独立的集合从小到大遍历排序后的边列表使用并查集Union-Find检查当前边的两个端点 $u$ 和 $v$ 是否属于同一个集合若不属于同一个集合union(u, v) true说明加入该边绝对不会成环将该边纳入最小生成树并累加权重若已在同一集合说明加入该边会构成冗余环路直接丢弃当成功选入了 $V - 1$ 条边时最小生成树构建完毕工业级 Java 代码实现LeetCode 1584 连接所有点的最小费用import java.util.*; public class KruskalMstSolution { // 边结构体 static class Edge implements ComparableEdge { int u, v, weight; public Edge(int u, int v, int weight) { this.u u; this.v v; this.weight weight; } Override public int compareTo(Edge o) { return Integer.compare(this.weight, o.weight); } } public int minCostConnectPoints(int[][] points) { int n points.length; ListEdge edges new ArrayList(); // 1. 构造所有点对之间的边 (曼哈顿距离) for (int i 0; i n; i) { for (int j i 1; j n; j) { int dist Math.abs(points[i][0] - points[j][0]) Math.abs(points[i][1] - points[j][1]); edges.add(new Edge(i, j, dist)); } } // 2. 将所有边按权重从小到大排序: O(E log E) Collections.sort(edges); // 3. 并查集贪心合并 UnionFind uf new UnionFind(n); int totalWeight 0; int edgeCount 0; for (Edge edge : edges) { if (uf.union(edge.u, edge.v)) { totalWeight edge.weight; edgeCount; if (edgeCount n - 1) { break; // 已经选满 n - 1 条边提前结束 } } } return edgeCount n - 1 ? totalWeight : -1; } }二、Prim 算法点生长 优先队列稠密图与矩阵图的利器与 Kruskal 面向边不同Prim 算法从某一个起始顶点出发像滚雪球一样不断向外“生长”维护一个集合 $S$初始时将顶点 0 放入 $S$维护一个最小堆PriorityQueue存储所有从集合 $S$ 伸向集合外部 $V \setminus S$ 的割边每次从堆中取出权重最小的边 $(u, v)$若顶点 $v$ 已经在集合 $S$ 中说明是内部边直接跳过否则将 $v$ 纳入集合 $S$累加边权并将顶点 $v$ 引出的所有连接到外部的新边全部推入堆中重复直到所有 $N$ 个顶点全部被纳入集合 $S$。public int minCostConnectPointsPrim(int[][] points) { int n points.length; boolean[] visited new boolean[n]; // 优先队列存 int[]{targetNode, weight} PriorityQueueint[] pq new PriorityQueue(Comparator.comparingInt(a - a[1])); pq.offer(new int[]{0, 0}); int totalWeight 0; int visitedCount 0; while (!pq.isEmpty() visitedCount n) { int[] curr pq.poll(); int u curr[0]; int w curr[1]; if (visited[u]) continue; // 已在集合中跳过 visited[u] true; totalWeight w; visitedCount; // 将与 u 相邻的未访问节点加入优先队列 for (int v 0; v n; v) { if (!visited[v]) { int dist Math.abs(points[u][0] - points[v][0]) Math.abs(points[u][1] - points[v][1]); pq.offer(new int[]{v, dist}); } } } return visitedCount n ? totalWeight : -1; }两大算法的复杂度与工程选型决策矩阵评估维度Kruskal 算法Prim 算法堆优化Prim 算法邻接矩阵朴素版算法操作主体边Edges顶点与割边Vertices Cut Edges顶点Vertices辅助数据结构边数组排序 并查集优先队列Min-Heap visited数组一维数组minDist[]时间复杂度$O(E \log E)$$O(E \log V)$$O(V^2)$空间复杂度$O(E)$$O(V E)$$O(V)$最佳适用场景稀疏图Sparse Graph$E \ll V^2$中等稠密图完全图 / 超稠密图Dense Graph$E \approx V^2$总结在实际工程开发与算法面试中如果图是以**边列表Edge List**形式给出或者图比较稀疏如交通公路网、网络拓扑Kruskal 算法配合并查集不仅代码极其好写运行速度也极具优势如果图是完全图如 LeetCode 1584 任意两点均有边连通使用朴素版 $O(V^2)$ 的 Prim 算法甚至比堆优化版更快免去了昂贵的堆操作与边对象创建。深刻理解割性质与数据结构选型最小生成树的各类变种题便能迎刃而解。
返回列表