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

算法训练营DAY60 第十一章:图论part11

Floyd 算法精讲

卡码网:97. 小明逛公园(opens new window)

【题目描述】

小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。

给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。

小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。

【输入描述】

第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。

接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。

接下里的一行包含一个整数 Q,表示观景计划的数量。

接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。

【输出描述】

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

【输入示例】

7 3 1 2 4 2 5 6 3 6 8 2 1 2 2 3

【输出示例】

4 -1

【提示信息】

从 1 到 2 的路径长度为 4,2 到 3 之间并没有道路。

1 <= N, M, Q <= 1000.

Floyd 算法思路

Floyd算法核心思想是动态规划。

动规五部曲:

  • 确定dp数组(dp table)以及下标的含义
  • 确定递推公式
  • dp数组如何初始化
  • 确定遍历顺序
  • 举例推导dp数组

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

这里我们用 grid数组来存图,那就把dp数组命名为 grid。

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

2、确定递推公式

  1. 节点i 到 节点j 的最短路径经过节点k;grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]
  2. 节点i 到 节点j 的最短路径不经过节点k;grid[i][j][k] = 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数组如何初始化

底部一层是需要初始化的数据,根据输入初始化,注意本题是无向图;其他没有边连接的部分,初始化为一个相对比较大的数字,同时避免计算中溢出,我们选择10005;

4、确定遍历顺序

从递推公式可以看出,k层的grid依赖于上一层k-1的结果,所以三层for循环k循环要在最外层,i、j顺序不影响结果;

5、举例推导dp数组

可以一层一层打印出来去分析。

#include <iostream>
#include <vector>
#include <climits>
using namespace std;int main(){int n,m,s,t,val;cin>>n>>m;vector<vector<vector<int>>> grid(n+1,vector<vector<int>>(n+1,vector<int>(n+1,10005)));for(int i=0;i<m;i++){cin>>s>>t>>val;grid[s][t][0]=val;grid[t][s][0]=val;}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 z,start,end;cin>>z;for(int i=0;i<z;i++){cin>>start>>end;if(grid[start][end][n]==10005) cout<<-1<<endl;else cout<<grid[start][end][n]<<endl;}
}

二维数组优化

#include <iostream>
#include <vector>
using namespace std;int main() {int n, m, p1, p2, val;cin >> n >> m;vector<vector<int>> grid(n + 1, vector<int>(n + 1, 10005));  // 因为边的最大距离是10^4for(int i = 0; i < m; i++){cin >> p1 >> p2 >> val;grid[p1][p2] = val;grid[p2][p1] = val; // 注意这里是双向图}// 开始 floydfor (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 z, start, end;cin >> z;while (z--) {cin >> start >> end;if (grid[start][end] == 10005) cout << -1 << endl;else cout << grid[start][end] << endl;}
}

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

卡码网:126. 骑士的攻击(opens new window)

题目描述

在象棋中,马和象的移动规则分别是“马走日”和“象走田”。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。

骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。

棋盘大小 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

输出示例

2
4
6
5
1
0

思路

这道题目的第一个想法就是广搜,这也是最经典的广搜类型题目。

广搜中,做了很多无用的遍历, 我们能不能让便利方向,向这终点的方向去遍历呢?从而避免很多无用遍历。

Astar(A*)

Astar 是一种 广搜的改良版。 有的是 Astar是 dijkstra 的改良版。

其实只是场景不同而已 我们在搜索最短路的时候, 如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密)

如果是有权图(边有不同的权值),优先考虑 dijkstra。

而 Astar 关键在于 启发式函数, 也就是 影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。

这里使用BFS版本的A*;

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

每个节点的权值为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))

代码编写

用moves记录从起点到达每个点的最短步数;

dir表示可能的移动方向;

目标点用全局变量表示

构造Knight结构体,重载运算符

这个重载运算符定义了当两个Knight对象使用<运算符时的行为:

  • 如果k.f < f为真,返回true

  • 这意味着当前对象的f值比参数对象的f值大时,当前对象被视为"小于"参数对象

这会导致在排序时,f值较大的对象会排在前面(降序排列)

g用每一步增加5的欧氏距离来表示,表示日字形斜边的平方

h用欧式距离计算d = sqrt( (x1-x2)^2 + (y1-y2)^2 )

f=g+h;

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

#include<iostream>
#include<queue>
#include<string.h>
using namespace std;
int moves[1001][1001];
int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-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 Heuristic(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;// 开始计算Fnext.g = cur.g + 5; // 统一不开根号,这样可以提高精度,马走日,1 * 1 + 2 * 2 = 5next.h = Heuristic(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 = Heuristic(start);start.f = start.g + start.h;astar(start);while(!que.empty()) que.pop(); // 队列清空cout << moves[b1][b2] << endl;}return 0;
}

