最小生成树算法概述
最小生成树(Minimum Spanning Tree, MST)是指在一个加权连通图中,选取一棵生成树使得所有边的权值之和最小。常见的最小生成树算法包括Prim算法和Kruskal算法。
Prim算法
Prim算法是一种贪心算法,从某个顶点开始,逐步扩展生成树,每次选择权值最小的边连接生成树和非生成树顶点。
算法步骤
- 初始化一个空的最小生成树和一个优先队列(最小堆)。
- 从任意顶点开始,将其加入生成树,并将其所有邻边加入优先队列。
- 从优先队列中取出权值最小的边,如果连接的顶点不在生成树中,则将该顶点加入生成树,并将其邻边加入优先队列。
- 重复上述步骤,直到所有顶点都加入生成树。
Go语言实现
go
package main
import (
"container/heap" // 引入Go标准库的堆结构,用于实现最小堆
"fmt"
)
// Edge 定义边的结构体
// from: 边的起点顶点编号
// to: 边的终点顶点编号
// weight: 边的权重(权值)
type Edge struct {
from, to, weight int
}
// MinHeap 定义最小堆类型,底层是Edge切片
// 用于按边的权重从小到大排序,是Prim算法的核心数据结构
type MinHeap []Edge
// Len 实现heap.Interface接口的Len方法,返回堆的长度
func (h MinHeap) Len() int { return len(h) }
// Less 实现heap.Interface接口的Less方法,定义堆的比较规则(最小堆)
// 比较两个边的权重,权重小的优先级更高
func (h MinHeap) Less(i, j int) bool { return h[i].weight < h[j].weight }
// Swap 实现heap.Interface接口的Swap方法,交换堆中两个元素的位置
func (h MinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
// Push 实现heap.Interface接口的Push方法,向堆中添加元素
// x是待添加的元素,需要断言为Edge类型
func (h *MinHeap) Push(x interface{}) {
*h = append(*h, x.(Edge)) // 将元素追加到切片末尾,堆结构由heap包自动调整
}
// Pop 实现heap.Interface接口的Pop方法,从堆中弹出元素(弹出最小权重的边)
// 按照堆的规范,需要弹出切片最后一个元素,并调整堆结构
func (h *MinHeap) Pop() interface{} {
old := *h // 保存当前堆的所有元素
n := len(old) // 获取堆的长度
x := old[n-1] // 取出最后一个元素(堆顶元素已被调整到末尾)
*h = old[0 : n-1] // 截断切片,移除最后一个元素
return x // 返回弹出的元素
}
// Prim 实现Prim算法,求解无向加权图的最小生成树(MST)
// graph: 图的邻接矩阵表示,graph[i][j]表示顶点i到顶点j的边的权重,0表示无直接边
// start: 起始顶点编号(从哪个顶点开始构建MST)
// 返回值: 构成最小生成树的所有边的切片
func Prim(graph [][]int, start int) []Edge {
n := len(graph) // 获取图的顶点总数(邻接矩阵的行数/列数)
visited := make([]bool, n) // 标记顶点是否已被加入MST,初始值都是false
mst := make([]Edge, 0) // 存储最小生成树的所有边
pq := &MinHeap{} // 初始化最小堆(优先级队列)
heap.Init(pq) // 初始化堆结构(Go标准库的heap包需要显式初始化)
// 第一步:标记起始顶点为已访问,并将起始顶点的所有邻接边加入最小堆
visited[start] = true
for to, weight := range graph[start] { // 遍历起始顶点的所有邻接顶点
if weight > 0 { // weight>0表示存在从start到to的边
heap.Push(pq, Edge{start, to, weight}) // 将这条边加入最小堆
}
}
// 第二步:循环取出堆中权重最小的边,扩展MST,直到选出n-1条边(MST的边数=顶点数-1)
// 终止条件:堆为空 或 已选够n-1条边
for pq.Len() > 0 && len(mst) < n-1 {
// 弹出堆中权重最小的边
e := heap.Pop(pq).(Edge)
// 如果这条边的终点已经被访问过,说明会形成环,跳过这条边
if visited[e.to] {
continue
}
// 否则,将这条边加入MST,并标记终点为已访问
mst = append(mst, e)
visited[e.to] = true
// 遍历当前终点的所有邻接顶点,将未访问的邻接边加入堆
for to, weight := range graph[e.to] {
// weight>0表示存在边,且to未被访问过(避免环)
if weight > 0 && !visited[to] {
heap.Push(pq, Edge{e.to, to, weight})
}
}
}
// 返回构建好的最小生成树的边集合
return mst
}
func main() {
// 定义图的邻接矩阵(5个顶点,编号0-4)
// graph[i][j] = w 表示顶点i到j的边权重为w,0表示无直接边
// 例如:graph[0][1]=2 表示顶点0到顶点1有一条权重为2的边
graph := [][]int{
{0, 2, 0, 6, 0}, // 顶点0的邻接边:0-1(2)、0-3(6)
{2, 0, 3, 8, 5}, // 顶点1的邻接边:1-0(2)、1-2(3)、1-3(8)、1-4(5)
{0, 3, 0, 0, 7}, // 顶点2的邻接边:2-1(3)、2-4(7)
{6, 8, 0, 0, 9}, // 顶点3的邻接边:3-0(6)、3-1(8)、3-4(9)
{0, 5, 7, 9, 0}, // 顶点4的邻接边:4-1(5)、4-2(7)、4-3(9)
}
// 调用Prim算法,从顶点0开始构建最小生成树
mst := Prim(graph, 0)
// 遍历输出最小生成树的所有边
for _, e := range mst {
fmt.Printf("Edge %d-%d: %d\n", e.from, e.to, e.weight)
}
}
Kruskal算法
Kruskal算法也是一种贪心算法,通过按权值从小到大排序所有边,逐步选择不形成环的边加入生成树。
算法步骤
- 将所有边按权值从小到大排序。
- 初始化一个并查集数据结构,用于检测环。
- 依次选择排序后的边,如果边的两个顶点不在同一集合中,则将该边加入生成树,并合并两个顶点所在的集合。
- 重复上述步骤,直到生成树包含所有顶点。
Go语言实现
go
package main
import (
"fmt"
"sort"
)
type Edge struct {
from, to, weight int
}
type UnionFind struct {
parent []int // parent[i]表示i的父节点
rank []int // rank[i]表示以i为根的集合的"秩"(用于按秩合并,优化效率)
}
// 初始化并查集:每个节点的父节点是自己,秩初始为0
func NewUnionFind(size int) *UnionFind {
parent := make([]int, size)
rank := make([]int, size)
for i := range parent {
parent[i] = i // 初始时,每个节点独立成集合
}
return &UnionFind{parent, rank}
}
// Find操作:查找节点u的根节点,同时做"路径压缩"(优化查询效率)
func (uf *UnionFind) Find(u int) int {
if uf.parent[u] != u { // 如果u不是根节点
uf.parent[u] = uf.Find(uf.parent[u]) // 递归找根,并把u的父节点直接指向根(路径压缩)
}
return uf.parent[u] // 返回根节点
}
// Union操作:合并u和v所在的集合(按秩合并,避免树退化成链表)
func (uf *UnionFind) Union(u, v int) {
rootU := uf.Find(u) // 找u的根
rootV := uf.Find(v) // 找v的根
if rootU == rootV { // 根相同,说明已连通,无需合并
return
}
// 按秩合并:把秩小的树合并到秩大的树下
if uf.rank[rootU] > uf.rank[rootV] {
uf.parent[rootV] = rootU
} else {
uf.parent[rootU] = rootV
if uf.rank[rootU] == uf.rank[rootV] { // 秩相同,合并后秩+1
uf.rank[rootV]++
}
}
}
func Kruskal(edges []Edge, n int) []Edge {
// 步骤1:把所有边按权重从小到大排序(贪心的核心)
sort.Slice(edges, func(i, j int) bool {
return edges[i].weight < edges[j].weight
})
// 步骤2:初始化并查集,大小为顶点数n
uf := NewUnionFind(n)
// 步骤3:存储最小生成树的边
mst := make([]Edge, 0)
// 步骤4:遍历排序后的边,依次选择不形成环的边
for _, e := range edges {
// 判断边的两个顶点是否连通(根节点是否相同)
if uf.Find(e.from) != uf.Find(e.to) {
mst = append(mst, e) // 不连通,加入MST
uf.Union(e.from, e.to) // 合并两个顶点的集合
}
}
return mst // 返回MST的所有边
}
func main() {
edges := []Edge{
{0, 1, 2},
{0, 3, 6},
{1, 2, 3},
{1, 3, 8},
{1, 4, 5},
{2, 4, 7},
{3, 4, 9},
}
mst := Kruskal(edges, 5)
for _, e := range mst {
fmt.Printf("Edge %d-%d: %d\n", e.from, e.to, e.weight)
}
}
性能比较
- Prim算法:适合稠密图,时间复杂度为O(V²)(使用邻接矩阵)或O(E log V)(使用优先队列)。
- Kruskal算法:适合稀疏图,时间复杂度为O(E log E),主要开销在于排序边。
应用场景
最小生成树算法广泛应用于网络设计(如电缆布线、通信网络)、交通规划(如道路建设)等领域。