Skip to content

Commit e894c2a

Browse files
🆕 新增最少换乘、最少价格路径和最少交换价格的算法示例
- 在文档中新增了最少换乘算法的描述及其实现,使用广度优先搜索(BFS)解决单向站点的换乘问题。 - 增加了最少价格路径的Dijkstra算法示例,详细阐述了如何在带权有向图中找到价格总和最小的路径。 - 引入了Bellman-Ford算法的实现,处理带负权边的情况,确保能够检测负权环并找到最短路径。 - 提升了文档的实用性与参考价值,增强了对图论算法的理解与应用场景的展示。
1 parent 05573eb commit e894c2a

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed

docs/docs/选择编程语言/Python/99练习.mdx

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,3 +891,318 @@ def fun(num, list=None):
891891
x = 9*5
892892
print(fun(x))# [3, 3, 5]
893893
```
894+
895+
### 最少换乘
896+
897+
#### 描述
898+
899+
假设你要从起点去往终点,路上有多个**单向**站点,怎么设计一个算法,找到最少换乘的路线?
900+
901+
这是一个经典的图论问题,可以用广度优先搜索(BFS)来解决。每条路线可以看作是一条边,换乘次数就是路径的边数。
902+
903+
#### 思路
904+
905+
1. 找到起点能去的所有节点
906+
2. 检查可去节点是否包含终点
907+
3. 如果不包含则继续前往这些节点的可去节点
908+
4. 使用广度优先搜索(BFS)确保找到的是最少换乘路线
909+
5. 使用队列记录当前层级的节点,逐层扩展直到找到终点
910+
911+
#### 题解
912+
913+
```python showLineNumbers
914+
915+
def min_transfers(edges, start, end):
916+
"""
917+
找到从起点到终点的最少换乘路线
918+
919+
:param edges: 图的邻接表表示,edges[u] = [v1, v2, ...] 表示从u可以到达的站点
920+
:param start: 起点
921+
:param end: 终点
922+
:return: 最少换乘次数和路径,如果无法到达返回-1和None
923+
"""
924+
if start == end:
925+
return 0, [start]
926+
927+
# BFS队列,存储(当前站点, 换乘次数, 路径)
928+
queue = [(start, 0, [start])]
929+
visited = {start} # 已访问的站点
930+
931+
# 只要队列不为空,就继续遍历
932+
while queue:
933+
# 获取队列中最左侧(最左边是第一个进入队列的元素)元素,并从队列中删除
934+
# 当前站点, 换乘次数, 路径
935+
current, transfers, path = queue.pop(0)
936+
937+
# 遍历所有可达的下一个站点(即当前站点可去的站点)
938+
# 如果当前站点没有下一个站点,则跳过
939+
# 如果当前站点有下一个站点,则将下一个站点加入队列
940+
for next_station in edges.get(current, []):
941+
if next_station == end:
942+
# 找到终点
943+
return transfers + 1, path + [end]
944+
# 如果下一个站点没有被访问过,则将下一个站点加入已访问集合,并加入队列
945+
# 这一步可以避免重复访问同一个站点,提升算法效率
946+
if next_station not in visited:
947+
# 将下一个站点加入已访问集合
948+
visited.add(next_station)
949+
# 将下一个站点加入队列
950+
# 换乘次数加1,路径加上下一个站点
951+
queue.append((next_station, transfers + 1, path + [next_station]))
952+
953+
# 无法到达终点
954+
return -1, None
955+
956+
# 测试数据
957+
if __name__ == '__main__':
958+
# 示例:站点A->B->D, A->C->D, A->D
959+
edges = {
960+
'A': ['B', 'C'],
961+
'B': ['C'],
962+
'C': ['X'],
963+
'X': [],
964+
'D': []
965+
}
966+
967+
transfers, path = min_transfers(edges, 'A', 'D')
968+
print(f"最少换乘次数: {transfers}")
969+
print(path) # 最少换乘次数: 1, 路径: A -> D
970+
```
971+
972+
### 最少价格路径
973+
974+
#### 描述
975+
976+
给定一个带权有向图,每条边都有一个价格(权重),请找到从起点到终点的**价格总和最小**的路径。
977+
978+
这是一个典型的**单源最短路径**问题,可以使用Dijkstra算法来解决(假设所有边权非负)。
979+
980+
#### 思路
981+
982+
1. 初始化起点的距离为0,其他所有节点距离为无穷大
983+
2. 找到起点能去的所有节点,计算到达这些节点的价格
984+
3. 选择价格最小的节点作为下一个访问节点
985+
4. 检查该节点的可去节点,更新到达这些节点的最少价格
986+
5. 重复步骤3-4,直到访问到终点或所有可达节点都访问完毕
987+
6. 使用优先队列(堆)来快速找到价格最小的节点
988+
989+
#### 题解
990+
991+
```python showLineNumbers
992+
def dijkstra(graph, start, end):
993+
"""
994+
使用Dijkstra算法找到从起点到终点的最少价格路径
995+
996+
:param graph: 图的邻接表表示,graph[u] = [(v1, weight1), (v2, weight2), ...]
997+
:param start: 起点
998+
:param end: 终点
999+
:return: 最少价格和路径,如果无法到达返回-1和None
1000+
"""
1001+
# 初始化节点信息字典:每个节点包含价格、前驱节点等信息
1002+
nodes = {}
1003+
processed = set() # 已处理的节点集合
1004+
1005+
# 获取所有节点
1006+
# 1. 获取所有节点放入 集合,集合具有去重的特点
1007+
all_nodes = set(graph.keys())
1008+
# 2. 获取所有邻居节点
1009+
for neighbors in graph.values():
1010+
for neighbor, _ in neighbors:
1011+
all_nodes.add(neighbor) # 将邻居节点加入所有节点集合
1012+
1013+
# 步骤1:初始化起点的距离为0,其他所有节点距离为无穷大
1014+
for node in all_nodes:
1015+
nodes[node] = {
1016+
'price': 0 if node == start else float('inf'),
1017+
'previous': None # 前一个节点
1018+
}
1019+
"""
1020+
all_nodes = {'A', 'B', 'C', 'D'}
1021+
1022+
nodes = {'A': {'price': 0, 'previous': None},
1023+
'D': {'price': inf, 'previous': None},
1024+
'C': {'price': inf, 'previous': None},
1025+
'B': {'price': inf, 'previous': None}}
1026+
"""
1027+
1028+
def find_lowest_price_node():
1029+
"""在所有未处理的节点中,找到价格最小的节点"""
1030+
lowest_price = float('inf')
1031+
lowest_price_node = None
1032+
for node_name, node_info in nodes.items():
1033+
# 依次取出每个节点,并检查其价格是否小于最低价格
1034+
# 初始选择中,除了起点是0,其他都是无穷大,所以一定会选择起点
1035+
if node_name not in processed: # 只从未处理的节点中查找
1036+
cost = node_info['price']
1037+
if cost < lowest_price:
1038+
lowest_price = cost
1039+
lowest_price_node = node_name
1040+
return lowest_price_node
1041+
1042+
# 步骤3-5:重复选择价格最小的节点,更新其邻居节点的价格
1043+
node = find_lowest_price_node() # 在所有未处理的节点中,找到价格最小的节点
1044+
1045+
while node is not None: # 只要存在未处理的节点,就继续循环
1046+
1047+
# 如果到达终点,可以提前结束(可选优化)
1048+
if node == end:
1049+
break
1050+
1051+
price = nodes[node]['price'] # 当前节点的价格
1052+
1053+
# 步骤2和4:找到当前节点能去的所有节点,计算到达这些节点的价格
1054+
neighbors = graph.get(node, []) # 当前节点的邻居(从图的邻接表中获取)
1055+
for neighbor_name, edge_weight in neighbors: # 遍历当前节点的邻居
1056+
# 计算到达邻居的价格 = 当前节点价格 + 边权重
1057+
new_price = price + edge_weight
1058+
# 如果到达邻居的价格比当前价格更小,则更新邻居的价格
1059+
if new_price < nodes[neighbor_name]['price']:
1060+
nodes[neighbor_name]['price'] = new_price # 更新邻居的价格
1061+
nodes[neighbor_name]['previous'] = node # 更新邻居的前一个节点
1062+
1063+
processed.add(node) # 将当前节点标记为已处理
1064+
node = find_lowest_price_node() # 在所有未处理的节点中,找到价格最小的节点
1065+
1066+
# 检查是否能到达终点
1067+
if nodes[end]['price'] == float('inf'):
1068+
return -1, None
1069+
1070+
# 重建路径
1071+
def get_path(node_name):
1072+
"""根据前驱节点重建路径"""
1073+
path = []
1074+
current = node_name
1075+
while current is not None:
1076+
path.append(current)
1077+
current = nodes[current]['previous']
1078+
path.reverse()
1079+
return path if path else None
1080+
1081+
path = get_path(end)
1082+
return nodes[end]['price'], path
1083+
1084+
1085+
# 测试数据
1086+
if __name__ == '__main__':
1087+
# 示例图:A->B(1), B->C(2),
1088+
# B->D(5),
1089+
# A->C(4), C->D(1)
1090+
graph = {
1091+
'A': [('B', 1), ('C', 4)],
1092+
'B': [('C', 2), ('D', 5)],
1093+
'C': [('D', 1)],
1094+
'D': []
1095+
}
1096+
1097+
price, path = dijkstra(graph, 'A', 'D')
1098+
print(f"最少价格: {price}")
1099+
print(f"路径: {' -> '.join(path)}") # 最少价格: 4, 路径: A -> B -> C -> D
1100+
```
1101+
1102+
### 最少交换价格
1103+
1104+
#### 描述
1105+
1106+
给定一个带权有向图,每条边都有一个价格(权重),**考虑负权边**的情况,请找到从起点到所有其他顶点的最少价格路径。
1107+
1108+
当图中存在负权边时,Dijkstra算法不再适用(因为可能出现负环),需要使用**Bellman-Ford算法****SPFA算法**。Bellman-Ford算法可以检测负权环,并找到最短路径。
1109+
1110+
#### 思路
1111+
1112+
1. 初始化起点的距离为0,其他所有节点距离为无穷大
1113+
2. 对所有边进行V-1次松弛操作(V为顶点数),每次检查所有边是否能更新最短距离
1114+
3. 对于每条边(u, v, w),如果dist[u] + w < dist[v],则更新dist[v] = dist[u] + w
1115+
4. 经过V-1次松弛后,如果没有负权环,所有最短路径应该已经找到
1116+
5. 再进行一次松弛操作,如果还能更新距离,说明存在负权环
1117+
6. 使用前驱数组记录路径,便于重建最短路径
1118+
1119+
#### 题解
1120+
1121+
```python showLineNumbers
1122+
def bellman_ford(graph, start):
1123+
"""
1124+
使用Bellman-Ford算法找到从起点到所有顶点的最少价格路径
1125+
可以处理负权边,并检测负权环
1126+
1127+
:param graph: 图的边列表表示,[(u, v, weight), ...]
1128+
:param start: 起点
1129+
:return: (距离字典, 前驱字典, 是否存在负权环)
1130+
"""
1131+
# 获取所有顶点
1132+
vertices = set()
1133+
for u, v, w in graph:
1134+
vertices.add(u)
1135+
vertices.add(v)
1136+
vertices = list(vertices)
1137+
1138+
# 初始化距离和前驱
1139+
distances = {v: float('inf') for v in vertices}
1140+
predecessors = {v: None for v in vertices}
1141+
distances[start] = 0
1142+
1143+
# 松弛操作:进行V-1次(V为顶点数)
1144+
for _ in range(len(vertices) - 1):
1145+
updated = False
1146+
for u, v, weight in graph:
1147+
if distances[u] != float('inf') and distances[u] + weight < distances[v]:
1148+
distances[v] = distances[u] + weight
1149+
predecessors[v] = u
1150+
updated = True
1151+
if not updated:
1152+
break # 提前退出优化
1153+
1154+
# 检测负权环:再进行一次松弛,如果还能更新,说明存在负权环
1155+
has_negative_cycle = False
1156+
for u, v, weight in graph:
1157+
if distances[u] != float('inf') and distances[u] + weight < distances[v]:
1158+
has_negative_cycle = True
1159+
break
1160+
1161+
return distances, predecessors, has_negative_cycle
1162+
1163+
def get_path(predecessors, start, end):
1164+
"""
1165+
根据前驱字典重建路径
1166+
"""
1167+
if end not in predecessors or predecessors[end] is None:
1168+
return None
1169+
1170+
path = []
1171+
current = end
1172+
while current is not None:
1173+
path.append(current)
1174+
current = predecessors[current]
1175+
if current == start:
1176+
path.append(start)
1177+
break
1178+
1179+
path.reverse()
1180+
return path if path[0] == start else None
1181+
1182+
# 测试数据
1183+
if __name__ == '__main__':
1184+
# 示例图:A->B(-1), A->C(4), B->C(3), B->D(2), B->E(2), D->B(1), D->C(5), E->D(-3)
1185+
graph = [
1186+
('A', 'B', -1),
1187+
('A', 'C', 4),
1188+
('B', 'C', 3),
1189+
('B', 'D', 2),
1190+
('B', 'E', 2),
1191+
('D', 'B', 1),
1192+
('D', 'C', 5),
1193+
('E', 'D', -3)
1194+
]
1195+
1196+
distances, predecessors, has_cycle = bellman_ford(graph, 'A')
1197+
1198+
if has_cycle:
1199+
print("警告:图中存在负权环!")
1200+
else:
1201+
print("从A到各顶点的最少价格:")
1202+
for vertex, dist in distances.items():
1203+
if dist != float('inf'):
1204+
path = get_path(predecessors, 'A', vertex)
1205+
print(f"A -> {vertex}: {dist}, 路径: {' -> '.join(path)}")
1206+
else:
1207+
print(f"A -> {vertex}: 无法到达")
1208+
```

0 commit comments

Comments
 (0)