一、bellman_ford

1. 是什么松弛

在《算法四》中,对松弛的解释是:relax the edge,看起来比较抽象,不过如果我们从生活中的实例去理解,就简单多了:
试想一根绳索,当你握着绳索的两头使劲用力拉伸时,绳子紧绷,长度会变长;而当你减小用力时,绳子松弛,长度会变短。当你没有用任何力时,此时绳子的长度最短。
这里当用力减小时,绳子收到的拉力减小,进而长度变短的过程,就是“松弛的过程”
体现在图论中,就是对于任意边 a->b,所谓的松弛操作就是通过 dist[b]=min(dist[b], dist[a]+g[a][b]) 来更新最短路的过程。
通过松弛操作,绳子变短,即起点到终点的距离逐渐趋近于最短路。
所以说,图论中常说的松弛操作就是一个“比喻”说法。它将最短路和绳子做类比。很有意思😊
在不同的算法中,松弛的具体操作(形式)可能不同,不过它们的本质都是相同的,那就是减小起点到终点的距离使其最终趋近于最短路。

2. 限定最短路的边数

为什么 bellman_ford 算法的松弛操作可以限定最短路?
img
从图中我们就可以发现,对所有边的松弛过程有点像“层序遍历”的过程,具体来说,以起点为出发点的层次遍历:我们每次从上一轮拓展出的结点出发,拓展下一个相邻的结点。形式上就相当于多走了一条边。对于图示,就是:
第一轮:从起点1拓展2,3
第二轮:从2,3拓展出4,5
第三轮:从4,5拓展出3,6

可以发现,3被拓展了两次

通过这我们也能证明出 bellman_ford 算法的正确性,只要我们松弛 n-1 次,那么就一定能得到起点到终点的最短路。因为此时我们是相当于枚举了所有从起点走到终点的情况。
即,先枚举走 1 条边到终点的情况;再枚举走 2 条边到终点的情况,…,最后枚举走 n-1 条边到终点的情况。并且对于走 k 条边的情况,还会枚举所有可能的路线,最后对其结果取最小值。
但是这也说明了,bellman_ford 求最短路的过程是 “暴力枚举”,因此它的时间效率不高。主要用于限定边数的最短路。

3. 不可达的点

对于不可达的点,我们的松弛会导致从起点到该点的距离减小。例如我们只有一条边 2->3,权值为 -1,那么松弛之后将导致 dist[3]=dist[2]-1。在初始状态下,dist[2]=dist[3]=INF。
你可能很聪明的想到,哎,按你这么说,bellman_ford 算法看起来完全不像层次遍历了啊,因为在初始状态下,我们只拓展了起点(1),它可以不从起点进行拓展呀(2->3)。
没错,但问题是,不从起点(上一轮拓展的点)开始拓展的点,是无意义的,因为我们不能保证其可达性,就如我们上面的说的,1->2 和 1->3 都不可达,那么从 2 开始拓展 3 有什么意义呢?

二、spfa

SPFA算法(Shortest Path Faster Algorithm),也称为 bellman_ford 队列优化算法。

SPFA的称呼来自 1994年西南交通大学段凡丁的论文,其实Bellman_ford 提出后不久 (20世纪50年代末期) 就有队列优化的版本,国际上不承认这个算法是是国内提出的。 所以国际上一般称呼 该算法为 Bellman_ford 队列优化算法(Queue improved Bellman-Ford)

1. spfa优化了什么

在上面 bellman_ford 算法中,每一轮松弛需要对所有边进行操作,时间复杂度为 O(M),效率很低。
但其实就如我们上面所提到的,每次松弛看起来就是层序遍历,而对于层序遍历来说,每一次只需要从最末端元素开始遍历即可,之前已经遍历过的元素就无需再遍历了。
其实这里的松弛操作也是类似的,对于 1->2 这条边,我们只需要在第一轮遍历即可,在第二轮和第三轮中就无需对这条边进行松弛了,因为 dist[a] 值已经确定了,它不会在变化。
由此可见,每一轮都枚举所有边是无意义的。并且,我们可以总结出:对于 a->b 这条边,只要当 dist[a] 发生变化时,松弛这条边才有意义。否则如果 dist[a] 不变,那么 dist[a]+g[a][b] 也不变,判断 dist[a]+g[a][b] 和 dist[b] 的大小从而进行松弛就完全没有意义了。
因此,我们可以用一个队列来保存这些 dist 发生变化的起始点 a,然后只对以 a 为起点的边进行松弛操作。这就是队列优化的 bellman_ford 算法。

