图:A(0)
/ | \
421
/ | \
B C D
| | |
332
| | |
E F G
求:从 A 到所有其他顶点的最短路径
解题思路
Dijkstra 算法的贪心策略
策略:选择距离估计最小的未访问顶点
算法步骤:
初始化:d[s] = 0,其他 d[v] = ∞
维护两个集合:已确定顶点集 S,未确定顶点集 V-S
重复以下步骤,直到 S = V:
从 V-S 中选择 d 值最小的顶点 u,加入 S
松弛 u 的所有出边 (v, u):
如果 d[u] + w(u, v) < d[v],更新 d[v]
为什么这个策略正确?
边权非负保证了:一旦顶点加入 S,其距离就是最终的
贪心选择(最小距离估计)每次都能确定一个顶点的最短路径
正确性证明
定理:Dijkstra 算法能正确计算单源最短路径。
证明(归纳法):
不变量:
对于所有已确定顶点 u ∈ S,d[u] 是从 s 到 u 的最短距离
对于所有未确定顶点 v ∈ V-S,d[v] 是从 s 到 v 且只经过 S 中顶点的最短路径长度
归纳基础:初始时 S = {s},d[s] = 0 显然正确
归纳步骤:设 u 是选择的顶点(d[u] 最小)。
假设存在更短的路径 P 从 s 到 u
路径 P 必须经过某个顶点 x ∈ V-S
由于边权非负,d[x] ≤ P 的长度 < d[u]
但 d[u] 是 V-S 中的最小距离,矛盾
因此,d[u] 是最短距离
Python 实现
import heapq
from typing importDict, List, Tuple, Setfrom collections import defaultdict
classGraph:
"""图类(邻接表表示)"""def__init__(self):
self.adj = defaultdict(list)
defadd_edge(self, u: str, v: str, weight: int, directed: bool = False):
"""添加边"""self.adj[u].append((v, weight))
ifnot directed:
self.adj[v].append((u, weight))
defvertices(self) -> Set[str]:
"""获取所有顶点"""
vertices = set(self.adj.keys())
for u inself.adj:
for v, _ inself.adj[u]:
vertices.add(v)
return vertices
defdijkstra(graph: Graph, start: str) -> Tuple[Dict[str, float], Dict[str, str]]:
"""
Dijkstra 算法实现
参数:
graph: 图对象
start: 源点
返回:
(距离字典,前驱字典)
"""
vertices = graph.vertices()
distances = {v: float('infinity') for v in vertices}
predecessors = {v: Nonefor v in vertices}
distances[start] = 0# 优先队列:(距离,顶点)
pq = [(0, start)]
visited = set()
print("Dijkstra 算法执行过程:")
print("=" * 70)
print(f"{'步骤':<6}{'当前顶点':<10}{'距离':<8}{'操作':<40}")
print("-" * 70)
step = 1while pq:
current_dist, current = heapq.heappop(pq)
if current in visited:
continue
visited.add(current)
print(f"{step:<6}{current:<10}{current_dist:<8}", end=" ")
# 松弛操作
relaxed = Falsefor neighbor, weight in graph.adj[current]:
if neighbor in visited:
continue
new_dist = current_dist + weight
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
predecessors[neighbor] = current
heapq.heappush(pq, (new_dist, neighbor))
relaxed = Trueifnot relaxed:
print("无可松弛边")
step += 1print("-" * 70)
return distances, predecessors
defreconstruct_path(predecessors: Dict[str, str], start: str, end: str) -> List[str]:
"""重构最短路径"""
path = []
current = end
while current isnotNone:
path.append(current)
current = predecessors[current]
path.reverse()
if path[0] == start:
return path
returnNone# 示例用法if __name__ == "__main__":
graph = Graph()
graph.add_edge('A', 'B', 4)
graph.add_edge('A', 'C', 2)
graph.add_edge('B', 'C', 1)
graph.add_edge('B', 'D', 5)
graph.add_edge('C', 'D', 8)
graph.add_edge('C', 'E', 10)
graph.add_edge('D', 'E', 2)
print("图的邻接表:")
for u insorted(graph.adj.keys()):
edges = ', '.join([f"{v}({w})"for v, w in graph.adj[u]])
print(f" {u} → [{edges}]")
distances, predecessors = dijkstra(graph, 'A')
print("\n算法结果:")
print("=" * 70)
print(f"{'顶点':<10}{'最短距离':<12}{'路径'}")
print("-" * 70)
for v insorted(distances.keys()):
if distances[v] == float('infinity'):
path = "不可达"else:
path = reconstruct_path(predecessors, 'A', v)
path_str = ' → '.join(path)
print(f"{v:<10}{distances[v]:<12}{path_str}")
print("=" * 70)
输出:
图的邻接表:A → [B(4), C(2)]B → [C(1), D(5)] C → [B(1), D(8), E(10)] D → [B(5), C(8), E(2)] E → [C(10), D(2)] Dijkstra 算法执行过程: ====================================================================== 步骤 当前顶点 距离 操作 ---------------------------------------------------------------------- 1A0 松弛 A→B(4) 2 C 2 松弛 C→B(1) 3B3 松弛 B→D(5) 4 E 5 无可松弛边 5 D 8 无可松弛边 ---------------------------------------------------------------------- 算法结果: ====================================================================== 顶点 最短距离 路径 ---------------------------------------------------------------------- A0AB3A → C → B C 2A → C D 8A → C → B → D E 5A → C → D → E ======================================================================
复杂度分析
时间复杂度:O((V + E) log V)
使用优先队列(二叉堆)
每个顶点入堆一次:O(V log V)
每条边可能导致一次松弛:O(E log V)
空间复杂度:O(V + E)
与其他算法对比
算法
时间复杂度
边权限制
特点
Dijkstra
O((V+E)log V)
非负
最常用,贪心
Bellman-Ford
O(VE)
可负
可检测负环
Floyd-Warshall
O(V³)
可负
全源最短路径
4.6 最小生成树
问题描述
给定带权无向连通图 G = (V, E),求一棵生成树 T,使得 T 中所有边的权值之和最小。
示例:
图:A---4---B
| \ |
213
| \ |
C---5---D
求:最小生成树
解题思路
最小生成树的性质
割性质(Cut Property):对于图的任意割,横跨该割的最小权边必属于某个 MST
环性质(Cycle Property):对于图中的任意环,该环中权值最大的边必不属于任何 MST
Prim 算法
贪心策略:从已选顶点集选择最小权值边
算法步骤:
从任意顶点开始,将其加入已选顶点集 S
重复以下步骤,直到 S = V:
在所有横跨割 (S, V-S) 的边中,选择权值最小的边 (u, v)
将 v 和边 (u, v) 加入 MST
复杂度:O((V + E) log V)
Kruskal 算法
贪心策略:全局选择最小权值边
算法步骤:
将所有边按权值从小到大排序
初始化空边集 T
依次检查每条边 (u, v):
如果 u 和 v 不连通,将边加入 T
否则,跳过(会形成环)
直到 |T| = |V| - 1
复杂度:O(E log E)
两种算法对比
特性
Prim 算法
Kruskal 算法
策略
顶点驱动
边驱动
数据结构
优先队列
并查集
适用图
稠密图
稀疏图
中间结果
始终连通
可能不连通
Python 实现
import heapq
from typing importDict, List, Tuple, Setfrom collections import defaultdict
classGraph:
"""图类"""def__init__(self):
self.adj = defaultdict(list)
defadd_edge(self, u: str, v: str, weight: int):
"""添加无向边"""self.adj[u].append((v, weight))
self.adj[v].append((u, weight))
defedges(self) -> List[Tuple[int, str, str]]:
"""获取所有边"""
edges = set()
for u inself.adj:
for v, w inself.adj[u]:
if u < v: # 避免重复
edges.add((w, u, v))
returnsorted(edges)
classUnionFind:
"""并查集(带路径压缩和按秩合并)"""def__init__(self, vertices: Set[str]):
self.parent = {v: v for v in vertices}
self.rank = {v: 0for v in vertices}
deffind(self, x: str) -> str:
"""查找(带路径压缩)"""ifself.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
returnself.parent[x]
defunion(self, x: str, y: str) -> bool:
"""合并(返回是否成功)"""
root_x = self.find(x)
root_y = self.find(y)
if root_x == root_y:
returnFalse# 按秩合并ifself.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elifself.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1returnTruedefprim_mst(graph: Graph, start: str) -> Tuple[List[Tuple[str, str, int]], int]:
"""Prim 算法实现"""
pq = [(0, start, None)]
visited = set()
mst_edges = []
total_weight = 0print("\nPrim 算法执行过程:")
print("-" * 60)
while pq andlen(visited) < len(graph.adj):
weight, current, from_vertex = heapq.heappop(pq)
if current in visited:
continue
visited.add(current)
if from_vertex isnotNone:
mst_edges.append((from_vertex, current, weight))
total_weight += weight
print(f"添加边:{from_vertex}-{current} (权值:{weight}), 累计:{total_weight}")
for neighbor, edge_weight in graph.adj[current]:
if neighbor notin visited:
heapq.heappush(pq, (edge_weight, neighbor, current))
return mst_edges, total_weight
defkruskal_mst(graph: Graph) -> Tuple[List[Tuple[str, str, int]], int]:
"""Kruskal 算法实现"""
edges = graph.edges()
vertices = set(graph.adj.keys())
for u in graph.adj:
for v, _ in graph.adj[u]:
vertices.add(v)
uf = UnionFind(vertices)
mst_edges = []
total_weight = 0print("\nKruskal 算法执行过程:")
print("-" * 60)
for weight, u, v in edges:
if uf.union(u, v):
mst_edges.append((u, v, weight))
total_weight += weight
print(f"选择边:{u}-{v} (权值:{weight}), 累计:{total_weight}")
else:
print(f"跳过边:{u}-{v} (形成环)")
iflen(mst_edges) == len(vertices) - 1:
breakreturn mst_edges, total_weight
# 示例用法if __name__ == "__main__":
graph = Graph()
graph.add_edge('A', 'B', 4)
graph.add_edge('A', 'C', 4)
graph.add_edge('B', 'C', 2)
graph.add_edge('B', 'D', 3)
graph.add_edge('C', 'D', 5)
graph.add_edge('C', 'E', 2)
graph.add_edge('D', 'E', 1)
print("图的邻接表:")
for u insorted(graph.adj.keys()):
edges = ', '.join([f"{v}({w})"for v, w in graph.adj[u]])
print(f" {u} → [{edges}]")
# Prim 算法
prim_edges, prim_weight = prim_mst(graph, 'A')
print(f"\nPrim 算法结果:")
print(f" MST 边数:{len(prim_edges)}")
print(f" 总权值:{prim_weight}")
# Kruskal 算法
kruskal_edges, kruskal_weight = kruskal_mst(graph)
print(f"\nKruskal 算法结果:")
print(f" MST 边数:{len(kruskal_edges)}")
print(f" 总权值:{kruskal_weight}")
复杂度分析
Prim 算法:
时间复杂度:O((V + E) log V)
空间复杂度:O(V + E)
Kruskal 算法:
时间复杂度:O(E log E)
空间复杂度:O(V)
4.7 多机调度问题
问题描述
有 n 个作业需要分配给 m 台相同的机器并行处理,每个作业 i 有处理时间 t[i]。目标是最小化所有作业的最大完成时间(makespan)。
注意:这是一个 NP 难问题,因此讨论近似算法。
示例:
作业时间:[8, 7, 6, 5, 4, 3, 2, 1]
机器数量:3
求:最小化最大完成时间
解题思路
LPT 算法
贪心策略:Longest Processing Time first(长作业优先)
算法步骤:
将作业按处理时间从大到小排序
依次分配给当前负载最小的机器
近似比:4/3 - 1/(3m)
正确性分析
LPT 算法不能保证得到最优解,但可以保证近似解不超过最优解的 4/3 倍。
定理:LPT 算法的近似比为 4/3 - 1/(3m)。
Python 实现
import heapq
from typing importList, Tupledeflpt_schedule(jobs: List[int], m: int) -> Tuple[List[List[int]], int]:
"""
LPT 算法求解多机调度问题
参数:
jobs: 作业处理时间列表
m: 机器数量
返回:
(调度方案,makespan)
"""# 按处理时间从大到小排序
sorted_jobs = sorted(jobs, reverse=True)
# 最小堆:(机器负载,机器编号)
heap = [(0, i) for i inrange(m)]
heapq.heapify(heap)
# 调度结果
schedules = [[] for _ inrange(m)]
print(f"\nLPT 算法执行过程({m}台机器):")
print("-" * 60)
for job_time in sorted_jobs:
load, machine = heapq.heappop(heap)
schedules[machine].append(job_time)
new_load = load + job_time
heapq.heappush(heap, (new_load, machine))
print(f"作业{job_time} → 机器{machine+1} (负载:{load}→{new_load})")
# 计算 makespan
makespan = max(load for load, _ in heap)
print("-" * 60)
return schedules, makespan
# 示例用法if __name__ == "__main__":
jobs = [8, 7, 6, 5, 4, 3, 2, 1]
m = 3print(f"作业时间:{jobs}")
print(f"机器数量:{m}")
schedules, makespan = lpt_schedule(jobs, m)
print(f"\n最终调度:")
for i, schedule inenumerate(schedules, 1):
total = sum(schedule)
jobs_str = ' + '.join(map(str, schedule))
print(f" 机器{i}: {jobs_str} = {total}")
print(f"\nMakespan: {makespan}")