算法图示:Bilibili《最短路径查找—Dijkstra算法》
Dijkstra’s algorithm(迪杰斯特拉算法)是一种用于求解单源最短路径问题的经典算法。该算法可以计算从单个起始节点到图中所有其他节点的最短路径。Dijkstra’s algorithm适用于没有负权边的有向或无向带权图。
算法的基本思想是从起始节点开始,不断扩展当前已知的最短路径,直到到达目标节点或处理完所有节点。该算法使用一个辅助数组(通常称为距离数组)来保存从起始节点到每个节点的最短路径长度。算法的步骤如下:
Dijkstra’s algorithm保证在没有负权边的情况下能够找到最短路径。然而,如果图中存在负权边,就不能保证得到正确的最短路径,这时候需要使用其他算法,例如Bellman-Ford算法,来处理含有负权边的情况。
import sys
def dijkstra(graph, start):
num_nodes = len(graph)
visited = [False] * num_nodes
distances = [sys.maxsize] * num_nodes
distances[start] = 0
for _ in range(num_nodes):
# 在每次循环中,选择距离数组中最小距离的节点进行扩展
min_distance = sys.maxsize
min_node = -1
for node in range(num_nodes):
# 遍历所有未被访问过的节点,找到距离最小的节点
if not visited[node] and distances[node] < min_distance:
min_distance = distances[node]
min_node = node
visited[min_node] = True
for neighbor in range(num_nodes):
# 更新与当前节点相邻的节点的最短路径长度
if graph[min_node][neighbor] > 0:
new_distance = distances[min_node] + graph[min_node][neighbor]
if new_distance < distances[neighbor]:
distances[neighbor] = new_distance
return distances
# 示例
if __name__ == "__main__":
# 示例图的邻接矩阵表示
graph = [
[0, 4, 0, 0, 0, 0, 0, 8, 0],
[4, 0, 8, 0, 0, 0, 0, 11, 0],
[0, 8, 0, 7, 0, 4, 0, 0, 2],
[0, 0, 7, 0, 9, 14, 0, 0, 0],
[0, 0, 0, 9, 0, 10, 0, 0, 0],
[0, 0, 4, 14, 10, 0, 2, 0, 0],
[0, 0, 0, 0, 0, 2, 0, 1, 6],
[8, 11, 0, 0, 0, 0, 1, 0, 7],
[0, 0, 2, 0, 0, 0, 6, 7, 0]
]
start_node = 0
shortest_distances = dijkstra(graph, start_node)
print("从节点{}到其他节点的最短距离:".format(start_node))
print(shortest_distances)
这个证明过程是关于Dijkstra’s algorithm(迪杰斯特拉算法)在计算最短路径时的正确性。证明使用了归纳法(induction),来说明在算法的每一步中,维持一个不变量(invariant):对于集合S中的每个节点u,d(u)表示从起始节点s到u的最短路径长度。
接下来证明对于集合S的大小为k + 1时,维持不变量仍然成立:
综上所述,无论路径P如何选择,其长度都不会小于π(v)。因此,当集合S的大小为k + 1时,维持不变量依然成立。
由归纳法的原理,对于任意大小的集合S,都能够保持维持不变量:对于集合S中的每个节点u,d(u)是最短s到u的路径长度。这证明了Dijkstra’s algorithm计算最短路径的正确性。
最小生成树算法(Minimum Spanning Tree, MST)是一类用于在加权连通图中找到一棵包含所有节点且边权重之和最小的树的算法。MST算法常用于解决优化问题,如网络设计、电力传输等领域。
常见的MST算法有两种:Kruskal算法和Prim算法。
两种算法的选择依赖于具体的问题和数据结构。Kruskal算法更适用于稀疏图,而Prim算法更适用于稠密图。
下图就是切割{4,5,8}子集所形成的割集
命题:环和割集相交于偶数条边
令 T = (V, F) 为 G = (V, E) 的子图。 TFAE:
最小生成树本质还是生成树,最重要的一条属性就是边权重之和最小,是最优情况下的生成树
算法图示:Bilibili《最小生成树Kruskal和Prim算法动画演示》
Prim’s algorithm(普里姆算法)是用于解决最小生成树(Minimum Spanning Tree, MST)问题的一种常用贪心算法。它通过逐步添加节点来构建最小生成树,并保证最终生成的树是整个图中权重之和最小的树。
算法步骤如下:
Prim’s algorithm的关键在于不断地选取权重最小的节点,并更新相关节点的权重。它保证了每次选择的节点都是与最小生成树相邻且权重最小的节点,从而逐步构建出整个图的最小生成树。
Prim’s algorithm适用于稠密图,即节点之间的边相对较多的情况。在实现上,通常使用优先级队列(最小堆)来维护未访问节点的权重,并通过快速查找和更新节点的权重来加速算法的执行。
import heapq
def prim(graph):
num_nodes = len(graph)
visited = [False] * num_nodes
min_heap = [(0, 0)] # 最小堆,用于存储权重和节点的元组
mst = [] # 最小生成树的边
total_weight = 0 # 最小生成树的总权重
while min_heap:
weight, node = heapq.heappop(min_heap) # 从堆中弹出最小权重的节点
if visited[node]:
continue
visited[node] = True # 标记节点为已访问
total_weight += weight
mst.append((weight, node)) # 将权重和节点添加到最小生成树的边列表中
for neighbor, weight in graph[node]:
if not visited[neighbor]:
heapq.heappush(min_heap, (weight, neighbor)) # 将与当前节点相邻且未访问的节点添加到堆中
return mst, total_weight
# 示例
if __name__ == "__main__":
# 示例图的邻接表表示
graph = {
0: [(1, 2), (3, 3)],
1: [(0, 2), (2, 1), (3, 4)],
2: [(1, 1), (3, 5)],
3: [(0, 3), (1, 4), (2, 5)]
}
mst, total_weight = prim(graph)
print("最小生成树边的列表及其权重:")
print(mst)
print("最小生成树的总权重:", total_weight)
Kruskal算法是一种常用的贪婪算法,用于寻找连通无向图的最小生成树(MST)。
以下是Kruskal算法的工作原理概述:
Kruskal算法确保加入的边不会在生成树中引起循环,这使得它成为一种安全的选择。算法会继续添加权重最小的边,同时避免产生循环,从而形成最小生成树。
在算法过程中通常会使用并查集数据结构(也称为并查集数据结构)来有效地检测循环。这个数据结构有助于追踪哪些顶点已经属于生成树,哪些顶点尚未连接。
Kruskal算法高效,其时间复杂度为O(E log E),其中E为图中的边数。这主要归因于排序步骤,它需要O(E log E)时间,而后续步骤需要额外的线性时间。
总之,Kruskal算法通过迭代地添加权重最小的边,同时避免产生循环,从而找到连通无向图的最小生成树。
# 并查集实现
class UnionFind:
def __init__(self, n):
self.parent = list(range(n)) # 初始化每个节点的父节点为自身
self.rank = [0] * n # 初始化每个节点的秩为0
# 查找节点x所属的集合的代表节点
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 通过递归路径压缩来优化查找过程
return self.parent[x]
# 合并两个集合
def union(self, x, y):
root_x = self.find(x)
root_y = self.find(y)
# 如果两个节点在同一个集合中,实际就表示形成了环
if root_x == root_y: # 如果两个节点已经在同一集合中,不需要合并
return False
if self.rank[root_x] < self.rank[root_y]: # 将秩较小的集合合并到秩较大的集合中
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1 # 如果秩相等,合并后秩增加
return True
# Kruskal算法
def kruskal(edges, n):
edges.sort(key=lambda x: x[2]) # 将边按照权重升序排序
uf = UnionFind(n) # 创建并查集对象
min_spanning_tree = [] # 用于存储最小生成树的边
for edge in edges:
u, v, weight = edge
if uf.union(u, v): # 如果边的两个节点不在同一集合中,合并集合并将边添加到最小生成树中
min_spanning_tree.append(edge)
return min_spanning_tree
# 示例数据
edges = [
(0, 1, 4),
(0, 2, 6),
(1, 2, 8),
(1, 3, 3),
(2, 3, 2),
(2, 4, 7),
(3, 4, 5),
(4, 3, 1) # 注意这里的有向边形成了环
]
num_vertices = 6
# 执行Kruskal算法
minimum_spanning_tree = kruskal(edges, num_vertices)
# 输出结果
print("Minimum Spanning Tree:")
for edge in minimum_spanning_tree:
print(edge)
上述代码通过定义并查集来简化Kruskal算法过程中的添加边过程和检测环路过程(如果两个点本身就在同一个集合内,就表明它们当前已经有一条能够相互连接的通路,此时再加入它们两个顶点的直接连接路径就会构成环路)
Reverse-delete算法是一种用于找到图的最小生成树(MST)的算法,与Kruskal算法相似。与Kruskal从小到大按权重选择边来构建MST不同,Reverse-delete算法从大到小按权重删除边来构建MST。这个算法首先将所有边按权重降序排列,然后依次删除边,每次删除都会检查是否导致图的断开。如果删除边后图仍然是连通的,说明这条边不是构成MST所必需的,可以被删除。
以下是Reverse-delete算法的步骤:
与Kruskal算法不同,Reverse-delete算法不需要检测环路,因为每次删除边后图总是连通的。然而,这个算法需要进行图的连通性检查,以确保删除边后图仍然保持连通。
需要注意的是,Reverse-delete算法可能对于稠密图(边数较多)的效率不如Kruskal算法,因为删除边的过程可能会涉及到多次图的连通性检查。
总之,Reverse-delete算法是一种寻找图最小生成树的方法,通过从大到小按权重删除边来逐步构建最小生成树。
Borůvka’s algorithm(博鲁夫卡算法)是一种用于寻找图的最小生成树(MST)的算法。它是一种并行算法,旨在充分利用多个处理单元或计算机来加速计算。这个算法也被称为Sollin’s algorithm(索林算法)或Sarnowski’s algorithm(萨诺夫斯基算法)。
Borůvka’s算法适用于无向图的最小生成树问题,其基本思想是通过从每个连通组件中选择一个最小权重的边,然后将连通组件合并,最终构建出整个图的最小生成树。
以下是Borůvka’s算法的步骤:
将每个顶点作为一个单独的连通组件。 重复以下步骤,直到只剩下一个连通组件(即构建完整的最小生成树): 对于每个连通组件,选择连接该组件的最小权重的边。 将这些最小权重边所连接的顶点合并为一个新的连通组件。 删除所有不再需要的边。 Borůvka’s算法的一个关键特点是它可以并行地处理多个连通组件,因此在具备多个处理单元或计算机的情况下,它可以实现较高的计算效率。
需要注意的是,Borůvka’s算法可能在稠密图(边数较多)上表现得更好,因为它在每个迭代步骤中可以并行地处理多个连通组件。
虽然Borůvka’s算法在理论上是一个有效的算法,但在实际应用中,由于现代计算机系统和并行算法的复杂性,它可能不如其他算法(如Kruskal、Prim算法)在实践中运行得快速和高效。