同样我们可能理解为什么在 spfa 中,一个点可以多次入队了,就如我们上面说的,对于 2->5 这条边,我们需要在第二轮和第三轮时进行遍历。由于它需要分别在两轮进行松弛操作,那么 2 这个点就需要入队两次。每次入队就相当于一轮松弛。
同样的我们也能解释为什么 spfa 一个点不可达,但是从起点到他的距离变小了,因为 spfa 本质上就是 bellman_ford 啊,所以他继承了 bellman_ford 的这个问题。

2. 时间复杂度

如何设计卡掉spfa的数据
队列优化版Bellman_ford 的时间复杂度 并不稳定,效率高低依赖于图的结构。
例如 如果是一个双向图,且每一个节点和所有其他节点都相连的话,那么该算法的时间复杂度就接近于 Bellman_ford 的 O(N * E), N 为节点数量,E为边的数量。
在这种图中,每一个节点都会重复加入队列 n-1 次,因为 这种图中每个节点都有 n-1 条指向该节点的边,每条边指向该节点,就需要加入一次队列。
当然这种图是比较极端的情况,也是最稠密的图。所以如果图越稠密,则 SPFA的效率越接近与 Bellman_ford。反之,图越稀疏,SPFA的效率就越高。
一般来说,SPFA 的时间复杂度为 O(K * N), K 为不定值,因为节点需要计入几次队列取决于图的稠密度。
如果图是一条线形图且单向的话,每个节点的入度为1,那么只需要加入一次队列,这样时间复杂度就是 O(N)。
所以 SPFA 在最坏的情况下是 O(N * E),但一般情况下时间复杂度为 O(K * N)。
尽管如此,以上分析都是 理论上的时间复杂度分析。
并没有计算 出队列 和 入队列的时间消耗。 因为这个在不同语言上 时间消耗也是不一定的。
以C++为例,以下两段代码理论上,时间复杂度都是 O(n) :

for (long long i = 0; i < n; i++) {k++;
}
for (long long i = 0; i < n; i++) {que.push(i);que.front();que.pop();
}

在 MacBook Pro (13-inch, M1, 2020) 机器上分别测试这两段代码的时间消耗情况:
n = 10^4,第一段代码的时间消耗:1ms,第二段代码的时间消耗: 4 ms
n = 10^5,第一段代码的时间消耗:1ms,第二段代码的时间消耗: 13 ms
n = 10^6,第一段代码的时间消耗:4ms,第二段代码的时间消耗: 59 ms
n = 10^7,第一段代码的时间消耗: 24ms,第二段代码的时间消耗: 463 ms
n = 10^8,第一段代码的时间消耗: 135ms,第二段代码的时间消耗: 4268 ms
在这里就可以看出 出队列和入队列 其实也是十分耗时的。
SPFA(队列优化版Bellman_ford) 在理论上 时间复杂度更胜一筹,但实际上,也要看图的稠密程度,如果 图很大且非常稠密的情况下,虽然 SPFA的时间复杂度接近Bellman_ford,但实际时间消耗 可能是 SPFA耗时更多。

4. 负环问题

在用 spfa 求负环时,不需要建立虚拟源点。我们只需要以任意结点为起点执行 spfa 就一定能判断负环是否存在。因此只要存在符号,就一定存在负权边,就一定能进行松弛操作,就一定能入队。

3. 限制边数的最短路

