本篇博客学习两个背景下的解决方法,1.求多个起点到多个终点的多条最短路径;2.通过启发式函数优化广搜
Floyd 算法精讲
本题是经典的多源最短路问题。在这之前我们讲解过,dijkstra朴素版、dijkstra堆优化、Bellman算法、Bellman队列优化(SPFA) 都是单源最短路,即只能有一个起点。而本题是多源最短路,即 求多个起点 到多个终点的多条最短路径。
通过本题,学习一个新的最短路算法-Floyd 算法。
Floyd 算法对边的权值正负没有要求,都可以处理。Floyd算法核心思想是动态规划。动态规划之前的博客我们练习过很多,所以不做详细介绍
- 确定dp数组(dp table)以及下标的含义
这里我们用 grid数组来存图,那就把dp数组命名为 grid。grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合中的一个节点为中间节点的最短距离为m。
- 确定递推公式(k是从头到尾循环的)
我们分两种情况:
1.节点i 到 节点j 的最短路径经过节点k
对于第一种情况,grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]
节点i 到 节点k 的最短距离 是不经过节点k,中间节点集合为[1...k-1],所以 表示为grid[i][k][k - 1];
节点k 到 节点j 的最短距离 也是不经过节点k,中间节点集合为[1...k-1],所以表示为 grid[k][j][k - 1]
2.节点i 到 节点j 的最短路径不经过节点k
第二种情况,grid[i][j][k] = grid[i][j][k - 1]
,如果节点i 到 节点j的最短距离 不经过节点k,那么 中间节点集合[1...k-1],表示为 grid[i][j][k - 1]
因为我们是求最短路,对于这两种情况自然是取最小值。即: grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])
- dp数组如何初始化
初始化时把k 赋值为 0,这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。
同时因为求最小值,其他位置全部赋值为最大值,注意对于三维的理解
- 确定遍历顺序
k 依赖于 k - 1, i 和j 的到 并不依赖与 i - 1 或者 j - 1 等等。所以遍历k 的for循环一定是在最外面,这样才能一层一层去遍历。
三维
floyd算法的时间复杂度相对较高,适合 稠密图且源点较多的情况。
python
if __name__ == '__main__':
max_int = 10005 # 设置最大路径,因为边最大距离为10^4
n, m = map(int, input().split())
grid = [[[max_int] * (n+1) for _ in range(n+1)] for _ in range(n+1)] # 初始化三维dp数组
for _ in range(m):
p1, p2, w = map(int, input().split())
grid[p1][p2][0] = w
grid[p2][p1][0] = w
# 开始floyd
for k in range(1, n+1):
for i in range(1, n+1):
for j in range(1, n+1):
grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1])
# 输出结果
z = int(input())
for _ in range(z):
start, end = map(int, input().split())
if grid[start][end][n] == max_int:
print(-1)
else:
print(grid[start][end][n])
二维
python
if __name__ == '__main__':
max_int = 10005 # 设置最大路径,因为边最大距离为10^4
n, m = map(int, input().split())
grid = [[max_int]*(n+1) for _ in range(n+1)] # 初始化二维dp数组
for _ in range(m):
p1, p2, val = map(int, input().split())
grid[p1][p2] = val
grid[p2][p1] = val
# 开始floyd
for k in range(1, n+1):
for i in range(1, n+1):
for j in range(1, n+1):
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j])
# 输出结果
z = int(input())
for _ in range(z):
start, end = map(int, input().split())
if grid[start][end] == max_int:
print(-1)
else:
print(grid[start][end])
A star算法
Astar 是一种 广搜的改良版。我们在搜索最短路的时候, 如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密),如果是有权图(边有不同的权值),优先考虑 dijkstra。而 Astar 关键在于 启发式函数, 也就是 影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。
对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢?每个节点的权值为F,给出公式为:F = G + H,G:起点达到目前遍历节点的距离H:目前遍历的节点到达终点的距离计算出来 F 之后,按照 F 的 大小,来选去出队列的节点。其实就是一个指标,主要是比较哪个节点离终点近,来进行后续判断。即:使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。
在游戏开发设计中,保证运行效率的情况下,A * 算法中的启发式函数 设计往往不是最短路,而是接近最短路的 次短路设计。
A * 算法的时间复杂度 其实是不好去量化的,因为他取决于 启发式函数怎么写。最坏情况下,A * 退化成广搜,算法的时间复杂度 是 O(n * 2),n 为节点数量。最佳情况,是从起点直接到终点,时间复杂度为 O(dlogd),d 为起点到终点的深度。
python
import heapq # 导入heapq模块,用于实现优先队列(最小堆)
n = int(input()) # 读取测试用例的数量
# 定义马的8种可能移动方式(横纵方向的位移组合)
moves = [(1, 2), (2, 1), (-1, 2), (2, -1),
(1, -2), (-2, 1), (-1, -2), (-2, -1)]
def distance(a, b):
"""计算两点之间的欧氏距离(直线距离)"""
return ((a[0] - b[0]) **2 + (a[1] - b[1])** 2) **0.5
def bfs(start, end):
"""
使用A*算法(结合BFS和启发式搜索)寻找马从起点到终点的最短路径步数
start: 起点坐标 tuple(x, y)
end: 终点坐标 tuple(x, y)
返回:最短路径的步数
"""
# 优先队列初始化,存储(启发式函数值, 当前位置)
# 启发式函数使用:当前位置到终点的直线距离 + 已走步数
q = [(distance(start, end), start)]
# 记录到达每个位置的最少步数,键为坐标 tuple(x, y),值为步数
step = {start: 0}
while q: # 当队列不为空时循环
# 弹出启发式函数值最小的位置(优先探索更可能接近终点的路径)
d, cur = heapq.heappop(q)
# 堆是根据 q 中元素的第一个值进行排序的
if cur == end: # 如果到达终点,返回最少步数
return step[cur]
# 尝试所有可能的移动
for move in moves:
# 计算新位置坐标
new = (move[0] + cur[0], move[1] + cur[1])
# 确保新位置在1-1000的棋盘范围内
if 1 <= new[0] <= 1000 and 1 <= new[1] <= 1000:
# 到达新位置的步数是当前步数+1
step_new = step[cur] + 1
# 如果新位置未被访问过,或发现更少步数的路径
if step_new < step.get(new, float('inf')):
# 不会存储inf
step[new] = step_new # 更新步数
# 将新位置和其启发式函数值加入优先队列
heapq.heappush(q, (distance(new, end) + step_new, new))
return False # 理论上不会触发,因为马在有限棋盘内总能到达目标
# 处理每个测试用例
for _ in range(n):
# 读取起点和终点坐标
a1, a2, b1, b2 = map(int, input().split())
# 计算并打印最短路径步数
print(bfs((a1, a2), (b1, b2)))
如果题目中,给出 多个可能的目标,然后在这多个目标中 选择最近的目标,这种 A * 就不擅长了, A * 只擅长给出明确的目标 然后找到最短路径。如果是多个目标找最近目标(特别是潜在目标数量很多的时候),可以考虑 Dijkstra ,BFS 或者 Floyd
最短路算法总结篇
- dijkstra朴素版:单源且边为正数,稠密图( O(n^2) )
- dijkstra堆优化版:单源且边为正数,稀疏图 ( O(Elog(E)) )
- Bellman_ford:单源边可为负数,稠密图( O(n * e) )
- Bellman_ford 队列优化算法(又名SPFA):单源边可为负数,稀疏图 ( O(n * k) )
- bellman_ford 算法判断负权回路:负权回路
- bellman_ford之单源有限最短路:有限节点
- Floyd 算法精讲:多源点求最短路
- 启发式搜索:A * 算法:为优化算法,游戏开发、地图导航、数据包路由等都广泛使用 A * 算法。