代码随想录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 是维护边的集合。
拓扑排序怎么写。
最短路算法不同场景用什么,分别怎么写。