最小生成树算法(Go)

最小生成树算法概述

最小生成树(Minimum Spanning Tree, MST)是指在一个加权连通图中,选取一棵生成树使得所有边的权值之和最小。常见的最小生成树算法包括Prim算法和Kruskal算法。

Prim算法

Prim算法是一种贪心算法,从某个顶点开始,逐步扩展生成树,每次选择权值最小的边连接生成树和非生成树顶点。

算法步骤
  1. 初始化一个空的最小生成树和一个优先队列(最小堆)。
  2. 从任意顶点开始,将其加入生成树,并将其所有邻边加入优先队列。
  3. 从优先队列中取出权值最小的边,如果连接的顶点不在生成树中,则将该顶点加入生成树,并将其邻边加入优先队列。
  4. 重复上述步骤,直到所有顶点都加入生成树。
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算法也是一种贪心算法,通过按权值从小到大排序所有边,逐步选择不形成环的边加入生成树。

算法步骤
  1. 将所有边按权值从小到大排序。
  2. 初始化一个并查集数据结构,用于检测环。
  3. 依次选择排序后的边,如果边的两个顶点不在同一集合中,则将该边加入生成树,并合并两个顶点所在的集合。
  4. 重复上述步骤,直到生成树包含所有顶点。
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),主要开销在于排序边。

应用场景

最小生成树算法广泛应用于网络设计(如电缆布线、通信网络)、交通规划(如道路建设)等领域。

相关推荐
添尹3 小时前
Go语言基础之数组
后端·golang
liurunlin8884 小时前
Go环境搭建(vscode调试)
开发语言·vscode·golang
添尹7 小时前
Go语言基础之流程控制
golang
添尹7 小时前
Go语言基础之基本数据类型
开发语言·后端·golang
lars_lhuan8 小时前
Go Cond 源码解析
golang
F1FJJ8 小时前
我用一条命令把内网的 RDP 桌面开到了浏览器里 —— Shield CLI 与主流隧道工具的技术对比
网络·golang
lars_lhuan11 小时前
Go map 与并发
后端·golang
Lewiis11 小时前
Go语言的错误处理机制
开发语言·后端·golang
benzun_yinzi12 小时前
go升级之后找不到goroot解决办法
golang