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

代码随想录算法训练营 Day61 图论ⅩⅠ Floyd A※ 最短路径算法

图论

题目

97. 小明逛公园
本题是经典的多源最短路问题。
在这之前我们讲解过,dijkstra朴素版、dijkstra堆优化、Bellman算法、Bellman队列优化(SPFA) 都是单源最短路,即只能有一个起点。
而本题是多源最短路,即求多个起点到多个终点的多条最短路径。
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的最短距离 + 节点3 到节点5 的最短距离组成呢? 即 grid[1][5] = grid[1][3] + grid[3][5]
以此类推,节点1 到节点3的最短距离可以由更小的区间组成。
那么这样我们是不是就找到了,子问题推导求出整体最优方案的递归关系呢。
是不是 要选一个最小的,毕竟是求最短路。此时我们已经接近明确递归公式了。之前在讲解动态规划的时候,给出过动规五部曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
具体讲解
1. Dp 数组表示当前节点 i 到节点 j 以 [1,...,k] 集合节点为中间节点的最短距离grid[i][j][k] = m
节点i 到 节点j 的最短距离为m,这句话可以理解,但 以[1...k]集合为中间节点就理解不了。
节点i 到节点j 的最短路径中一定是经过很多节点,那么这个集合用[1...k] 来表示。
你可以反过来想,节点i 到节点j 中间一定经过很多节点,那么你能用什么方式来表述中间这么多节点呢?所以这里的k不能单独指某个节点,k 一定要表示一个集合,即 [1...k] ,表示节点1 到节点k 一共k个节点的集合。
2. 确定递推公式,我们分两种情况:
1. 节点i 到节点j 的最短路径经过节点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]
第二种情况,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数组是一个三维数组,那么我们初始化的数据在 i 与 j 构成的平层,如图:在这里插入图片描述

有初始化代码

vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));  // C++定义了一个三位数组,10005是因为边的最大距离是10^4for(int i = 0; i < m; i++){cin >> p1 >> p2 >> val;grid[p1][p2][0] = val;grid[p2][p1][0] = val; // 注意这里是双向图
} 

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

// C++写法,定义了一个三位数组,10005是因为边的最大距离是10^4
vector<vector<vector<int>>> grid(n + 1, vector<vector<int>>(n + 1, vector<int>(n + 1, 10005)));  
  1. 遍历顺序
    从递推公式:grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1]) 可以看出,我们需要三个for循环,分别遍历i,j 和k

而 k 依赖于 k - 1, i 和j 的到 并不依赖与 i - 1 或者 j - 1 等等。
那么这三个for的嵌套顺序应该是什么样的呢?我们来看初始化,我们是把 k =0 的 i 和j 对应的数值都初始化了,这样才能去计算 k = 1 的时候 i 和 j 对应的数值。这就好比是一个三维坐标,i 和j 是平层,而k 是垂直向上的。遍历的顺序是从底向上一层一层去遍历。所以遍历k 的for循环一定是在最外面,这样才能一层一层去遍历。如图:
其他情况:[代码随想录](https://www.programmercarl.com/ka其他情况:
5. 打印 dp 数组

#include <iostream>
#include <vector>
#include <list>using namespace std;int main() {int n, m, x, y, val;cin >> n >> m;// 构建dp数组 最大边距离为nvector<vector<vector<int>>> grid(n+1, vector<vector<int>>(n+1, vector<int>(n+1, 10001)));// 初始化dp数组for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid[x][y][0] = val;grid[y][x][0] = val;}// foryd开始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] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]);}}}// 输出结果int q, start, end;cin >> q;while (q--) {cin >> start >> end;if (grid[start][end][n] == 10001) cout << -1 << endl;else cout << grid[start][end][n] << endl;}
}// 空间优化版本
#include <iostream>
#include <vector>using namespace std;int main() {int n, m, x, y, val;cin >> n >> m;// 使用二维数组更新 dp数组定义表明x到y直接距离最近的路径点vector<vector<int>> grid(n+1, vector<int>(n+1, 10001));// 初始化dp数组for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid[x][y] = val;grid[y][x] = val;}// forydfor (int k = 1; k <= n; ++k) {for (int i = 1; i <= n; ++i) {for (int j = 1; j<= n; ++j) {// 递推公式grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);}}}int q, start, end;cin >> q;while (q--) {cin >> start >> end;if (grid[start][end] == 10001) cout << -1 << endl;else cout << grid[start][end] << endl;}
}#include <iostream>
#include <vector>using namespace std;int main() {int n, m, x, y, val;cin >> n >> m;// 使用二维数组更新 dp数组定义表明x到y直接距离最近的路径点vector<vector<int>> grid(n+1, vector<int>(n+1, 10001));// 初始化dp数组for (int i = 0; i < m; ++i) {cin >> x >> y >> val;grid[x][y] = val;grid[y][x] = val;}// forydfor (int k = 1; k <= n; ++k) {for (int i = 1; i <= n; ++i) {for (int j = 1; j<= n; ++j) {// 递推公式grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);}}}int q, start, end;cin >> q;while (q--) {cin >> start >> end;if (grid[start][end] == 10001) cout << -1 << endl;else cout << grid[start][end] << endl;}
}

