代码随想录Day53图:Floyd算法精讲_ Astar算法精讲_最短路算法总结篇_图论总结

代码随想录Day53图:Floyd算法精讲_ Astar算法精讲_最短路算法总结篇_图论总结

Floyd算法精讲

题目:小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。

输入描述:第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。接下里的一行包含一个整数 Q,表示观景计划的数量。接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。

输出描述:对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。

链接:https://kamacoder.com/problempage.php?pid=1155

之前都是单源最短路,本题是多源最短路径。

Floyd 算法对边的权值正负没有要求,都可以处理。Floyd算法核心思想是动态规划。

例如:求节点1 到 节点9 的最短距离,用二维数组来表示即:grid[1][9],如果最短距离是10 ,那就是 grid[1][9] = 10。那节点1到节点9的最短距离是不是可以由节点1到节点5的最短距离 + 节点5到节点9的最短距离组成呢?即 grid[1][9] = grid[1][5] + grid[5][9]。节点1到节点5的最短距离是不是可以有节点1到节点的最短距离 + 节点3到节点5的最短距离组成呢?即 grid[1][5] = grid[1][3] + grid[3][5]。以此类推,节点1到节点3的最短距离可以由更小的区间组成。

Floyd的动规五部曲:

1确定dp数组以及下标的含义

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

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

2确定递推公式

我们分两种情况:

节点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]。

节点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])

3 dp数组如何初始化

grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合为中间节点的最短距离为m。

刚开始初始化k 是不确定的。例如题目中只是输入边(节点2 -> 节点6,权值为3),那么grid[2][6][k] = 3,k需要填什么呢?把k 填成1,那如何上来就知道节点2经过节点1到达节点6的最短距离是多少呢。所以只能把k 赋值为 0,本题节点0是无意义的,节点是从1 到 n。这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是节点i 经过节点1到达节点j的最小距离了。

grid数组中其他元素数值应该初始化多少呢?本题求的是最小值,所以输入数据没有涉及到的节点的情况都应该初始为一个最大数。这样才不会影响,每次计算去最小值的时候 初始值对计算结果的影响。

4 确定遍历顺序

根据递推公式可以看出,k 依赖于 k - 1, i 和j并不依赖与 i - 1 或者 j - 1 等等。

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

5举例推导dp数组

java 复制代码
import java.util.*;

public class Main{
    public static int MAX_VAL = 10005;

    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();//景点数量
        int m=sc.nextInt();//道路数量

        int[][][] grid=new int[n+1][n+1][n+1];
        //初始化grid
        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]=grid[j][i][k]=MAX_VAL;
                }
            }
        }
        while(m-->0){
            int u=sc.nextInt();//景点u
            int v=sc.nextInt();//景点v
            int weight=sc.nextInt();//长度
            grid[u][v][0]=grid[v][u][0]=weight;
        }

        //动规递推
        for(int k=1;k<=n;k++){
            for(int i=1;i<=n;i++){
                for(int j=1;j<=n;j++){
                    grid[i][j][k]=Math.min(grid[i][k][k-1]+grid[k][j][k-1],grid[i][j][k-1]);
                }
            }
        }

        int x=sc.nextInt(); //观景计划的数量
        while(x-->0){
            int src=sc.nextInt();
            int dst=sc.nextInt();
            if(grid[src][dst][n]==MAX_VAL){
                System.out.println("-1");
            }else{
                System.out.println(grid[src][dst][n]);
            }
        }
    }
}

Astar算法精讲

题目:在象棋中,马和象的移动规则分别是"马走日"和"象走田"。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界)

输入描述:第一行包含一个整数 n,表示测试用例的数量。接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。

输出描述:输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。

输入示例:6

5 2 5 4

1 1 2 2

1 1 8 8

1 1 8 7

2 1 3 3

4 6 4 6

链接:https://kamacoder.com/problempage.php?pid=1203

会想到广搜但是超时了。因为本题地图足够大,且 n 也有可能很大,导致有非常多的查询。广搜中做了很多无用的遍历,黄色的格子是广搜遍历到的点。这里我们能不能让遍历方向,向这终点的方向去遍历呢?这样我们就可以避免很多无用遍历。

BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索。A * 为什么可以有方向性的去搜索,它的如何知道方向呢?其关键在于启发式函数。

在广搜中,从队列里取出什么元素,接下来就是从哪里开始搜索。所以启发式函数要影响的就是队列里元素的排序。这是影响BFS搜索方向的关键。

对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢?

每个节点的权值为F,给出公式为:F = G + H。G:起点达到目前遍历节点的距离。H:目前遍历的节点到达终点的距离。起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。

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

曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2)

欧氏距离(欧拉距离) ,计算方式:d = sqrt( (x1-x2)^2 + (y1-y2)^2 )

切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2))

x1, x2 为起点坐标,y1, y2 为终点坐标 ,abs 为求绝对值,sqrt 为求开根号。

本题采用欧拉距离才能最大程度体现点与点之间的距离。

java 复制代码
import java.util.PriorityQueue;
import java.util.Scanner;

public class Main{
    // 方向数组:骑士的8种走法
    private static final int[][] DIR = {
        {-2, -1}, {-2, 1}, {-1, 2}, {1, 2},
        {2, 1}, {2, -1}, {1, -2}, {-1, -2}
    };
    
    // 棋盘大小(1~1000)
    private static final int SIZE = 1000;
    
    // 记录从起点到每个点的步数
    private static int[][] moves;
    
    // 目标坐标
    private static int targetX, targetY;
    
    // 骑士节点类,实现Comparable用于优先队列排序(按f值升序)
    static class Knight implements Comparable<Knight> {
        int x, y;       // 当前坐标
        int g, h, f;    // g: 已走代价, h: 启发式估计, f = g + h
        
