最小生成树算法(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),主要开销在于排序边。

应用场景

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

相关推荐
microrain14 小时前
读数是 1008,用电量是 8:SagooIoT 计算服务的增量口径与七种计算策略
物联网·golang·计算服务·sagooiot
看浪的路人15 小时前
第7讲:大模型后端服务化——Go 实现高并发 LLM Proxy / Router / Fallback
开发语言·后端·golang
灯澜忆梦18 小时前
【Go Zap 日志库】#1 |快速上手 + 配置详解
开发语言·后端·golang
不是万能的豆学长21 小时前
排查Go服务40天后的内存泄漏问题
开发语言·后端·golang·go
tachibana221 小时前
Go 语言 Map 的底层实现原理
开发语言·后端·golang·make
tachibana21 天前
Go语言里slice是怎么扩容的?
后端·golang·go·切片
西瓜很甜哟1 天前
golang实现大文件分片上传
后端·golang·html
tachibana22 天前
使用for range 的时候,它的地址会发生变化吗?
开发语言·后端·golang·go·语法
王中阳Go2 天前
Go 后端转 AI:非科班怎么补?一张 3 列路线表(可保存)
开发语言·人工智能·后端·python·golang·大语言模型
for_ever_love__2 天前
LangChain Agentic RAG:让 Agent 自己决定要不要检索、检索几次
人工智能·golang·langchain·rag·langgraph·agentic·自适应检索