当前位置: 首页 > news >正文

代码随想录Day62:图论(Floyd 算法精讲、A * 算法精讲、最短路算法总结、图论总结)

一、实战

Floyd 算法精讲

97. 小明逛公园

关于Floyd算法:多用于解决多源最短路问题,即 求多个起点到多个终点的多条最短路径。dijkstra、Bellman都是单源最短路,即只能有一个起点。对边的权值正负没有要求,都可以处理。核心处理思路是动态规划。

思路:

  • 确定dp数组(dp table)以及下标的含义

用 grid数组来存图,那就把dp数组命名为 grid。grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合中的一个节点为中间节点的最短距离为m

以[1...k]集合为中间节点怎么理解?节点i 到 节点j 的最短路径中 一定是经过很多节点,那么这个集合用[1...k] 来表示。反过来想,节点i 到 节点j 中间一定经过很多节点,那么你能用什么方式来表述中间这么多节点呢?所以 这里的k不能单独指某个节点,k 一定要表示一个集合,即[1...k] ,表示节点1 到 节点k 一共k个节点的集合。

  • 确定递推公式

节点1 到 节点9 的最短距离 可以由 节点1 到节点5的最短距离 + 节点5到节点9的最短距离组成, 也可以有 节点1 到节点7的最短距离 + 节点7 到节点9的最短距离的距离组成,要选一个最小的,毕竟是求最短路。

分两种情况:

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 是不确定的,把k 赋值为 0,本题 节点0 是无意义的,节点是从1 到 n。在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。grid数组是一个三维数组,那么我们初始化的数据在 i 与 j 构成的平层。

红色的 底部一层是我们初始化好的数据,注意:从三维角度去看初始化的数据很重要,影响遍历顺序
  • 确定遍历顺序

初始化是把 k =0 的 i 和j 对应的数值都初始化了,这样才能去计算 k = 1 的时候 i 和 j 对应的数值。这就好比是一个三维坐标,i 和j 是平层,而k 是 垂直向上 的。遍历的顺序是从底向上 一层一层去遍历。所以遍历k 的for循环一定是在最外面,这样才能一层一层去遍历,遍历 i 和 j 的话,for 循环的先后顺序无所谓。

import java.util.Scanner;public class Main {// MAX_VAL 要大于最大边权(10^4),但不能太大导致加法溢出public static int MAX_VAL = 10005;public static void main(String[] args) {Scanner sc = new Scanner(System.in);// 1. 输入景点数 N 和道路数 Mint n = sc.nextInt();int m = sc.nextInt();// 创建三维数组:grid[i][j][k] 表示 i -> j,只允许经过节点 1~k 时的最短距离int[][][] grid = new int[n + 1][n + 1][n + 1];// 初始化:所有距离为 MAX_VAL(表示不可达)for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {for (int k = 0; k <= n; k++) {grid[i][j][k] = MAX_VAL;}}}// 自身到自身距离为 0(对所有 k 都成立)for (int k = 0; k <= n; k++) {for (int i = 1; i <= n; i++) {grid[i][i][k] = 0;}}// 输入 M 条双向道路for (int i = 0; i < m; i++) {int u = sc.nextInt();int v = sc.nextInt();int w = sc.nextInt();// 初始化 k=0 层:不允许任何中间节点,只能走直接边if (w < grid[u][v][0]) {grid[u][v][0] = w;grid[v][u][0] = w; // 无向图}}// Floyd-Warshall 三维递推// k: 当前允许使用的中间节点编号(从 1 到 n)for (int k = 1; k <= n; k++) {for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {int notUseK = grid[i][j][k - 1];                    // 不经过节点 kint useK = grid[i][k][k - 1] + grid[k][j][k - 1];   // 经过节点 kif (grid[i][k][k - 1] < MAX_VAL && grid[k][j][k - 1] < MAX_VAL) {grid[i][j][k] = Math.min(notUseK, useK);} else {grid[i][j][k] = notUseK; // 防止溢出}}}}// 查询int q = sc.nextInt(); // 观景计划数量for (int i = 0; i < q; i++) {int start = sc.nextInt();int end = sc.nextInt();int result = grid[start][end][n]; // 使用所有节点中转后的最短距离if (result >= MAX_VAL) {System.out.println(-1);} else {System.out.println(result);}}sc.close();}
}

A * 算法精讲 (A star算法)

127. 骑士的攻击

看到这道题目的第一个想法就是广搜,这也是最经典的广搜类型题目。因为本题地图足够大,且 n 也有可能很大,导致有非常多的查询,所以会超时。我们来看一下广搜的搜索过程,如图,红色是起点,绿色是终点,黄色是要遍历的点,最后从 起点 找到 达到终点的最短路径是棕色。

