图与网络——最小树问题精解

最小生成树(Minimum Spanning Tree, MST)问题是图论中的一个重要问题,其核心思想是:给定一个无向加权图(每条边具有权重值),通过选择若干边构建一棵包含所有顶点的生成树,并确保这些边的权重总和最小。最小生成树不仅保持了生成树的连通性和无圈性,还要求总权重最小化。在实际应用中,最小生成树问题在计算机科学和网络设计领域广泛使用,它常用于优化电路网络、建立通信网络、规划道路交通系统等,可以有效降低基础设施建设成本、提高资源利用效率,这类问题可通过经典的算法如Prim和Kruskal进行求解。

无向图 最小树

一、最小树的界定

什么是树?
树是图论中的一种特殊数据结构,是一个无圈、连通的无向图。树由顶点和边组成,满足以下特征:
无圈性:树中没有任何回路,即任意两个顶点之间有且仅有一条路径。
连通性:树是连通的,任意两个顶点之间都有一条路径相连。

什么是生成树?
生成树是指从一个连通无向图中选择若干条边,构成一个包含图中所有顶点且无圈的子图。生成树满足以下条件:
覆盖所有顶点:生成树中的顶点与原图相同,包含原图的所有顶点。
无圈性:生成树中没有任何环,满足树的定义。
边的数量:生成树的边数是\(n-1\)\(n\)是顶点的数量。
对于一个连通图,可能存在多个不同的生成树,每棵生成树都符合上述定义。

树非生成树 生成树或支撑树

树模型具有下面重要性质:
\(T=(V, E)\) 是一个 \(|V|\geqslant 3\) 的无向图,则下列关于树的命题是等价的:
\(T\) 连通且无圈;
\(T\) 的任何两个顶点间均必有一条且仅有一条通路相连;
\(T\) 连通且有 \(n-1\) 条边,这里 \(n=|V|\)
\(T\)\(n-1\) 条边且无圈,这里 \(n=|V|\)
\(T\) 无圈,但在 \(T\) 中任意两个不相邻的顶点间添加一条边,就可构成一个圈;
\(T\) 连通,但去掉任意一条边后就不连通,即树 \(T\) 是连通且边数最少的图。

什么是最小生成树(最小树)?
最小生成树是生成树的一种特殊形式,定义为:在一个加权无向图中,所有生成树中边权和最小的那棵生成树。它不仅符合生成树的所有条件,还要求所选边的权重总和最小。若\(G=(V, E, W)\) 是一个连通的赋权图,\(T=(V, S)\)\(G\) 的支撑树,把 \(T\) 中所有边的权之和称作树 \(T\) 的权,记作 \(w(T)\),即:$$w(T) = \sum_{e\in S} w(e)$$,\(G\) 中权最小的支撑树 \(T\) 称为 \(G\) 的最小支撑树,简称最小树。
在现实生活中,比如在城市之间建立输电网络、电话线网或光纤网络时,我们希望找到一种最优的布线方案,使得总长度或总费用最小化。这类问题可以被转化为赋权图的最小树问题。

最小生成树具有特点:
连通性:最小生成树依然是连通的,能够覆盖图的所有顶点。
无圈性:最小生成树没有回路,符合树的基本属性。
最小权重和:最小生成树的边权重总和在所有可能的生成树中最小。
最小生成树在网络优化、路径规划、通信设计等领域具有重要应用,常用于降低成本、提高效率。

二、最小树问题的数学模型

2.1 问题描述

给定一个无向连通图 \(G=(V,E)\)

  • \(V\):顶点集合,\(|V|=n\)
  • \(E\):边集合,\(|E|=m\)
  • 每条边 \((i,j)\in E\) 有一个非负权重(或距离、费用)\(c_{ij}\)

目标
\(E\) 中选择 \(n-1\) 条边,使得

  1. 所选边连接所有节点(形成树);
  2. 总权重最小。

2.2 决策变量

定义二进制变量

\[x_{ij}= \begin{cases} 1, & \text{若边 }(i,j)\text{ 被选入最小生成树}\\[2pt] 0, & \text{否则} \end{cases} \quad \forall (i,j)\in E \]

2.3 目标函数

最小化选边总权重

\[\min \sum_{(i,j)\in E} c_{ij}\,x_{ij} \]

2.4 约束条件

生成树边数约束

树中必须恰好有 \(n-1\) 条边

\[\sum_{(i,j)\in E} x_{ij} = n-1 \]

连通性与无环约束(割约束)

对任意非空真子集 \(S\subset V\)

\[\sum_{\substack{i\in S\\j\in V\setminus S}} x_{ij}\geq 1,\qquad \forall S\subset V,\; S\neq\varnothing,\;S\neq V \]

防止回路(子环消除)

对任意子集 \(S\subset V\)

\[\sum_{\substack{i\in S\\j\in S}} x_{ij}\leq |S|-1,\qquad \forall S\subset V,\;2\leq |S|\leq n-1 \]

变量取值

\[x_{ij}\in\{0,1\},\quad \forall (i,j)\in E \]

2.5 完整模型

\[\begin{aligned} \min\quad & \sum_{(i,j)\in E} c_{ij}\,x_{ij} \\[4pt] \text{s.t.}\quad & \sum_{(i,j)\in E} x_{ij}=n-1 \\[4pt] & \sum_{\substack{i\in S\\j\in S}} x_{ij}\leq |S|-1,\qquad \forall S\subset V,\;2\leq |S|\leq n-1 \\[4pt] & x_{ij}\in\{0,1\},\qquad \forall (i,j)\in E \end{aligned} \]