感悟
将每个点作为中间点去更新
127. 骑士的攻击
本题可以使用广度优先搜索,每次有 8 种行动路径,带入广度优先搜索,但是广搜会超时
Astar 是一种 广搜的改良版。 有的是 Astar是 dijkstra 的改良版。
其实只是场景不同而已 我们在搜索最短路的时候, 如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密)
如果是有权图(边有不同的权值),优先考虑 dijkstra。
而 Astar 关键在于启发式函数,也就是影响广搜或者 dijkstra 从容器(队列)里取元素的优先顺序。
以下,我用BFS版本的A * 来进行讲解。
BFS中,我们想搜索,从起点到终点的最短路径,要一层一层去遍历 =
而使用 Astar 算法其搜索过程是这样的,如图,图中着色的都是我们要遍历的点在这里插入图片描述

大家可以发现 **BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索**。
看出 A * 可以节省很多没有必要的遍历步骤。	那么 A * 为什么可以有方向性的去搜索,它的如何知道方向呢?**其关键在于 启发式函数**。
那么启发式函数落实到代码处,如果指引搜索的方向?
int m=q.front();q.pop();
int n=q.front();q.pop();

从队列里取出什么元素,接下来就是从哪里开始搜索。所以 启发式函数 要影响的就是队列里元素的排序
如何影响元素的选择?
对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢?
每个节点的权值为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 * 算法的结果不同。
可以使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。
实现代码如下:(启发式函数采用欧拉距离计算方式)