可以看出做了很多无用的遍历, 黄色的格子是广搜遍历到的点

关于Astar算法:广搜的改良版。 有的是 Astar是 dijkstra 的改良版。如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密)如果是有权图(边有不同的权值),优先考虑 dijkstra。而 Astar 关键在于 启发式函数, 也就是 影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。

使用A * 搜索过程

可以发现 BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索

 A * 有方向性的去搜索关键在于 启发式函数。启发式函数落实到代码处,如果指引搜索的方向?从队列里取出什么元素,接下来就是从哪里开始搜索。所以 启发式函数 要影响的就是队列里元素的排序!这是影响BFS搜索方向的关键。

对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢?每个节点的权值为F,给出公式为:F = G + H。G:起点达到目前遍历节点的距离,H:目前遍历的节点到达终点的距离,起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。

本题的图是无权网格状,在计算两点距离通常有如下三种计算方式:

  1. 曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2)
  2. 欧氏距离(欧拉距离) ,计算方式:d = sqrt( (x1-x2)^2 + (y1-y2)^2 )
  3. 切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2))

x1, x2 为起点坐标,y1, y2 为终点坐标 ,abs 为求绝对值,sqrt 为求开根号,选择哪一种距离计算方式 也会导致 A * 算法的结果不同。本题,采用欧拉距离才能最大程度体现 点与点之间的距离。所以 使用欧拉距离计算 和 广搜搜出来的最短路的节点数是一样的。 (路径可能不同,但路径上的节点数是相同的)

曼哈顿、欧拉以及契比雪夫 三种计算方式下,A * 算法的寻路过程,动画地址:https://kamacoder.com/tools/knight.html

计算出来 F 之后,按照 F 的 大小,来选去出队列的节点。可以使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。

拓展:

        A * 算法并不是一个明确的最短路算法,A* 算法搜的路径如何,完全取决于 启发式函数怎么写,并不能保证一定是最短路,因为在设计启发式函数的时候,要考虑 时间效率与准确度之间的一个权衡。虽然本题中,A * 算法得到是最短路,也是因为本题 启发式函数 和 地图结构都是最简单的。例如在游戏中,在地图很大、不同路径权值不同、有障碍 且多个游戏单位在地图中寻路的情况,如果要计算准确最短路,耗时很大,会给玩家一种卡顿的感觉。

        所以在游戏开发设计中,保证运行效率的情况下,A * 算法中的启发式函数 设计往往不是最短路,而是接近最短路的 次短路设计

import java.util.*;public class Main {// 马走日的8个方向偏移量 (dx, dy)private static final int[] dx = {-2, -2, -1, -1, 1, 1, 2, 2};private static final int[] dy = {-1, 1, -2, 2, -2, 2, -1, 1};// 棋盘边界:1 到 1000private static final int MIN = 1;private static final int MAX = 1000;// A*搜索中的节点,包含坐标、g值(起点到当前步数)、h值(启发值)、f值(g+h)static class Node {int x, y;int g, h, f;public Node(int x, int y, int g, int h) {this.x = x;this.y = y;this.g = g;this.h = h;this.f = g + h; // f = g + h,用于优先队列排序}}public static void main(String[] args) {Scanner sc = new Scanner(System.in);int n = sc.nextInt(); // 读取测试用例数量// 处理每个查询for (int i = 0; i < n; i++) {int a1 = sc.nextInt(); // 起点xint a2 = sc.nextInt(); // 起点yint b1 = sc.nextInt(); // 终点xint b2 = sc.nextInt(); // 终点yint result = astar(a1, a2, b1, b2); // A*搜索最短步数System.out.println(result); // 输出结果}sc.close();}// A*算法:计算从(sx,sy)到(tx,ty)的最少移动步数private static int astar(int sx, int sy, int tx, int ty) {if (sx == tx && sy == ty) return 0; // 起点即终点// 优先队列:按f值从小到大排序,优先扩展最有希望的节点PriorityQueue<Node> pq = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));// gScore[i][j]:记录到达(i,j)的最小步数(g值)int[][] gScore = new int[MAX + 1][MAX + 1];// 初始化所有位置的g值为无穷大for (int i = 1; i <= MAX; i++)Arrays.fill(gScore[i], Integer.MAX_VALUE);// 计算起点的启发值int h0 = heuristic(sx, sy, tx, ty);pq.offer(new Node(sx, sy, 0, h0)); // 将起点加入队列gScore[sx][sy] = 0; // 起点步数为0while (!pq.isEmpty()) {Node current = pq.poll(); // 取出f值最小的节点int x = current.x, y = current.y, g = current.g;// 如果当前g值大于已记录的最优g值,跳过(旧数据)if (g > gScore[x][y]) continue;// 尝试8个马走日方向for (int i = 0; i < 8; i++) {int nx = x + dx[i], ny = y + dy[i]; // 新坐标// 检查是否越界if (nx < MIN || nx > MAX || ny < MIN || ny > MAX) continue;int newG = g + 1; // 到新位置的步数// 如果到达目标点,直接返回步数if (nx == tx && ny == ty) return newG;// 如果找到更短路径,更新gScore并加入队列if (newG < gScore[nx][ny]) {gScore[nx][ny] = newG; // 更新最短步数int h = heuristic(nx, ny, tx, ty); // 计算启发值pq.offer(new Node(nx, ny, newG, h)); // 加入优先队列}}}return -1; // 理论上不会到达这里(骑士总能到达任意点)}// 启发函数:估计从(x,y)到(tx,ty)的最小步数(不会高估)// 使用 (dx² + dy²) / 5 作为启发值,避免sqrt提高效率,且保证A*最优性private static int heuristic(int x, int y, int tx, int ty) {int dx = Math.abs(x - tx);int dy = Math.abs(y - ty);return (dx * dx + dy * dy) / 5;}
}