三、最小树算法的Python求解

根据最小树的最小权要求,所以大致构成这颗树的边权也应该小一些,这样就获得最小生成树的多种求解方法。例如手工计算的破圈法和避圈法,软件程序最经典的两种是Kruskal算法和Prim算法。

3.1 Kruskal算法

Kruskal算法同样基于贪心思想,但与Prim算法不同的是,它是基于边的排序进行操作的。Kruskal算法先将图中的所有边按权重从小到大排序,然后依次选择这些边,确保选择的边不会形成圈。算法适合处理稀疏图。Kruskal算法步骤:

将图中的所有边按权重升序排列。
依次选择权重最小的边,判断该边是否会形成环。如果不会形成环,则将该边加入生成树。
重复上面步骤,直到生成树包含 \(n-1\)条边为止。

例1:给定无向图\(G(n,m)\)表明图G有\(n\)个顶点,\(m\)条边,通过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()

3.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()

例3:某地有10个城镇,它们之间的距离见表1。城镇1处有一条河流,现需要从各城镇之间铺设管道,使城镇1处的水可以输送到各城镇,求铺设管道最少的设计方式。

地区 1 2 3 4 5 6 7 8 9 10
1 0 8 5 9 12 14 12 16 17 22
2 8 0 9 15 16 8 11 18 14 22
3 5 9 0 7 9 11 7 12 12 17
4 9 15 7 0 3 17 10 7 15 15
5 12 16 9 3 0 8 10 6 15 15
6 14 8 11 17 8 0 9 14 8 16
7 12 11 7 10 10 9 0 8 6 11
8 16 18 12 7 6 14 8 0 11 11
9 17 14 12 25 15 8 6 11 0 10
10 22 22 17 15 15 16 11 11 10 0
import networkx as nx
import matplotlib.pyplot as plt

# 创建一个空的无向图
G = nx.Graph()

# 添加边和对应的权重
edges = [
    (1, 2, 8), (1, 3, 5), (1, 4, 9), (1, 5, 12), (1, 6, 14), (1, 7, 12), (1, 8, 16), (1, 9, 17), (1, 10, 22),
    (2, 3, 9), (2, 4, 15), (2, 5, 16), (2, 6, 8), (2, 7, 11), (2, 8, 18), (2, 9, 14), (2, 10, 22),
    (3, 4, 7), (3, 5, 9), (3, 6, 11), (3, 7, 7), (3, 8, 12), (3, 9, 12), (3, 10, 17),
    (4, 5, 3), (4, 6, 17), (4, 7, 10), (4, 8, 7), (4, 9, 15), (4, 10, 15),
    (5, 6, 8), (5, 7, 10), (5, 8, 6), (5, 9, 15), (5, 10, 15),
    (6, 7, 9), (6, 8, 14), (6, 9, 8), (6, 10, 16),
    (7, 8, 8), (7, 9, 6), (7, 10, 11),
    (8, 9, 11), (8, 10, 11),
    (9, 10, 10)
]

G.add_weighted_edges_from(edges)

# 使用 spring_layout 确保两个图布局一致
pos = nx.spring_layout(G, seed=42)

# 构造最小生成树
mst = nx.minimum_spanning_tree(G, weight='weight')

# 计算最小生成树总权重
total_weight = sum(mst[u][v]['weight'] for u, v in mst.edges())
print("Total weight of the Minimum Spanning Tree:", total_weight)

# 创建两个子图画布
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

# 绘制原始图
nx.draw(G, pos, with_labels=True, ax=ax1, node_color='lightgreen', edge_color='gray', node_size=700, font_size=10, font_weight='bold')
nx.draw_networkx_edge_labels(G, pos, edge_labels=nx.get_edge_attributes(G, 'weight'), ax=ax1)
ax1.set_title("Original Network Graph")

# 绘制最小生成树图
nx.draw(mst, pos, with_labels=True, ax=ax2, node_color='lightblue', edge_color='black', node_size=700, font_size=10, font_weight='bold')
nx.draw_networkx_edge_labels(mst, pos, edge_labels=nx.get_edge_attributes(mst, 'weight'), ax=ax2)
ax2.set_title("Minimum Spanning Tree")

plt.tight_layout()
plt.show()

总结

最小生成树问题是图论中重要的优化问题,它要求在给定的无向图中找到一棵边权和最小的生成树。生成树具有连通性和无环性,而最小生成树则在此基础上还要求边的权重和最小。通过Prim算法和Kruskal算法,我们可以高效地解决最小生成树问题。Prim算法适用于稠密图,而Kruskal算法则更适合稀疏图。在实际应用中,最小生成树被广泛用于网络设计、通信系统规划、输电线路布局等领域。在实际工程中,最小生成树的求解不仅能降低系统的成本,还能提高资源的利用效率。通过使用适当的算法和数据结构,我们能够快速且准确地求解最小生成树问题。

参考资料

  1. 算法设计分析(Kruskal构造最小生成树)
  2. 最小生成树(最小支撑树)算法
posted @ 2024-09-10 23:01  郝hai  阅读(692)  评论(0)    收藏  举报