图与网络——最小树问题精解
最小生成树(Minimum Spanning Tree, MST)问题是图论中的一个重要问题,其核心思想是:给定一个无向加权图(每条边具有权重值),通过选择若干边构建一棵包含所有顶点的生成树,并确保这些边的权重总和最小。最小生成树不仅保持了生成树的连通性和无圈性,还要求总权重最小化。在实际应用中,最小生成树问题在计算机科学和网络设计领域广泛使用,它常用于优化电路网络、建立通信网络、规划道路交通系统等,可以有效降低基础设施建设成本、提高资源利用效率,这类问题可通过经典的算法如Prim和Kruskal进行求解。
无向图 | 最小树 |
---|---|
![]() |
![]() |
一、最小树的界定描述
什么是树?
树是图论中的一种特殊数据结构,是一个无圈、连通的无向图。树由顶点和边组成,满足以下特征:
无圈性:树中没有任何回路,即任意两个顶点之间有且仅有一条路径。
连通性:树是连通的,任意两个顶点之间都有一条路径相连。
什么是生成树?
生成树是指从一个连通无向图中选择若干条边,构成一个包含图中所有顶点且无圈的子图。生成树满足以下条件:
覆盖所有顶点:生成树中的顶点与原图相同,包含原图的所有顶点。
无圈性:生成树中没有任何环,满足树的定义。
边的数量:生成树的边数是,是顶点的数量。
对于一个连通图,可能存在多个不同的生成树,每棵生成树都符合上述定义。
图 | 树非生成树 | 生成树或支撑树 |
---|---|---|
![]() |
![]() |
![]() |
树模型具有下面重要性质:
设 是一个 的无向图,则下列关于树的命题是等价的:
连通且无圈;
的任何两个顶点间均必有一条且仅有一条通路相连;
连通且有 条边,这里 ;
有 条边且无圈,这里 ;
无圈,但在 中任意两个不相邻的顶点间添加一条边,就可构成一个圈;
连通,但去掉任意一条边后就不连通,即树 是连通且边数最少的图。
什么是最小生成树(最小树)?
最小生成树是生成树的一种特殊形式,定义为:在一个加权无向图中,所有生成树中边权和最小的那棵生成树。它不仅符合生成树的所有条件,还要求所选边的权重总和最小。若 是一个连通的赋权图, 是 的支撑树,把 中所有边的权之和称作树 的权,记作 ,即:, 中权最小的支撑树 称为 的最小支撑树,简称最小树。
在现实生活中,比如在城市之间建立输电网络、电话线网或光纤网络时,我们希望找到一种最优的布线方案,使得总长度或总费用最小化。这类问题可以被转化为赋权图的最小树问题。
最小生成树具有特点:
连通性:最小生成树依然是连通的,能够覆盖图的所有顶点。
无圈性:最小生成树没有回路,符合树的基本属性。
最小权重和:最小生成树的边权重总和在所有可能的生成树中最小。
最小生成树在网络优化、路径规划、通信设计等领域具有重要应用,常用于降低成本、提高效率。
二、最小树算法的Python求解
根据最小树的最小权要求,所以大致构成这颗树的边权也应该小一些,这样就获得最小生成树的多种求解方法。例如手工计算的破圈法和避圈法,软件程序最经典的两种是Kruskal算法和Prim算法。
2.1 Kruskal算法
Kruskal算法同样基于贪心思想,但与Prim算法不同的是,它是基于边的排序进行操作的。Kruskal算法先将图中的所有边按权重从小到大排序,然后依次选择这些边,确保选择的边不会形成圈。算法适合处理稀疏图。Kruskal算法步骤:
将图中的所有边按权重升序排列。
依次选择权重最小的边,判断该边是否会形成环。如果不会形成环,则将该边加入生成树。
重复上面步骤,直到生成树包含 条边为止。
例1:给定无向图表明图G有个顶点,条边,通过Kruskal算法构造一个最小生成树。
import networkx as nx
import matplotlib.pyplot as plt
# 设置中文字体,确保Matplotlib支持中文
plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体为黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 创建图
G = nx.Graph()
# 添加节点
nodes = ['V1', 'V2', 'V3', 'V4', 'V5', 'V6']
G.add_nodes_from(nodes)
# 添加边和权重
edges = [
('V1', 'V5', 1),
('V1', 'V6', 2),
('V1', 'V2', 3),
('V2', 'V6', 4),
('V2', 'V3', 5),
('V3', 'V6', 6),
('V3', 'V4', 7),
('V4', 'V6', 8),
('V4', 'V5', 9)
]
G.add_weighted_edges_from(edges)
# Kruskal最小生成树的边
mst_edges = [('V1', 'V5'), ('V1', 'V6'), ('V1', 'V2'), ('V2', 'V3'), ('V3', 'V4')]
# 手动设置每个节点的位置,确保V6在中间,其他顶点围绕它
pos = {
'V6': (0, 0), # V6在中间
'V1': (0, 1), # V1在上方
'V2': (1, 0.5), # V2在右上方
'V3': (1, -0.5), # V3在右下方
'V4': (0, -1), # V4在下方
'V5': (-1, 0) # V5在左侧
}
# 绘制图形
plt.figure(figsize=(8, 6))
# 绘制所有边,调整线条粗细和节点大小,设置节点标签的字体大小为15
nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=3000, font_size=45, font_weight='bold', edge_color='gray', width=6) # 字体和边线粗细增加三倍
nx.draw_networkx_edge_labels(G, pos, edge_labels={(u, v): f'{d}' for u, v, d in edges}, font_size=30) # 边权重的字体大小为默认的三倍
# 用红色标出最小生成树的边,线宽设置为原来的三倍
nx.draw_networkx_edges(G, pos, edgelist=mst_edges, width=6, edge_color='red') # 最小生成树的边线宽度为原来的三倍
# 设置标题,字体大小增大三倍
plt.title("最小生成树", fontsize=60)
# 显示图形
plt.show()
2.2 Prim算法
Prim算法是一种基于贪心思想的算法,适合处理稠密图。算法从一个起始顶点开始,将其加入生成树,然后不断选择与当前生成树相连的权重最小的边,将该边所连接的顶点加入生成树,直到所有顶点都被包括在内为止。Prim算法步骤:
从任意一个顶点出发,初始时将该顶点加入生成树。
查找与已加入生成树的顶点相邻的最小权重边,将该边及其相邻的顶点加入生成树。
重复上述步骤,直到所有顶点都包含在生成树中。
例2:某地的5个城镇需要铺设天然气管道,下图中给出了5个城镇之间可以铺设管道的情况的以及距离,先求一个设计方案,用最短的管道将5个城镇连接起来。
原图 | 迭代1 | 迭代5 |
---|---|---|
![]() |
![]() |
![]() |
import networkx as nx
import matplotlib.pyplot as plt
import heapq
# 设置中文字体,确保Matplotlib支持中文
plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体为黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 创建图
G = nx.Graph()
# 添加节点
nodes = ['V1', 'V2', 'V3', 'V4', 'V5', 'V6']
G.add_nodes_from(nodes)
# 添加边和权重
edges = [
('V1', 'V2', 1),
('V1', 'V4', 7),
('V1', 'V3', 3),
('V1', 'V5', 9),
('V2', 'V3', 6),
('V2', 'V4', 4),
('V2', 'V5', 3),
('V3', 'V4', 5),
('V3', 'V6', 10),
('V4', 'V5', 8),
('V4', 'V6', 3)
]
G.add_weighted_edges_from(edges)
# 修改邻接表,基于edges
graph = {
'V1': {'V2': 1, 'V3': 3, 'V4': 7, 'V5': 9},
'V2': {'V1': 1, 'V3': 6, 'V4': 4, 'V5': 3},
'V3': {'V1': 3, 'V2': 6, 'V4': 5, 'V6': 10},
'V4': {'V1': 7, 'V2': 4, 'V3': 5, 'V5': 8, 'V6': 3},
'V5': {'V1': 9, 'V2': 3, 'V4': 8},
'V6': {'V3': 10, 'V4': 3}
}
# Prim算法实现
def prim_mst(graph, start):
mst = [] # 存储最小生成树的边
visited = set([start]) # 记录已经访问过的节点
edges = [(weight, start, to) for to, weight in graph[start].items()]
heapq.heapify(edges) # 将边加入优先队列
while edges:
weight, frm, to = heapq.heappop(edges)
if to not in visited:
visited.add(to)
mst.append((frm, to))
for next_to, next_weight in graph[to].items():
if next_to not in visited:
heapq.heappush(edges, (next_weight, to, next_to))
return mst
# 使用Prim算法求最小生成树
mst_edges = prim_mst(graph, 'V1')
# 定义节点的位置布局,V3在中间
pos = {
'V3': (0, 0), # V3在中间
'V1': (0, 1), # V1在上方
'V2': (1, 0.5), # V2在右上方
'V4': (0, -1), # V4在下方
'V5': (-1, 0), # V5在左侧
'V6': (1, -0.5) # V6在右下方
}
# 调整字体大小和边线粗细
plt.figure(figsize=(8, 6))
nx.draw(G, pos, with_labels=True, node_color='lightblue', node_size=3000, font_size=36, font_weight='bold', edge_color='gray', width=6) # 调整font_size和width
nx.draw_networkx_edge_labels(G, pos, edge_labels={(u, v): f'{d}' for u, v, d in edges}, font_size=30) # 边权重的字体大小调大
# 用红色标出最小生成树的边
nx.draw_networkx_edges(G, pos, edgelist=mst_edges, width=6, edge_color='red') # 边线粗三倍
# 设置标题
plt.title("Prim算法求解最小生成树", fontsize=36) # 标题字体调整
# 显示图形
plt.show()
总结
最小生成树问题是图论中重要的优化问题,它要求在给定的无向图中找到一棵边权和最小的生成树。生成树具有连通性和无环性,而最小生成树则在此基础上还要求边的权重和最小。通过Prim算法和Kruskal算法,我们可以高效地解决最小生成树问题。Prim算法适用于稠密图,而Kruskal算法则更适合稀疏图。在实际应用中,最小生成树被广泛用于网络设计、通信系统规划、输电线路布局等领域。在实际工程中,最小生成树的求解不仅能降低系统的成本,还能提高资源的利用效率。通过使用适当的算法和数据结构,我们能够快速且准确地求解最小生成树问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!