算法训练营day62 图论⑪ Floyd 算法精讲、A star算法、最短路算法总结篇

本篇博客学习两个背景下的解决方法,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 * 算法。
相关推荐
2501_924878591 小时前
强光干扰下漏检率↓78%!陌讯动态决策算法在智慧交通违停检测的实战优化
大数据·深度学习·算法·目标检测·视觉检测
耳总是一颗苹果2 小时前
排序---插入排序
数据结构·算法·排序算法
YLCHUP2 小时前
【联通分量】题解:P13823 「Diligent-OI R2 C」所谓伊人_连通分量_最短路_01bfs_图论_C++算法竞赛
c语言·数据结构·c++·算法·图论·广度优先·图搜索算法
GuGu20243 小时前
新手刷题对内存结构与形象理解的冲突困惑
算法
汤永红3 小时前
week4-[二维数组]平面上的点
c++·算法·平面·信睡奥赛
晴空闲雲4 小时前
数据结构与算法-字符串、数组和广义表(String Array List)
数据结构·算法
颜如玉5 小时前
位运算技巧总结
后端·算法·性能优化
冷月半明5 小时前
时间序列篇:Prophet负责优雅,LightGBM负责杀疯
python·算法
秋难降6 小时前
聊聊 “摸鱼式” 遍历 —— 受控遍历的小心机
数据结构·算法·程序员