最短路径问题
概念性质
单源最短路径:从图中某个顶点出发,到达另一个顶点所经过的边的权重之和最小的一条路径
图论中的图,边的权重可以是路径距离,也可以是时间、费用等
图中边的长度和形状(直线、曲线)与实际无关
基本性质:最短路径上的任一子路径也是最短路径
Dijkstra算法(迪克斯特拉算法)
迪克斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
贪心算法(greedy algorithm):是指在对问题求解时,总是做出在当前看来是最好的选择。 也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解 。
例题
计算从A到其他节点的最短路径
import heapq
# 创建图
graph = {
'A': {'B': 6, 'C': 1},
'B': {'A': 6, 'C': 5, 'E': 3},
'C': {'A': 1, 'B': 5, 'D': 2, 'E': 2},
'D': {'C': 2, 'E': 4},
'E': {'B': 3, 'C': 2, 'D': 4}
}
def dijkstra(graph, start):
# 初始化距离字典,所有节点的距离都设为无穷大
distances = {node: float('infinity') for node in graph}
distances[start] = 0 # 起点到起点的距离为0
# 初始化优先队列,按照距离排序
priority_queue = [(0, start)]
while priority_queue:
# 从优先队列中取出距离最小的节点
current_distance, current_node = heapq.heappop(priority_queue)
# 如果这个节点的距离已经不是最短,则跳过
if current_distance > distances[current_node]:
continue
# 遍历当前节点的邻接节点
for neighbor, weight in graph[current_node].items():
distance = current_distance + weight
# 如果找到更短的路径,则更新距离字典,并将这个节点加入优先队列
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
return distances
# 执行迪克斯特拉算法
shortest_paths = dijkstra(graph, 'A')
# 输出从A到其他节点的最短路径
print(shortest_paths)
- 初始化:创建一个字典来存储每个节点到起始节点A的距离,除了A到自身的距离为0,其他节点的距离都初始化为无穷大。
- 创建一个集合来存储所有未访问的节点。
- 当未访问的节点集合非空时,执行以下步骤:
- 从未访问节点集合中找出距离A最近的节点,记为当前节点。
- 遍历当前节点的所有邻接节点,计算通过当前节点到达邻接节点的距离。如果这个距离比已知的距离短,则更新这个邻接节点的距离。
- 将当前节点从未访问集合中移除。
运行结果:{'A': 0, 'B': 6, 'C': 1, 'D': 3, 'E': 3}
Floyd算法(弗洛伊德算法)
Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径算法。
首先,我们要构造一个距离矩阵 D 。
(1)第 i 行第 j 列的数字代表结点 i 到结点 j 的直达距离。即 D [ i ] [ j ]。
(2) D [ i ] [ j ] = ∞ 表示的是结点 i 没有直达结点 j的路径。
(3)出现 D [ i ] [ j ] = 0 的情况都是在对角线上,即 i = j。
假设现在有四个顶点1、2、3、4.构成的矩阵如下:
第一次迭代,选中顶点K=1.
D[2][1]+D[1][3]=∞,与D[2][3]相等,不更新。
D[2][1]+D[1][4]=∞+7=∞ > D[2][4],不更新。
D[3][1]+D[1][2]=1+3=4 < D[3][2]=∞,所以更新D[3][2]为4.
D[3][1]+D[1][4]=1+7=8 < D[3][4]=∞,所以更新D[3][4]为8.
D[4][1]+D[1][2]=∞+3=∞.不更新
D[4][1]+D[1][3]=∞.不更新
更新后为下图:
以此类推,选中顶点2、3、4,更新下去。
代码
def floyd_warshall(graph):
"""
计算图中所有顶点对之间的最短路径。
:param graph: 邻接矩阵表示的图,graph[i][j]表示顶点i到顶点j的权重,如果i和j不直接相连,则graph[i][j]为float('inf')
:return: 更新后的邻接矩阵,包含所有顶点对之间的最短路径长度
"""
# 获取顶点的数量
V = len(graph)
# 初始化最短路径矩阵
# 由于是引用类型,这里直接在原矩阵上进行修改
for i in range(V):
for j in range(V):
if graph[i][j] == 0 and i != j:
graph[i][j] = float('inf')
# 更新最短路径
for k in range(V):
for i in range(V):
for j in range(V):
graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j])
return graph
# 示例图的邻接矩阵
graph = [
[0, 3, float('inf'), 7],
[float('inf'), 0, 4, 2],
[1, float('inf'), 0, float('inf')],
[float('inf'), float('inf'), 3, 0]
]
# 计算最短路径
shortest_paths = floyd_warshall(graph)
# 打印结果
for row in shortest_paths:
print(row)
运行结果
最小生成树问题
基本概念
连通图(Connected Graph):指无向图中任意两个顶点之间都有路径相连。也就是说,在一个连通图中,任意两个顶点之间都可以通过图中的边相连而相互到达。下图即是一个连通图。
树(Tree):一种无向无环连通图,它由n个节点和n-1条边组成。树具有很多重要的性质,比如任意两个节点之间都有唯一的路径、树中任意两个节点之间的路径长度唯一等等。
生成树(Spanning Tree):指一个连通图的一棵包含所有顶点的树,它是由原图的所有顶点和边所组成的子图,且这些边构成一个树。一个图,可能有多个生成树。
最小生成树(Minimum Spanning Tree,MST):指一个连通无向图的一棵生成树,使得所有生成树边的 权值之和最小。一个图的所有生成树中,可能有多个最小生成树。
最短路径是指定起点终点,两点间最短路径。最小生成树是全局最短,两两连通,总路径最短。
kruskal(克鲁斯卡尔)算法
Kruskal算法(适合点多边少的图): 1、把图G中所有边全部去掉,得到所有单独的顶点V构成的图T。
2、从图G中取出当前权值最小的边,如果该边加入T的边集合后T不形成回路,则加入T,否则舍弃。
3、重复第二步,直到T中有n-1条边(n是定点数) 第二步若遇到两条权值相同的最小权值边,任选一条即可,所以最小生成树可能不唯一 。
例题
A国有六个城市之间一直没有通信线路,现在国家想使这六个城市通信连通
六个城市,相互之间能够建设通信线缆的线路路径距离已测
要求以最小的成本建设通信线路,使得这六个城市之间能够互相通信
即从任意顶点出发,都可以到达其他顶点,且线缆总长度最小。
最小线缆长度为:10+12+13+14+14=63
import matplotlib.pyplot as plt
import networkx as nx
import random
# 生成一个随机的图
def generate_random_graph(num_vertices, num_edges):
G = nx.Graph()
G.add_nodes_from(range(num_vertices))
# 随机生成边
for _ in range(num_edges):
while True:
u, v = random.sample(range(num_vertices), 2)
weight = random.randint(1, 10)
if not G.has_edge(u, v):
G.add_edge(u, v, weight=weight)
break
return G
# 应用克鲁斯卡尔算法
def kruskal(G):
edges = G.edges(data=True)
vertices = G.nodes()
min_spanning_tree = nx.Graph()
# 按权重排序边
edges_sorted = sorted(edges, key=lambda x: x[2]['weight'])
# 初始化父节点
parent = {v: v for v in vertices}
# 查找函数
def find(v):
if parent[v] != v:
parent[v] = find(parent[v])
return parent[v]
# 合并函数
def union(v1, v2):
root1 = find(v1)
root2 = find(v2)
parent[root2] = root1
# 添加边到最小生成树
for u, v, data in edges_sorted:
if find(u) != find(v):
min_spanning_tree.add_edge(u, v, weight=data['weight'])
union(u, v)
return min_spanning_tree
# 生成图
num_vertices = 7
num_edges = 15
G = generate_random_graph(num_vertices, num_edges)
# 应用克鲁斯卡尔算法
min_spanning_tree = kruskal(G)
# 绘制原图
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
pos = nx.spring_layout(G)
nx.draw(G, pos, with_labels=True, node_color='lightblue', edge_color='gray')
labels = nx.get_edge_attributes(G, 'weight')
nx.draw_networkx_edge_labels(G, pos, edge_labels=labels)
plt.title('Original Graph')
# 绘制最小生成树
plt.subplot(1, 2, 2)
pos_mst = nx.spring_layout(min_spanning_tree)
nx.draw(min_spanning_tree, pos_mst, with_labels=True, node_color='lightgreen', edge_color='red')
labels_mst = nx.get_edge_attributes(min_spanning_tree, 'weight')
nx.draw_networkx_edge_labels(min_spanning_tree, pos_mst, edge_labels=labels_mst)
plt.title('Minimum Spanning Tree (Kruskal)')
plt.tight_layout()
plt.show()
左边图为原图,右边为结果生成的最小生成树图。
Prim (普里姆算法)算法
Prim算法(适合边多点少的图):1、设一空图U,首先将图G中任意一顶点取出加入U中 。
2、从图U外并与图U连接的边中,找到权值最小的边和该边连接的顶点并入图U中 。
3、重复第二步,直到U中包含了所有顶点 第二步若遇到两条权值相同的最小权值边,任选一条即可,所以最小生成树可能不唯一。
例题
A国有六个城市之间一直没有通信线路,现在国家想使这六个城市通信连通
六个城市,相互之间能够建设通信线缆的线路路径距离已测
要求以最小的成本建设通信线路,使得这六个城市之间能够互相通信
即从任意顶点出发,都可以到达其他顶点,且线缆总长度最小。
最小线缆长度为:10+12+13+14+14=63
import random
import matplotlib.pyplot as plt
import networkx as nx
# 生成一个随机的图
def generate_random_graph(num_vertices, num_edges):
G = nx.Graph()
G.add_nodes_from(range(num_vertices))
# 随机生成边
for _ in range(num_edges):
while True:
u, v = random.sample(range(num_vertices), 2)
weight = random.randint(1, 10)
if not G.has_edge(u, v):
G.add_edge(u, v, weight=weight)
break
return G
# 使用Prim算法求解最小生成树
def prim(G, start_node):
# 初始化
mst = nx.Graph()
total_weight = 0
visited = set([start_node])
edges = []
# 初始化边列表,只考虑与起始节点相连的边
for neighbor, data in G[start_node].items():
edges.append((data['weight'], start_node, neighbor))
while edges:
cost, parent, current_node = min(edges)
edges.remove((cost, parent, current_node))
if current_node not in visited:
visited.add(current_node)
mst.add_edge(parent, current_node, weight=G[parent][current_node]['weight'])
total_weight += cost
for neighbor, data in G[current_node].items():
if neighbor not in visited:
edges.append((data['weight'], current_node, neighbor))
return mst, total_weight
# 生成图
num_vertices = 7
num_edges = 15
G = generate_random_graph(num_vertices, num_edges)
# 应用Prim算法
prim_mst, prim_total_weight = prim(G, 0)
# 绘制原图
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
pos = nx.spring_layout(G)
nx.draw(G, pos, with_labels=True, node_color='lightblue', edge_color='gray')
labels = nx.get_edge_attributes(G, 'weight')
nx.draw_networkx_edge_labels(G, pos, edge_labels=labels)
plt.title('Original Graph (Prim)')
# 绘制Prim算法得到的最小生成树
plt.subplot(1, 2, 2)
pos_prim_mst = nx.spring_layout(prim_mst)
nx.draw(prim_mst, pos_prim_mst, with_labels=True, node_color='lightgreen', edge_color='red')
labels_prim_mst = nx.get_edge_attributes(prim_mst, 'weight')
nx.draw_networkx_edge_labels(prim_mst, pos_prim_mst, edge_labels=labels_prim_mst)
plt.title('Minimum Spanning Tree (Prim)')
plt.tight_layout()
plt.show()
prim_mst.edges(data=True), prim_total_weight