        public Knight(int x, int y, int g, int h) {
            this.x = x;
            this.y = y;
            this.g = g;
            this.h = h;
            this.f = g + h;
        }
        
        @Override
        public int compareTo(Knight other) {
            return Integer.compare(this.f, other.f);
        }
    }
    
    // 启发式函数:返回当前点到目标点的欧几里得距离的平方(不开根号,提高精度)
    private static int heuristic(int x, int y) {
        int dx = x - targetX;
        int dy = y - targetY;
        return dx * dx + dy * dy;
    }
    
    // A*算法主逻辑
    private static void astar(Knight start) {
        PriorityQueue<Knight> pq = new PriorityQueue<>();
        pq.offer(start);
        
        while (!pq.isEmpty()) {
            Knight cur = pq.poll();
            
            // 到达目标点
            if (cur.x == targetX && cur.y == targetY) {
                break;
            }
            
            // 遍历八个方向
            for (int[] d : DIR) {
                int nx = cur.x + d[0];
                int ny = cur.y + d[1];
                
                // 检查边界
                if (nx < 1 || nx > SIZE || ny < 1 || ny > SIZE) {
                    continue;
                }
                
                // 如果该位置尚未访问过
                if (moves[nx][ny] == 0) {
                    // 更新步数(从当前节点移动一步)
                    moves[nx][ny] = moves[cur.x][cur.y] + 1;
                    
                    // 计算新节点的g, h, f
                    int newG = cur.g + 5; // 马步代价平方为 1^2 + 2^2 = 5
                    int newH = heuristic(nx, ny);
                    Knight next = new Knight(nx, ny, newG, newH);
                    pq.offer(next);
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt(); // 测试用例数量
        
        while (n-- > 0) {
            int a1 = sc.nextInt(); // 起点x
            int a2 = sc.nextInt(); // 起点y
            targetX = sc.nextInt(); // 目标x
            targetY = sc.nextInt(); // 目标y
            
            // 初始化步数数组(自动填充0)
            moves = new int[SIZE + 1][SIZE + 1]; // 索引0不使用,1~1000有效
            
            // 创建起始节点
            int startH = heuristic(a1, a2);
            Knight start = new Knight(a1, a2, 0, startH);
            
            // 执行A*搜索
            astar(start);
            
            // 输出结果(目标点的步数,若未到达则为0)
            System.out.println(moves[targetX][targetY]);
        }
        
        sc.close();
    }
}

A * 算法并不是一个明确的最短路算法,A * 算法搜的路径如何,完全取决于启发式函数怎么写。A * 算法并不能保证一定是最短路,因为在设计启发式函数的时候,要考虑时间效率与准确度之间的一个权衡。虽然本题中,A * 算法得到是最短路,也是因为本题 启发式函数 和 地图结构都是最简单的。

例如在游戏中,在地图很大、不同路径权值不同、有障碍 且多个游戏单位在地图中寻路的情况,如果要计算准确最短路,耗时很大,会给玩家一种卡顿的感觉。而真实玩家在玩游戏的时候,并不要求一定是最短路,次短路也是可以的 (玩家不一定能感受出来,及时感受出来也不是很在意),只要奔着目标走过去 大体就可以接受。所以 在游戏开发设计中,保证运行效率的情况下,A * 算法中的启发式函数 设计往往不是最短路,而是接近最短路的 次短路设计。

最短路算法总结篇

大体使用场景的分析:

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

(dijkstra三部曲:

第一步,选源点到哪个节点近且该节点未被访问过

第二步,该最近节点被标记访问过

第三步,更新非访问节点到源点的距离(即更新minDist数组)

在dijkstra算法中minDist数组用来记录每一个节点距离源点的最小距离。)

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

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

(Bellman_ford算法的核心思想是对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路。)

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

(Floyd算法核心思想是动态规划。动规五部曲。)

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

图论总结

两种图的存储方式怎么写。

深搜广搜的模板怎么写。

并查集的模板怎么写。为什么要用并查集。并查集能解决哪些问题,哪些场景会用到并查集。

最小生成树树是所有节点的最小连通子图。最小生成树的两个算法prim 算法是维护节点的集合,而 Kruskal 是维护边的集合。

拓扑排序怎么写。

最短路算法不同场景用什么,分别怎么写。

相关推荐
小小unicorn2 小时前
[微服务即时通讯系统]语音子服务的实现与测试
c++·算法·微服务·云原生·架构·xcode
lihihi2 小时前
P10471 最大异或对 The XOR Largest Pair
算法
漫随流水2 小时前
备战蓝桥杯(3)
数据结构·c++·算法·蓝桥杯
song8546011342 小时前
hash和history导航区别 个别服务器为啥不支持 history 模式
服务器·算法·哈希算法
IT猿手2 小时前
多无人机动态避障路径规划研究:基于粒子群优化算法PSO的多无人机动态避障路径规划研究(可以自定义无人机数量及起始点),MATLAB代码
算法·matlab·机器人·无人机·路径规划·动态路径规划
MoonBit月兔2 小时前
MoonBit 0.8.3版本更新
开发语言·人工智能·算法·ai编程·moonbit
xsyaaaan2 小时前
leetcode-hot100-哈希表:1两数之和-49字母异位词分组-128最长连续序列
算法·leetcode·散列表
代码探秘者2 小时前
【Redis】双写一致性:延迟双删 / 读写锁 / 异步通知 / Canal,一文全解
java·数据库·redis·后端·算法·缓存
小指纹2 小时前
2026牛客寒假算法基础集训营1
算法·macos·cocoa