二、总结

最短路算法总结

四大最短路算法,分别是Dijkstra、Bellman_ford、SPFA 和 Floyd。

  • dijkstra朴素版
  • dijkstra堆优化版
  • Bellman_ford
  • Bellman_ford 队列优化算法(又名SPFA)
  • bellman_ford 算法判断负权回路
  • bellman_ford之单源有限最短路
  • Floyd 算法精讲
  • 启发式搜索:A * 算法
因为A * 属于启发式搜索,和上面最短路算法并不是一类,不适合一起对比,所以没有放在一起

大体使用场景的分析:

如果遇到单源且边为正数,直接Dijkstra

至于 使用朴素版还是 堆优化版 还是取决于图的稠密度, 多少节点多少边算是稠密图,多少算是稀疏图,这个没有量化,如果想量化只能写出两个版本然后做实验去测试,不同的判题机得出的结果还不太一样。一般情况下,可以直接用堆优化版本。

如果遇到单源边可为负数,直接 Bellman-Ford,同样 SPFA 还是 Bellman-Ford 取决于图的稠密度。一般情况下,直接用 SPFA。

如果有负权回路,优先 Bellman-Ford, 如果是有限节点最短路 也优先 Bellman-Ford,理由是写代码比较方便。

如果是遇到多源点求最短路,直接 Floyd。除非 源点特别少,且边都是正数,那可以 多次 Dijkstra 求出最短路径,但这种情况很少,一般出现多个源点了,就是想让你用 Floyd 了。

对于A * ,由于其高效性,所以在实际工程应用中使用最为广泛 ,由于其 结果的不唯一性,也就是可能是次短路的特性,一般不适合作为算法题。游戏开发、地图导航、数据包路由等都广泛使用 A * 算法。

http://www.dtcms.com/a/351471.html

相关文章:

  • ElementUI之菜单(Menu)使用
  • 美团购物车小球动画效果
  • Docker Compose 使用指南 - 1Panel 版
  • 国产化芯片ZCC3790--同步升降压控制器的全新选择, 替代LT3790
  • 第17章|PowerShell 安全警报——高分学习笔记(运维实战向)
  • Tableau Server高危漏洞允许攻击者上传任意恶意文件
  • 数据库云平台:提升运维效率与降低成本的有效工具
  • 【Ubuntu系统实战】一站式部署与管理MySQL、MongoDB、Redis三大数据库
  • WPS 智能文档,5分钟上手!
  • React学习教程,从入门到精通, React教程:构建你的第一个 React 应用(1)
  • 电力时序预测相关论文
  • 物流配送路径规划项目方案
  • yggjs_rbutton React按钮组件v1.0.0 最佳实践指南
  • 从陪聊到客服,声网如何支撑AI实时交互?
  • Rust 登堂 之 函数式编程(三)
  • 面试之JVM
  • CentOS 7 服务器初始化:从 0 到 1 的安全高效配置指南
  • 使用 flutter_tts 的配置项
  • C# 13 中的新增功能实操
  • 深入了解AWS Auto Scaling
  • OpenAI API Python实战教程:如何稳定获取结构化 JSON 输出(简易/复杂 双示例)
  • Nginx Ubuntu vs CentOS 常用命令对照表---详解笔记
  • AR技术引领航空制造迈向智能化新时代
  • Java标识符命名规则与规范
  • 32.Attention-注意力机制
  • 【算法--链表题2】19.删除链表的倒数第 N 个节点:通俗详解
  • A股大盘数据-20250826 分析
  • Java大厂面试实战:从Spring Boot到微服务架构的全链路技术剖析
  • 英伟达jetson开发板Ubuntu系统配置显示屏系统脱离手动输入指令自动编译执行操作
  • InnoDB详解2