既然 spfa 是 bellman_ford 的改进版,那么按理来说,spfa 也能实现限制边数的最短路问题。没错,问题是,如何在 spfa 的执行逻辑中限制松弛次数?
还是从层序遍历的思想来看,在层序遍历中,每一轮遍历需要取出当前队列中所有保存的结点。由于 spfa 是对 bellman_ford 的优化,所以 spfa 在形式上和层序遍历更相似了。
这里的实现思路和层序遍历非常相似:
题目链接

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>using namespace std;const int N = 510, M = 10010, INF = 0x3f3f3f3f;int n, m, k;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], backup[N];
bool st[N];void add(int a, int b, int c) 
{w[idx] = c;e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}int spfa()
{memset(dist, 0x3f, sizeof dist);dist[1] = 0;queue<int> q;q.push(1);st[1] = true;while(k -- ) {memset(st, false, sizeof st);// (1)这里要将所有st置位false,因为上一轮置位true的点和这一轮需要加入的点“不一样”// 上一轮入队的点,我们使用的是backup[]进行松弛的// 而这一轮加入的点,我们希望使用 dist[] 进行// 可以发现,这一轮对 dist 的更新不会体现在上一轮中,因为上一轮使用 backup 而不是 dist// 因此对于上一轮入队,st置位true的点,这一轮依然需要入队int cnt = q.size();// (2)和bellman_ford一样,使用上一轮的dist进行松弛操作memcpy(backup, dist, sizeof(dist));while(cnt -- ) // (3)遍历上一层所有加入的点,进行松弛{auto t = q.front(); q.pop();st[t] = false;for(int i = h[t]; i != -1; i = ne[i]) {int j = e[i];if(dist[j] > backup[t] + w[i]) {dist[j] = backup[t] + w[i];if(!st[j]) {q.push(j);st[j] = true;}}}}}return dist[n];
}int main()
{memset(h, -1, sizeof h);cin >> n >> m >> k;while(m -- ) {int a, b, c;   cin >> a >> b >> c;add(a, b, c);}int t = spfa();if(t >= INF / 2) puts("impossible");else    cout << t << endl;return 0;
}

通过上面的代码其实可以发现,形式上和 spfa 差不多,不过有几个细节需要注意,特别是 st 数组和 backup 数组的处理,详细内容可以看代码中的注释部分。

三、堆优化dijkstra

1. 时间复杂度分析

reference
在C++中一般通过优先队列priority_queue作为数据结构来优化dijkstra
当使用优先队列时,如果同一个点的最短路被更新多次,因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,故优先队列内的元素个数是 O(m) 的
每次从队列中取出一个元素的时间复杂度为 O(logm)。故,总的时间复杂度是 O(mlogm)

四、AStar

1. 权值转换

对于很多情景来说,我们不好直接使用 AStar 算法,特别是在需要确保“最短路”特性的前提下,找到一个合适的估价函数比较困难!例如:题目链接
在上面这道题目中,骑士按照日字形移动,如果我们使用 AStar 算法,是不容易找出一个合适的估价函数使得估价值小于等于真实距离的。
就比如,骑士可以从 [0,0] 移动到 [1,2]。此时的:

  • 哈密顿距离为 3
  • 欧拉距离为 5 \sqrt{5} 5
  • 切比雪夫距离为 2
    他们都大于骑士的实际移动距离 1,所以说,按照实际的移动权值 1 进行移动时,很难找出合适的估价函数。
    不过如果我们将这个权值 1 看作 5(=22+11,分别沿着 x 轴和 y 轴移动) 的话。然后以欧拉距离的平方作为估价函数。那么此时估价值就一定小于等于实际值。
    因此说,通过改变权值,我们可以将原本不适用的估价函数修改为合法。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <cmath>using namespace std;const int N = 1000;
const int DISTANCE = 5; // 2 * 2 + 1 * 1typedef pair<int, int> PII;int dist[N + 1][N + 1];
int dx[8] = {1, 1, -1, -1, 2, 2, -2, -2};
int dy[8] = {2, -2, 2, -2, 1, -1, 1, -1};typedef struct Node {PII p; // pointint g, h;    // f = g(grape_dist) + h(heuristic)bool operator<(const Node &rhs) const {return g + h > rhs.g + rhs.h;}
} Node; // astar_node_typeint eular_distance(PII x, PII y)
{return (x.first - y.first) * (x.first - y.first) + (x.second - y.second) * (x.second - y.second);
}int astar_bfs(PII start, PII goal)
{if(start == goal)   return 0;memset(dist, 0, sizeof dist);dist[start.first][start.second] = 1;priority_queue<Node> q;q.push({start, 5, eular_distance(start, goal)});while(q.size()) {auto t = q.top();   q.pop();auto [x, y] = t.p;int  g = t.g;for(int i = 0; i < 8; i ++ ) {int a = x + dx[i], b = y + dy[i];if(a <= 0 || a > N || b <= 0 || b > N)   continue;  if(!dist[a][b] || dist[a][b] > dist[x][y] + 1){if(a == goal.first && b == goal.second)    return dist[x][y];q.push({{a, b}, g + DISTANCE, eular_distance({a,b}, goal)});dist[a][b] = dist[x][y] + 1;   }}}return -1;  // cant move the goal
}int main()
{int T;  cin >> T;while(T -- ) {int sx, sy, ex, ey;cin >> sx >> sy >> ex >> ey;cout << astar_bfs({sx, sy}, {ex, ey}) << endl;}return 0;
}

2. 只能确保终点的最短路

在 AStar 算法中,我们只能确保从起点到终点的最短路,而不能确保从起点到其余节点的路线也是最短路。
具体来说,当终点第一次出队时,我们能确保它是最短路,但对于其余节点来说,我们并不能保证这一点。
这是因为我们的估价函数保证的是:任意节点到终点的估价值小于等于实际值。
如果我们相求非终点的最短路,需要确保任意节点到该节点的估价值小于等于实际值,但我们的估价函数并未保证这一点。
当然,很显然的,如果运气好的话,对于某些节点来说,我们的估价函数或许也满足其余节点到该节点的估计值小于等于实际值,但这完全是运气,它完全不可靠!
所以说,即使我们求出了非终点的最短路,也只是运气好而已。
因此说,由于非终点第一次出队可能不是最短路,就意味着它可能在后续再次被更新,此时就需要再次入队。这不同于 dijkstra 和 bfs 的单次入队。
因为 dijkstra 和 bfs 可以保证某个节点第一次出队时一定是最短路。但 Astar 由于估计值的原因,只能保证终点。
从上面的代码中,我们可能看出这一处理:if(!dist[a][b] || dist[a][b] > dist[x][y] + 1)
其中 dist[a][b] > dist[x][y] + 1 对某个节点做了允许多次入队处理。如果我们去掉这段代码,那么 bfs 的正确性完全看运气了。
如果此时遍历到的其余节点恰好能满足最短路的特性,程序正确;反之,如果其余节点(中间节点)不满足最短路,那么从其余节点出发走向终点的路线也必然不是最短路了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/900001.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/900001.shtml
英文地址,请注明出处:http://en.pswp.cn/news/900001.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于pycharm的YOLOv11模型训练方法

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、前期准备1.1 软件环境配置1.2 训练集参考 二、训练步骤2.1 打开文件夹2.2 打开文件2.3 data.yaml最终代码 三、train.py四、最终结果五、detect.py六、 拓展…

用nodejs连接mongodb数据库对标题和内容的全文本搜索,mogogdb对文档的全文本索引的设置以及用node-rs/jieba对标题和内容的分词

//首先我们要在Nodejs中安装 我们的分词库node-rs/jieba,这个分词不像jieba安装时会踩非常多的雷&#xff0c;而且一半的机率都是安装失败&#xff0c;node-rs/jieba比jieba库要快20-30%&#xff1b;安装分词库是为了更好达到搜索的效果 这个库直接npm install node-rs/jieba即…

水下声呐探测仪,应急救援中的高效水下定位技术|深圳鼎跃

近年来&#xff0c;随着水域活动增多及自然灾害频发&#xff0c;水下救援需求日益增长。传统人工打捞方法在复杂水域中效率低、风险高&#xff0c;尤其在能见度差、水流湍急或深水区域中&#xff0c;救援难度倍增。 在此背景下&#xff0c;水下声呐探测仪凭借其声波定位与视频…

AI 网关代理 LLMs 最佳实践

作者&#xff1a;付宇轩&#xff08;计缘&#xff09; DeepSeek/QWen 普惠 AI 趋势 随着 DeepSeek-R1 的横空出世&#xff0c;又一次点燃了原本已经有点冷淡的大语言模型市场和话题&#xff0c;并且快速成为了现象级&#xff0c;小到中小学生&#xff0c;大到父母辈都知道了中…

策略模式实际用处,改吧改吧直接用,两种方式

controller RestController RequestMapping("admin/test") RequiredArgsConstructor(onConstructor __(Autowired)) public class TestController {Autowiredprivate VideoFactory VideoFactory;GetMapping("getList")public R getList(){// 第一种方式T…

chromium魔改——修改 navigator.webdriver 检测

chromium源码官网 https://source.chromium.org/chromium/chromium/src 说下修改的chromium源码思路&#xff1a; 首先在修改源码过检测之前&#xff0c;我们要知道它是怎么检测的&#xff0c;找到他通过哪个JS的API来做的检测&#xff0c;只有知道了如何检测&#xff0c;我们…

Muduo网络库实现 [九] - EventLoopThread模块

目录 设计思路 类的设计 模块的实现 私有接口 公有接口 设计思路 我们说过一个EventLoop要绑定一个线程&#xff0c;未来该EventLoop所管理的所有的连接的操作都需要在这个EventLoop绑定的线程中进行&#xff0c;所以我们该如何实现将EventLoop和线程绑定呢&#xff1f;…

UE5学习笔记 FPS游戏制作38 继承标准UI

文章目录 UE的UIUMG的继承继承标准控件创建标准控件继承标准控件的用处 UE的UI 和Untiy有onGui和UGui类似&#xff0c;UE有slateUI和UMG,slateUI是早期只能用C编写的UI&#xff0c;UMG是现在使用的&#xff0c;可以拖拽编辑的UI slateUI是UMG的父类 UMG的继承 我们编写一个控…

C#核心学习(七)面向对象--封装(6)C#中的拓展方法与运算符重载: 让代码更“聪明”的魔法

目录 一、什么是拓展方法&#xff1f; 二、拓展方法有啥用&#xff1f;怎么写拓展方法&#xff1f; 1. ​核心用途 2. ​编写步骤 实现步骤 关键点说明 关键规则 3. ​注意事项 三、什么是运算符重载&#xff1f; 四、运算符重载有啥用&#xff1f;怎么写&#xff1f;…

银行卡归属地查询API接口如何对接?

银行卡归属地查询 API 接口是一种能让开发者通过编程方式获取银行卡归属地等相关信息的工具。借助此接口&#xff0c;开发者可将银行卡归属地查询功能集成到自己的应用程序或系统里&#xff0c;像电商平台、第三方支付公司等都能运用它来提升业务的准确性与安全性。 银行卡归属…

ORM mybits mybits-plus

ORM ORM 即对象关系映射&#xff08;Object Relational Mapping&#xff09;&#xff0c;是一种程序设计技术&#xff0c;用于实现面向对象编程语言里不同类型系统的数据之间的转换。下面从基本概念、工作原理、优势与劣势、常见的 ORM 框架等方面详细介绍 ORM。 常见的orm框架…

网络编程—网络概念

目录 1 网络分类 1.1 局域网 1.2 广域网 2 常见网络概念 2.1 交换机 2.2 路由器 2.3 集线器 2.4 IP地址 2.5 端口号 2.6 协议 3 网络协议模型 3.1 OSI七层模型 3.2 TCP/IP五层模型 3.3 每层中常见的协议和作用 3.3.1 应用层 3.3.2 传输层 3.3.3 网络层 3.3.4…

4月3日工作日志

一个朴实无华的目录 今日学习内容&#xff1a;1.关系数据库 今日学习内容&#xff1a; 1.关系数据库

git commit Message 插件解释说明

- feat - 一项新功能 - fix - 一个错误修复 - docs - 仅文档更改 - style - 不影响代码含义的更改&#xff08;空白、格式化、缺少分号等&#xff09; - refactor - 既不修复错误也不添加功能的代码更改 - perf - 提高性能的代码更改 - build - 影响构建系统或外部依赖项…

ngx_open_file

定义在 src\os\unix\ngx_files.h #define ngx_open_file(name, mode, create, access) \open((const char *) name, mode|create, access) name&#xff1a;文件名&#xff08;通常是一个字符串&#xff09;。mode&#xff1a;文件打开模式&#x…

23种设计模式-行为型模式-责任链

文章目录 简介问题解决代码核心改进点&#xff1a; 总结 简介 责任链是一种行为设计模式&#xff0c;允许你把请求沿着处理者链进行发送。收到请求后&#xff0c;每个处理者均可对请求进行处理&#xff0c;或将其传递给链上的下个处理者。 问题 假如你正在开发一个订单系统。…

注意力机制在大语言模型中的原理与实现总结

注意力机制在大语言模型中的原理与实现总结 1. 章节介绍 在大语言模型的学习中&#xff0c;理解注意力机制至关重要。本章节旨在深入剖析注意力机制的原理及其在大语言模型中的应用&#xff0c;为构建和优化大语言模型提供理论与实践基础。通过回顾神经网络基础及传统架构的局…

kafka消息可靠性传输语义

Kafka提供了多种消息传递语义&#xff0c;以适应不同的业务需求和可靠性要求。以下是Kafka消息传输的可靠性语义及其实现机制&#xff1a; 1. At Most Once&#xff08;至多一次&#xff09; 语义&#xff1a;消息可能会丢失&#xff0c;但不会被重复传递。 实现机制&#xf…

NLP高频面试题(三十三)——Vision Transformer(ViT)模型架构介绍

Transformer架构在自然语言处理领域取得了显著成功&#xff0c;激发了研究人员将其应用于计算机视觉任务的兴趣。Vision Transformer&#xff08;ViT&#xff09;应运而生&#xff0c;成为图像分类等视觉任务中的新兴架构。本文将介绍ViT的基本架构、工作原理&#xff0c;并与传…

Oracle数据库数据编程SQL<3.6 PL/SQL 包(Package)>

包是Oracle数据库中一种重要的PL/SQL程序结构,它将逻辑相关的变量、常量、游标、异常、过程和函数组织在一起,提供了更好的封装性和模块化。在大型项目中,可能有很多模块,而每一个模块又有自己的存过、函数等。而这些存过、函数默认是放在一起的,如果所有的存过函数都是放…