复杂度分析

最坏情况下,A * 退化成广搜,算法的时间复杂度 是 O(n * 2),n 为节点数量。

最佳情况,是从起点直接到终点,时间复杂度为 O(dlogd),d 为起点到终点的深度。

A * 算法的空间复杂度 O(b ^ d) ,d 为起点到终点的深度,b 是 图中节点间的连接数量,本题因为是无权网格图,所以 节点间连接数量为 4。

拓展

A * 算法 并不是一个明确的最短路算法,A * 算法搜的路径如何,完全取决于 启发式函数怎么写

A * 算法并不能保证一定是最短路,因为在设计 启发式函数的时候,要考虑 时间效率与准确度之间的一个权衡。

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

A * 的缺点

相对 普通BFS,A * 算法只从 队列里取出 距离终点最近的节点。

A * 在一次路径搜索中,大量不需要访问的节点都在队列里,会造成空间的过度消耗。

如果题目中,给出 多个可能的目标,然后在这多个目标中 选择最近的目标,这种 A * 就不擅长了, A * 只擅长给出明确的目标 然后找到最短路径。

如果是多个目标找最近目标(特别是潜在目标数量很多的时候),可以考虑 Dijkstra ,BFS 或者 Floyd。


文章转载自:

http://02z1RjKK.yxwnn.cn
http://lpBCKU5E.yxwnn.cn
http://4qc07IWn.yxwnn.cn
http://IRDI5xGg.yxwnn.cn
http://9xfdfM5c.yxwnn.cn
http://9CkfbO2H.yxwnn.cn
http://aZircNF6.yxwnn.cn
http://Mkk2018u.yxwnn.cn
http://pt6Fz2qw.yxwnn.cn
http://4T9yLzlj.yxwnn.cn
http://iR23x6tT.yxwnn.cn
http://4Fp8tNW0.yxwnn.cn
http://9iRIckC7.yxwnn.cn
http://bKJN0p2V.yxwnn.cn
http://VeNI0MIT.yxwnn.cn
http://eUk6dQkU.yxwnn.cn
http://dnBZLtdP.yxwnn.cn
http://GC58FQyE.yxwnn.cn
http://Ceko8giv.yxwnn.cn
http://3RUxqpxM.yxwnn.cn
http://VsGwNX7j.yxwnn.cn
http://0kArXruW.yxwnn.cn
http://GjRXTQnf.yxwnn.cn
http://8Bbqih6o.yxwnn.cn
http://qznx4lKk.yxwnn.cn
http://iKD9yxui.yxwnn.cn
http://MF0saCuW.yxwnn.cn
http://5dRY2iZ2.yxwnn.cn
http://i8bSWGMf.yxwnn.cn
http://Vmnqfhgn.yxwnn.cn
http://www.dtcms.com/a/379816.html

相关文章:

  • java 反射Class类/加载类/创建对象及方法
  • RL【9】:Policy Gradient
  • Java短链接生成服务实战指南
  • JAVA Web —— A / 网页开发基础
  • TensorFlow深度学习实战:从零开始构建你的第一个神经网络
  • Keepalived 负载均衡
  • 智能文档处理业务,应该选择大模型还是OCR专用小模型?
  • 《Redis核心机制解析》
  • Netty 在 API 网关中的应用篇(请求转发、限流、路由、负载均衡)
  • 金蝶云星空插件开发记录(一)
  • Knockout-ES5 入门教程
  • 基于 Art_DAQ、InfluxDB 和 PyQt 的传感器数据采集、存储与可视化
  • 【图像处理基石】图像压缩有哪些经典算法?
  • C语言实战:简单易懂通讯录
  • youte-agent部署(windows)
  • Python实现点云法向量各种方向设定
  • Linnux IPC通信和RPC通信实现的方式
  • apache实现LAMP+apache(URL重定向)
  • MongoDB 与 GraphQL 结合:现代 API 开发新范式
  • k8s-临时容器学习
  • uni-app 根据用户不同身份显示不同的tabBar
  • ubuntu18.04安装PCL1.14
  • Ubuntu 系统下 Anaconda 完整安装与环境配置指南(附常见问题解决)
  • 网络链路分析笔记mtr/traceroute
  • 在 Ubuntu 系统中利用 conda 创建虚拟环境安装 sglang 大模型引擎的完整步骤、版本查看方法、启动指令及验证方式
  • 基带与射频的区别与联系
  • 《企业安全运营周报》模板 (极简实用版)​
  • opencv基于SIFT特征匹配的简单指纹识别系统实现
  • Node.js 操作 Elasticsearch (ES) 的指南
  • 使用tree命令导出文件夹/文件的目录树( Windows 和 macOS)