#include <iostream>
#include <vector>
#include <queue>
#include <cstring>using namespace std;// 棋盘网格大小
int moves[1001][1001];
// 可以走的方向
int dir[8][2] = {-1,2,-2,1,-2,-1,-1,-2,1,-2,2,-1,2,1,1,2};
int b1, b2;// F = G + H
// G = 从起点到该节点路径消耗
// H = 该节点到终点的预估消耗struct Knight {int x,y;int g,h,f;bool operator< (const Knight& k) const {return k.f < f;}
};priority_queue<Knight> que;// 欧拉距离公式
int Eular(const Knight& k) {return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2);
}void Astar(const Knight& k) {Knight cur, next;que.push(k);while (!que.empty()) {cur = que.top();que.pop();if (cur.x == b1 && cur.y == b2) break;for (int i = 0; i < 8; ++i) {next.x = cur.x + dir[i][0];next.y = cur.y + dir[i][1];if (next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) continue;if (!moves[next.x][next.y]) {moves[next.x][next.y] = moves[cur.x][cur.y] + 1;// 计算F next.g = cur.g + 5; // 5 走日 1*1 + 2*2next.h = Eular(next);next.f = next.g + next.h;que.push(next);}}}
}int main() {int n, a1, a2;cin >> n;while (n--) {cin >> a1 >> a2 >> b1 >> b2;memset(moves, 0, sizeof(moves));Knight start;start.x = a1;start.y = a2;start.g = 0;start.h = Eular(start);start.f = start.g + start.h;Astar(start);while (!que.empty()) que.pop();cout << moves[b1][b2] << endl;}return 0;
}

Astar 缺点
大家看上述 A * 代码的时候,可以看到我们想队列里添加了很多节点,但真正从队列里取出来的仅仅是靠启发式函数判断距离终点最近的节点。
相对了普通BFS,A * 算法只从队列里取出距离终点最近的节点。那么问题来了,A * 在一次路径搜索中,大量不需要访问的节点都在队列里,会造成空间的过度消耗。
IDA * 算法对这一空间增长问题进行了优化,关于 IDA * 算法,本篇不再做讲解,感兴趣的录友可以自行找资料学习。
另外还有一种场景是 A * 解决不了的。
如果题目中,给出多个可能的目标,然后在这多个目标中选择最近的目标,这种 A * 就不擅长了, A * 只擅长给出明确的目标然后找到最短路径。
如果是多个目标找最近目标(特别是潜在目标数量很多的时候),可以考虑 Dijkstra ,BFS 或者 Floyd。

图论总结

深度优先搜索广度优先搜索

在二叉树章节中,其实我们讲过了 深搜和广搜在二叉树上的搜索过程。
在图论章节中,深搜与广搜就是在图这个数据结构上的搜索过程。
深搜与广搜是图论里基本的搜索方法,大家需要掌握三点:

  • 搜索方式:深搜是可一个方向搜,不到黄河不回头。 广搜是围绕这起点一圈一圈的去搜。
  • 代码模板:需要熟练掌握深搜和广搜的基本写法。
  • 应用场景:图论题目基本上可以即用深搜也可用广搜,无疑是用哪个方便而已

并查集

并查集相对来说是比较复杂的数据结构,其实他的代码不长,但想彻底学透并查集,需要从多个维度入手,我在理论基础篇的时候 讲解如下重点:

  • 为什么要用并查集,怎么不用个二维数据,或者set、map之类的。
  • 并查集能解决那些问题,哪些场景会用到并查集
  • 并查集原理以及代码实现
  • 并查集写法的常见误区
  • 带大家去模拟一遍并查集的过程
  • 路径压缩的过程
  • 时间复杂度分析

上面这几个维度 大家都去思考了,并查集基本就学明白了。
其实理论基础篇就算是给大家出了一道裸的并查集题目了,所以在后面的题目安排中,会稍稍的拔高一些,重点在于并查集的应用上。
例如 并查集可以判断这个图是否是树,因为树的话,只有一个根,符合并查集判断集合的逻辑,题目:0108.冗余连接。
在 0109.冗余连接II 中对有向树的判断难度更大一些,需要考虑的情况比较多。

最小生成树

最小生成树是所有节点的最小连通子图, 即:以最小的成本(边的权值)将图中所有节点链接到一起。
最小生成树算法,有prim 和 kruskal。
prim 算法是维护节点的集合,而 Kruskal 是维护边的集合
在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。

边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图

Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。
Kruskal算法 时间复杂度 为 O(nlogn),其中n 为边的数量,适用稀疏图。
关于 prim算法,我自创了三部曲,来帮助大家理解

  1. 第一步,选距离生成树最近节点
  2. 第二步,最近节点加入生成树
  3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组)

大家只要理解这三部曲, prim算法 至少是可以写出一个框架出来,然后在慢慢补充细节,这样不至于 自己在写prim的时候 两眼一抹黑 完全凭感觉去写。
minDist数组 是prim算法的灵魂,它帮助 prim算法完成最重要的一步,就是如何找到 距离最小生成树最近的点
kruscal的主要思路:

  • 边的权值排序,因为要优先选最小的边加入到生成树里
  • 遍历排序后的边
    • 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环
    • 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合

而判断节点是否在一个集合 以及将两个节点放入同一个集合,正是并查集的擅长所在。
所以 Kruskal 是需要用到并查集的。

拓扑排序

拓扑排序 是在图上的一种排序。
概括来说,给出一个 有向图,把这个有向图转成线性的排序 就叫拓扑排序
同样,拓扑排序也可以检测这个有向图 是否有环,即存在循环依赖的情况。
拓扑排序的一些应用场景,例如:大学排课,文件下载依赖 等等。
只要记住如下两步拓扑排序的过程,代码就容易写了:

  1. 找到入度为0 的节点,加入结果集
  2. 将该节点从图中移除

最短路径总结

至此已经讲解了四大最短路算法,分别是 Dijkstra、Bellman_ford、SPFA 和 Floyd。
针对这四大最短路算法,我用了七篇长文才彻底讲清楚,分别是:

  • Dijkstra 朴素版
  • Dijkstra 堆优化版
  • Bellman_ford
  • Bellman_ford 队列优化算法(又名 SPFA)
  • Bellman_ford 算法判断负权回路
  • Bellman_ford 之单源有限最短路
  • Floyd 算法精讲
  • 启发式搜索:A * 算法
    在这里插入图片描述

这里我给大家一个大体使用场景的分析:
如果遇到单源且边为正数,直接 Dijkstra
至于 使用朴素版还是堆优化版还是取决于图的稠密度,多少节点多少边算是稠密图,多少算是稀疏图,这个没有量化,如果想量化只能写出两个版本然后做实验去测试,不同的判题机得出的结果还不太一样。
一般情况下,可以直接用堆优化版本。
如果遇到单源边可为负数,直接 Bellman-Ford,同样 SPFA 还是 Bellman-Ford 取决于图的稠密度。
一般情况下,直接用 SPFA。
如果有负权回路,优先 Bellman-Ford,如果是有限节点最短路也优先 Bellman-Ford,理由是写代码比较方便。如果是遇到多源点求最短路,直接 Floyd
除非源点特别少,且边都是正数,那可以多次 Dijkstra 求出最短路径,但这种情况很少,一般出现多个源点了,就是想让你用 Floyd 了。
对于 A * ,由于其高效性,所以在实际工程应用中使用最为广泛,由于其结果的不唯一性,也就是可能是次短路的特性,一般不适合作为算法题。
游戏开发、地图导航、数据包路由等都广泛使用 A * 算法。

相关文章:

  • methods的实现原理
  • Chainlink:连接 Web2 与 Web3 的去中心化桥梁
  • iOS 使用CocoaPods 添加Alamofire 提示错误的问题
  • 【Docker 新手入门指南】第十四章:Docker常用命令
  • HTML5实现简洁的端午节节日网站源码
  • 电子电路:深入了解4013D触发器的机制和原理
  • 设计模式之简单工厂模式
  • OSG编译wasm尝试
  • LVS-NAT 负载均衡群集
  • PHP7内核剖析 学习笔记 第九章 PHP基础语法的实现
  • 51. N-Queens
  • 【达梦】达梦数据库使用TypeHandler读取数据库时,将字段中的数据读取为数组
  • 用 Python 模拟雪花飘落效果
  • 【从零开始学习QT】快捷键、帮助文档、Qt窗口坐标体系
  • 集成均衡功能电池保护芯片在大功率移动电源的应用,创芯微CM1341-DAT、杰华特JW3312、赛微微电CW1244、中颖SH366006
  • 25平航杯复现
  • java队列
  • 通义灵码2.5——基于MCP打造我的12306火车票智能查询小助手
  • 人工智能在智能城市中的创新应用与未来趋势
  • 67常用控件_QTreeWidget的使用
  • wordpress gitbook/seo优化师培训
  • 青岛网站排名方案/百度度小店申请入口
  • 网站开发待遇高吗/大连网站开发公司
  • 模板网站做外贸好不好/中国十大广告公司排行榜
  • 电影网站模板html/什么是核心关键词
  • 岳各庄网站建设/搜索引擎yandex入口