一文解决图论中有向图、无向图、非负加权图的单源最短路径问题【超详细】【通用模板程序】【深入分析】【无需基础】【c++】

   本文致力于提供一种解决图论中所有(或绝大部分)有向图、无向图、非负加权图的单源最短路径问题的通用程序模板,本文提供的模板并不是简单的可行模板,而是经过深入分析解释的一个较高质量和性能的通用程序模版。在每个关键变量存储的数据结构选择上都进行了深入的思考和解释,使得该方面基础较差的小伙伴,依然可以轻松的看懂。最核心的寻路部分封装成函数方便调用。

   记:我之前写的路径规划相关的程序基本上都是基于连续空间或者栅格地图的,没有对基于拓扑结构图的寻路算法的实现进行过深入分析,正好借助本文,动手从零开始边实现边分析,实现拓扑结构图下单源最短路径的通用模板设计。

   一、解决有向图、无向图、非负加权图单源最短路径问题的通用模板程序

   1、读取给定输入数据,并构建搜索图grid

   一般来说给定数据的第一行是图的顶点个数n和边数m,对应的顶点编号为1~n,如果默认起点为编号为1,终点编号为n,那么接下来一般会再给定m行,对于非加权图,边的权重一般默认为1,每行仅包含两个数据即每条边的两个顶点,对于加权图,每行一般有3个数据,前两个是边的两个顶点,第三个是边的权重。对于指定起点和目标点的题目,一般在m行边后面,还会给出一行数据,即给定指定的起点和目标点。

   思考1:那么选择什么数据结构来存储给定的拓扑图?

   在图论的应用中,C++中常用的图表示方法主要有邻接矩阵(Adjacency Matrix)和邻接表(Adjacency List)。它们各有优缺点,适用于不同的场景。下面对它们展开介绍并对比:

   (1)邻接矩阵(Adjacency Matrix)

   邻接矩阵是一种使用二维数组表示图的方法。对于一个有 n个顶点的图,需要建立一个n x n的矩阵,例如矩阵A。矩阵中的元素表示顶点之间的连接关系:

   如果顶点 ( i ) 和顶点 ( j ) 之间有边存在,对于有向图而言,则矩阵元素 ( A[i][j] ) 的值为 1,对于无向图而言,则矩阵元素 ( A[i][j] )和( A[j][i] )的值都为 1(对于无向图),对于加权图而言,只需要将1改为相应的权重值即可; 如果顶点 ( i ) 和顶点 ( j ) 之间没有边存在,则值一般为 0。下图是一个简单的有向加权图的示例:

   优点: 邻接矩阵的优点是实现和理解起来比较简单,并且可以快速的访问判断两个顶点是否相邻,时间复杂度为 ( O ( 1 ) O(1) O(1) )。适用于较为密集的图,当图中的边数接近顶点数的平方时,邻接矩阵的存储效率较高。

   缺点: 邻接矩阵的空间复杂度高:需要 ( O ( n 2 ) O(n^2 ) O(n2) ) 的存储空间,对于稀疏图而言,显得非常浪费。且邻接矩阵的遍历效率低,在寻找当前顶点的邻接点时需要遍历一遍所有的顶点,时间复杂度为 ( O ( n ) O(n) O(n) ) 。

   (2)邻接表(Adjacency List)

   邻接表是一种使用链表或动态数组(如 C++ 中的 vector)表示图的方法。对于每个顶点,建立一个链表或动态数组,链表中的元素表示与该顶点相邻的顶点,当然,对于加权图还需要存储边的权重。

   优点:采用邻接表可以节省空间,对于稀疏图,只存储实际存在的边,空间复杂度为 ( O(n + e) ),其中 ( e ) 是图中的边数。这种优势使得邻接表的适应范围更广,鲁棒性更好。此外,邻接表的遍历效率高,遍历一个顶点的所有邻接点只需要 ( O ( degree ( v ) ) O(\text{degree}(v)) O(degree(v)) ) 的时间复杂度,其中 ( degree ( v ) \text{degree}(v) degree(v) ) 是顶点的度数,或者说是邻接点的个数。

   缺点:邻接表的缺点是访问效率低,即判断两个顶点是否相邻的时间复杂度为 O ( degree ( v ) ) O(\text{degree}(v)) O(degree(v)) ) ,即需要遍历一遍当前顶点的所有邻接点来判断是否与某个顶点相邻。此外,相比于使用数组的邻接矩阵,邻接表实现起来相对复杂。

   ☆ 注:邻接表的构建示例和示意图将在下文中思考2部分进行介绍 ↓↓↓

   (3)邻接矩阵与邻接表的对比总结

特性邻接矩阵邻接表
存储空间 O ( n 2 ) O(n^2) O(n2) O ( n + e ) O(n + e) O(n+e)
邻接性查询 O ( 1 ) O(1) O(1) O ( degree ( v ) ) O(\text{degree}(v)) O(degree(v))
遍历顶点邻接点 O ( n ) O(n) O(n) O ( degree ( v ) ) O(\text{degree}(v)) O(degree(v))
适用场景边较密集的图边较稀疏的图、各种特殊结构图
实现复杂度简单相对复杂

   有了以上基础,现在我们回到思考1,选择邻接矩阵还是邻接表来存储图? 其实都可以,可以根据实际情况进行选择,本文致力于提供一种解决图论中所有(或绝大部分)有向图、无向图、加权图的单源最短路径问题的通用程序模板,因此,在本文中将选取适应范围更广,适应性更强的邻接表来存储给定的图。


   思考2:用什么实现邻接表?

  在上面思考1的邻接表部分中,我们提到了可以采用链表或者动态数组来实现邻接表,同样,我们先简单介绍一下这两种结构

   (4)动态数组(Dynamic Array)

   动态数组在内存中占据一块连续的内存区域。由于其连续存储特性,可以通过索引快速访问任意元素,时间复杂度为 ( O ( 1 ) O(1) O(1) )。当数组容量不足时,会分配更大的内存块并将元素复制到新的内存块中,这个过程的时间复杂度为 ( O ( n ) O(n) O(n) ),但摊销后的插入时间复杂度仍为 ( O ( 1 ) O(1) O(1) )。动态数组的插入和删除效率较低,例如在中间位置插入或删除元素时,需要移动大量元素,时间复杂度为 ( O ( n ) O(n) O(n) )。

   (5)链表(Linked List)

   链表常采用非连续内存存储,链表中的每个元素称为节点,每个节点包含数据和指向下一个节点的指针。节点在内存中可以是非连续的。由于链表节点的非连续性,访问任意节点都行需要从头开始遍历,时间复杂度为 ( O ( n ) O(n) O(n) )。链表可以进行高效的插入和删除,在已知位置的插入和删除操作不需要移动其他元素,只需调整指针,时间复杂度为 ( O ( 1 ) O(1) O(1) )。此外,与动态数组相比,链表的内存开销大,每个节点除了存储数据外,还需要存储指针,因此内存开销比动态数组大。

   (6)动态数组和链表的对比总结

特性动态数组(Dynamic Array)链表(Linked List)
内存布局连续非连续
随机访问快速, O ( 1 ) O(1) O(1)慢, O ( n ) O(n) O(n)
插入和删除慢, O ( n ) O(n) O(n)快, O ( 1 ) O(1) O(1)
内存开销较小较大
适用场景需要频繁随机访问的场景需要频繁插入和删除的场景

   有了以上基础,现在我们回到思考2,先考虑最外层的数据结构用什么,即1-n个节点用链表还是用动态数组来存储?,为了方便的通过索引来直接的访问每个节点,很显然动态数组更加合适,例如vector容器,数据个数n是确定的,且不会发生改变,完全不必担心频繁修改的情况下性能较差的问题。

   内层的数据结构用什么? 我们在读取边的数据的过程中是无法确定每个节点对应的边有多少的,也就无法为每个顶点预先分配存储其邻接点的空间大小,如果使用动态数组,如vector,必然涉及到动态扩展大小的问题。那是否要采用链表呢, 其实也不一定要用链表,因为虽然采用动态数组,如vector,在构建图的时候效率不如链表list,但是在后续对图进行访问来寻找路径的时候,效率是高于链表的。这里原则上,用动态数组或者链表都行,如采用如下的两种结构都行

vector<vector<pair<int, int>>>  //外层和内层用动态数组vector
vector<list<pair<int, int>>>    //外层用动态数组vector,内层使用链表list

   至于到底选哪个,就看具体情况和个人感觉了,我的想法是如果给定的图是静态的,即在寻找路径的过程中图不会发生改变,那么可以简单的认为频繁的访问图时高效的优势,远高于构建图过程中的劣势,内层依然选择动态数组。如果给定的图是动态的,即在寻找路径的过程中图会频繁的发生改变,那么此时内层选择链表更合适一些。

   鉴于大部分题目给定的图都是静态的,因此,本文选择vector<vector<pair<int, int>>>来实现邻接表,至于,为什么内层vector存储的是pair<int, int>,而不是int,是因为本文致力于提供一种解决图论中所有(或绝大部分)有向图、无向图、加权图的单源最短路径问题的通用程序模板,对于加权图而言,除了存储邻居节点,还需要存储权值,即用pair<int, int>的第一个元素来存储邻居节点索引,第二个元素来存储权值,对于非加权图,第二个元素存储1即可。下图给出了一个加权有向图的示例

   到这里,第1步读取给定输入数据,并构建搜索图grid的相关内容基本上就介绍完了,考虑到大家对C++算法与数据结构的了解程度可能差异较大,我花了大量的篇幅来详细的介绍和解释本文部分内容,现在给出对应的C++程序示例。

   第1部分 C++程序示例

   示例(1):有向加权图,默认起点为顶点1,终点为顶点n

   示例真题链接:https://kamacoder.com/problempage.php?pid=1047

   输入示例:

7 9
1 2 1
1 3 4
2 3 2
2 4 5
3 4 2
4 5 3
2 6 4
5 7 4
6 7 9

   读取给定输入数据,并构建搜索图grid的程序:

// 读取输入数据,并构建搜索图grid
int n, m, p1, p2, val;
cin >> n >> m;
vector<vector<pair<int, int>>> grid(n + 1);
for (int i = 0; i < m; i++) {
    cin >> p1 >> p2 >> val;
    grid[p1].push_back({ p2,val });
}

   示例(2):无向非加权图,起点终点由输入给定

   示例真题链接:https://kamacoder.com/problempage.php?pid=1179

   输入示例:

5 4
1 2
1 3
2 4
3 4
1 4

   读取给定输入数据,并构建搜索图grid的程序:

 // 读取输入数据,并构建搜索图grid
 int n, m, p1, p2, val,si,ei;
 cin >> n >> m;
 vector<vector<pair<int, int>>> grid(n + 1);
 for (int i = 0; i < m; i++) {
     cin >> p1 >> p2;
     grid[p1].push_back({ p2,1 });
     grid[p2].push_back({ p1,1 });
 }
 cin>>si>>ei;  // 读取起点和终点

   第1部分读取给定输入数据,并构建搜索图grid的程序需要根据题目给定的数据形式和图的类型来进行细微的调整,总的来说,有向图只需要通过grid[p1].push_back({ p2,val });添加从p1指向p2的边即可,而无向图则需要通过grid[p1].push_back({ p2,val }); grid[p2].push_back({ p1,val });同时添加p1指向p2的边和p2指向p1的边,加权图则权值val通过读入的参数获取,非加权图则根据题目描述直接设定(一般为1),默认起点为1,终点为n的不需要读取起点终点信息,通过输入指定的,需要读取和存储起点和终点信息。一般来说也就这几种变数,都很简单。灵活的对上面两个程序示例进行细微调节就行。


   2、选取寻路算法,寻找最短路径

   有些题目是只需要判断节点A和B之间是否存在可行路径,返回bool类型的结果即可,而有的题目,若存在可行路径,则需要返回节点A和B之间的最短路径。本文致力于提供一种解决图论中所有(或绝大部分)有向图、无向图、加权图的单源最短路径问题的通用程序模板,因此,我们直接选取寻找最短路径的算法。

   寻找最短路径的算法其实有多种,其中,A * 算法是路径规划领域最经典,也是应用最广泛的算法之一,但是由于启发式函数的存在,A * 算法并不能保证一定找到最短路径。因此,本文采用无启发式函数的类A * 算法(或者说将启发式函数的权重系数设置为0)来作为寻路算法。

   【选做、非必要】 对A*算法的感兴趣的可以看一下以下资料,资料1的第一部分有我写的对 A * 算法的基本原理简介和算法流程简介,想要深入了解的,可以看一下资料2,应该算是我看过的A * 算法最全的介绍了,资料3和4是我之前写的A * 算法的源码解读和详细的基本原理介绍和算法流程概括总结 。

   资料1:JPS(Jump Point Search)跳点搜索路径规划算法回顾

   资料2:Amit’s A* Pages

   资料3:zhm_real/MotionPlanning运动规划库中A*算法源码详细解读

   资料4:详细介绍用MATLAB实现基于A*算法的路径规划(附完整的代码,代码逐行进行解释)

   之前对A * 算法完全没有了解的小伙伴,不用慌,上面我特意标注了 【选做、非必要】 标识,即使你对于A * 算法是完全陌生的,也不影响你掌握本文接下来要介绍的寻找最短路径的策略,只需要记住以下算法流程和几个概念就可以了。

   必备概念

   ① 节点代价值:节点A的代价值是指从起点到节点A所需的累计代价,即从起点到节点A的路径经过的所有边的权重累加值,该值在寻路过程中可能会发生变化,例如发现一条从起点到节点A的更优路径时,其值会被更新为更优路径的代价值。

   ② 节点的父节点:节点A的父节点,即扩展出节点A的那个节点,例如已知路径 ①->②->A->④->⑤,则节点A的父节点,即为②

   ③ 节点扩展: 所谓的对节点A进行扩展可以理解为,当前已经得到了从起点到节点A的路径和累计代价,通过遍历节点A的邻接点和从节点A到达该邻接点的代价,就可以进一步得到从起点到节点A的所有邻接点的路径和累计代价,从而实现路径的扩展和延伸。

   ④ 节点状态:寻找最短路径的过程中,每个节点都有三种状态:【未被访问过的节点】,【被访问过,等待扩展的节点】,【被访问过,且已经扩展过的节点】

   ⑤ 两个容器:openlist容器存储被访问过,等待扩展的点,closelist容器存储,被访问过,且已经扩展过的节点

   ⑥ 路径回溯:每个节点都记录了其父节点,一但发现了终点,则通过终点记录的父节点,就可以得到终点前面的那个点,以此类推,直至回溯到起点,即可获得完整的路径。

   算法流程

   ① 将起点放入到openlist中完成算法初始化。

   ② 检查当前openlist是否为空。如果为空,则搜索失败,停止循环,不存在从起点到目标点的可行路径。若不为空,则继续循环。

   ③从openlist中取出代价值最小的节点作为当前扩展点current,并将其加入到closelist中。

   ④ 判断当前扩展点current是否为要找的终点,则说明已经找到了从起点到终点的最短路径,结束循环,若不是,则继续循环。

   ⑤ 对当前节点current进行扩展,即遍历其当所有的邻接点,生成一组子节点。对于每一个邻接点:

   如果该节点在closelist中,则说明该节点已经扩展过了,无需任何处理

   如果该节点在openlist中,则说明该节点之前已经被访问过,且添加到openlist中等待被扩展,检查其通过当前节点计算得到的代价值是否比其现在存储的代价值更小,如果更小,则说明我们找到了到节点的一条更优的路径,则更新其代价值,并将其父节点设置为当前节点。

   如果该节点不在openlist中,则说明该节点之前没有被访问过,将其加入到openlist列表,并计算代价值,设置其父节点为当前节点。

   ⑥ 本轮迭代结束, 转到第②步,循环进行下一轮迭代和扩展。直至openlist为空,在第②步中退出循环,或者找到最短路径,在第④步中退出循环。

   思考3:那么选择什么容器来构造openlist和closelist?

   从上面的必备概念和算法流程可知,在寻路过程中,除了我们已知的用于路径搜素的图grid,我们还需要记录每个节点的状态、代价值、父节点等信息。在寻路过程中,需要频繁对openlist执行访问、插入、删除等操作,对closelist执行访问和插入操作(对closelist的访问在路径回溯阶段)。且openlist和closelist中数据的存储可以是无序的。至此,基本上答案已经呼之欲出了—— unordered_map

   (1)unordered_map 简介和特点

   简介:unordered_map 是一个哈希表实现的关联容器,用于存储键值对。它提供了高效的插入、删除和查找操作,平均时间复杂度为 ( O(1) )。【高效的插入、删除和查找能力简直是充当openlist和closelist容器的绝配】unordered_map 具有以下的特点:

   哈希表实现:内部使用哈希表实现,键值对的存储位置由键的哈希值决定。

   无序:元素存储顺序不是按照插入顺序或键的顺序,而是根据哈希值。

   快速查找:平均查找时间复杂度为 ( O(1) ),最坏情况为 ( O(n) )(当发生大量哈希冲突时)。

   自动扩展:当元素数量增加时,哈希表会自动扩展,重新哈希,保持高效的查找性能。【这里的自动扩展能力使得我们对openlist和closelist执行插入操作时变得非常非常方便】

   (2)unordered_map 与其他容器的对比

特性unordered_mapmapvectorlist
内部实现哈希表红黑树动态数组双向链表
元素顺序无序按键排序按插入顺序按插入顺序
查找时间复杂度平均 O ( 1 ) O(1) O(1),最坏 O ( n ) O(n) O(n) O ( log ⁡ n ) O(\log n) O(logn) O ( n ) O(n) O(n) O ( n ) O(n) O(n)
插入/删除时间复杂度平均 O ( 1 ) O(1) O(1),最坏 O ( n ) O(n) O(n) O ( log ⁡ n ) O(\log n) O(logn)插入末尾 O ( 1 ) O(1) O(1),删除 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)
适用场景需要快速查找 ( 同时需要频繁插入删除) , 无需顺序需要有序存储和快速查找需要高效随机访问和批量操作需要频繁插入和删除

   有了以上基础,现在我们回到思考3,很显然unordered_map 高效的插入、删除和查找能力简直是充当openlist和closelist容器的绝配

   思考4:是否可以选择优先级队列priority_queue来构造openlist和closelist?

   (3)priority_queue简介与特点

   简介:priority_queue 是一个基于堆实现的容器适配器,用于存储一组元素,并提供快速访问和移除最大或最小元素的功能。priority_queue 具有以下特点:

   堆实现:通常使用最大堆(或最小堆)实现,保证队列中的最大(或最小)元素总是位于队首。

   无序存储:元素的存储顺序不保证,只有最大(或最小)元素的位置是确定的。

   快速访问最大/最小元素:访问最大(或最小)元素的时间复杂度为 ( O(1) ),插入和删除操作的时间复杂度为 ( O ( log ⁡ n ) O(\log n) O(logn) )。

   (4)priority_queue维持最大/小堆的时间复杂度

   priority_queue 中插入或删除一个元素后,维持堆顶元素最大或最小的时间复杂度是 ( O ( log ⁡ n ) O(\log n) O(logn) )。这里,n 是 priority_queue 中的元素数量。具体来说:

   插入元素的时间复杂度:

   当向 priority_queue 中插入一个元素时,元素会首先被添加到堆的末尾,然后通过上浮操作(heapify up)将其移动到正确的位置,以维持堆的性质。这个过程的时间复杂度为( O ( log ⁡ n ) O(\log n) O(logn) ),因为堆是一个完全二叉树,插入操作最多需要调整树的高度次,而高度为 ( log ⁡ n \log n logn )。

   删除堆顶元素的时间复杂度:

   当从 priority_queue 中删除堆顶元素时,堆顶元素会被移除,并用堆的最后一个元素替换,然后通过下沉操作(heapify down)将这个元素移动到正确的位置,以维持堆的性质。这个过程的时间复杂度同样为( O ( log ⁡ n ) O(\log n) O(logn) ),因为最多需要调整树的高度次。

   所以,总的来说,在 priority_queue 中,无论是插入元素还是删除堆顶元素,都会进行堆的调整操作,以维持堆的性质。这些操作的时间复杂度都是( O ( log ⁡ n ) O(\log n) O(logn) )。

   (5)priority_queue与unordered_map 的对比

特性unordered_mappriority_queue
内部实现哈希表堆(最大堆或最小堆)
元素顺序无序无序,只有最大/最小元素位置确定
插入时间复杂度平均 O ( 1 ) O(1) O(1),最坏 O ( n ) O(n) O(n) O ( log ⁡ n ) O(\log n) O(logn)
删除时间复杂度平均 O ( 1 ) O(1) O(1),最坏 O ( n ) O(n) O(n) O ( log ⁡ n ) O(\log n) O(logn)
查找时间复杂度平均 O ( 1 ) O(1) O(1),最坏 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) 仅访问最大/最小元素
适用场景快速查找、插入和删除键值对快速访问和移除最大/最小元素
常见用途字典、频率统计、关联数组优先级队列、任务调度
内存使用需要额外的哈希桶和节点指针需要额外的堆结构
是否支持随机访问是(通过键访问任意元素)否(仅能访问堆顶元素)

   有了以上基础,现在我们回到思考4,有些小伙伴可能注意到算法流程第③步中,在每轮循环迭代中,我们都需要从openlist中获取代价值最小的那个元素,自然而言也就想到了可以自动排序的优先级队列priority_queue。

   我们现在分析一下分别使用priority_queue和unordered_map时,算法每轮迭代获取代价值最小的节点所需的时间复杂度。假设当前openlist中存在n个节点,本轮迭代新插入到openlist中的节点个数为m,此外,每轮迭代都会从openlist列表中移除一个节点,在closelist中插入一个节点。priority_queue在插入或删除元素后,会自动维持其最大/小堆特性,堆顶元素即代价值最小的节点,因此,其时间复杂度主要取决于插入和删除次数,近似为 O ( ( m + 2 ) log ⁡ n ) O((m+2)\log n) O((m+2)logn),unordered_map从openlist中获取代价值最小的点,则需要遍历一遍openlist中的所有节点,删除插入的时间复杂度都是 O ( 1 ) O(1) O(1) ,因此,所以其时间复杂度近似为 O ( n + m + 2 ) O(n+m+2) O(n+m+2) ,当n远大于m时,priority_queue的时间复杂度更低,那是不是用priority_queue更好?

   答案是否定的,priority_queue存在一个致命的问题,其不支持随机访问,即只能访问堆顶的元素,而在前面介绍的算法流程的步骤⑤中,对当前节点进行扩展时,若该邻接点已经存在于openlist中,我们需要从openlist中访问并读取该邻接点现有的代价值,且若新的代价值更优,还需对openlist中存储的该邻接点的相关数据进行修改。如果使用priority_queue,则必然需要很多复杂的额外操作来实现以上需求。且上面算时间复杂度的时候,并没有考虑每轮迭代中大量的节点访问的时间复杂度,这方面unordered_map具有绝对的优势。因此,综合来看,使用priority_queue来实现openlist和closelist并不合适。

   注: unordered_map:可以通过将顶点索引值作为键,来通过键访问任意元素。因此,可以认为 unordered_map 支持随机访问,但这种访问是基于键而非位置索引的。

   思考5:算法所需的节点索引、父节点索引、代价值、状态值等变量应该如何存储?

   上面的思考4中,我们已经提到了将节点的索引作为unordered_map的键,来方便的通过节点索引来访问openlist和closelist中的任意元素,每个节点的父节点、代价值、状态值应该是与节点绑定的数据,那么是不是可以将他们打包在一起作为unordered_map的值呢?

   答案是不可以,我们需要明确那些量是只openlist和closelist中存储的那些节点,openlist中存储的是已经被访问过,且尚未扩展的节点,而closelist中存储的是已经被访问过,且已经扩展过的节点。前面的必备概念中,我们提到节点的状态,除了以上两种,还有未访问过,这个状态,即,我们并没有也不需要设计容器来存储未访问过的节点,且未访问过的节点的父节点和代价值信息是未知的。

   若将节点状态值也放closelist中存储,在前面介绍的算法流程的步骤⑤中,对当前节点进行扩展时,若该邻接点之前尚未被扩展过,即其不在openlist中也不在closelist中,此时就无法查询该节点的状态,或者有的小伙伴说可以遍历一遍openlist和closelist,都没找到就说明没访问过。但这种操作是很低效的,我们需要的对每个节点状态的查询都是高效的 O ( 1 ) O(1) O(1)复杂度。因此,节点状态量需要单独的进行存储,节点的数量n是已知的,且不发生改变,直接用一个vector来存储即可实现高效的访问和修改。

   对于未访问过的节点的父节点和代价值信息是未知且无意义的,所以每个节点的父节点和代价值则可以打包在一起作为unordered_map的值,存储在openlist或closelist中,就两个数据是否可以直接用pair<int ,int>来打包在一起? 原则上是可以的,但本文致力于提供一种解决图论中所有(或绝大部分)有向图、无向图、加权图的单源最短路径问题的通用程序模板,为了防止有些题目需要存储其他的额外信息,考虑到程序模板良好的可拓展性,这里面我们使用结构体来打包这两个数据,这样如果需要存储其他信息时,只需要在struct中添加即可,无需调整整个unordered_map,即本文设计的openlist和closelist的数据结构如下:

// 创建节点数据类型,记录节点累计代价值,父节点索引值等信息
struct Node
{
    int val_;
    int parent_;
    Node(int val, int parent) : val_(val), parent_(parent) {}
    Node() : val_(INT_MAX), parent_(0){}
};


unordered_map<int, Node> openlist;  // 存放待扩展点,int类型的键存储节点索引,自定义的Node类型的值存储其他绑定数据信息
unordered_map<int, Node> closelist; // 存放已经扩展过的节点

   至此,经过大量的分析和对比,我们已经明确了算法中涉及到的所有关键数据的数据存储结构,现在,我们来总结一下,如下所示:

1int类型的n,m,si,ei分别存储图的节点数、边数、起点索引、终点索引

2、vector<vector<pair<int, int>>>类型的变量grid(n + 1)存储输入给定的图

3、自定义Node类型的结构体存储与节点绑定的父节点索引、代价值等其他信息
	struct Node
	{
	    int val_;
	    int parent_;
	    Node(int val, int parent) : val_(val), parent_(parent) {}
	    Node() : val_(INT_MAX), parent_(0){}
	};
	
4、unordered_map<int, Node>类型的变量openlist和closelist分别存储待扩展点和已经扩展过的节点,
  其中int类型的键存储节点索引,自定义的Node类型的值存储其他绑定数据信息
  
5、vector<int>类型的变量state(n+,1)存储n个节点的状态,初始化为未访问状态1
   节点状态对应的值 1:未被访问过 2;在优先级队列中等待扩展 3:已经扩展过了

   寻找最短路径的程序(核心)

   有了上面的基础,再配合前面介绍的算法流程,很容易就可以写出寻找最短路径的程序,我将其封装成了独立的函数方便调用,如下所示,程序里面,我写了很详细的注释,配合前面的算法流程介绍,应该很容易看懂,本文的篇幅已经很长了,后面还有一些内容,这里就不对程序展开详细的介绍了,我把它单独放在一篇博客中了,有兴趣的可以去看一下,链接如下:

   拓扑结构图中寻找最短路径的findpath函数【by慕羽】的函数源码、算法流程概括、函数源码解析【点击可跳转】

   注:为了方便描述和理解,程序注释中将openlist和closelist描述为了“优先级队列”或“列表”,大家知道其实际上是unordered_map即可。

// 寻找最短路径
// 形参依次为 起点索引start_i,目标点索引end_i,搜索图grid,已经扩展过的节点列表closelist
// 返回值表示是否找到可行路径
bool findpath( const int start_i, const int end_i, const vector<vector<pair<int, int>>>& grid, unordered_map<int, Node>& closelist)
{  
   closelist.clear(); //清空closelist列表,准备存储规划结果
   unordered_map<int, Node> openlist; // 创建哈希表存放待扩展点
   vector<int> state(grid.size(),1);  // 记录节点状态 1:未被访问过 2;在优先级队列中等待扩展 3:已经扩展过了

   Node start(0, -1); //初始化起点,代价值为0,起点无父节点设置为-1
   openlist[start_i] = start; //将起点放入openlist列表
   state[start_i] = 2; //更新起点状态,完成初始化

   while (!openlist.empty())
   {
       //第一步:从openlist列表中选取代价值val最小的节点current_index
       int current_index = -1;
       int min_val = INT_MAX;
       for (const auto& item : openlist) {
           if (item.second.val_ < min_val) {
               min_val = item.second.val_;
               current_index = item.first;
           }
       }
       //第二步:将节点current_index从带扩展队列openlist中取出,并添加到扩展完成队列closelist
       Node current = openlist[current_index];
       openlist.erase(current_index); 
       closelist[current_index] = current;
       state[current_index] = 3;
       //第三步: 判断是否发现到目标点的最短路径
       //若当前扩展点是目标点,说明待扩展点队列中其他点的当前代价均大于目标点的代价,图中所有边为非负数,此时已经找到最短路径
       if (current_index == end_i) return true;
       //第四步: 扩展当前节点current_index
       for (const auto &neig: grid[current_index])
       {
           if (state[neig.first] == 1) //若该邻居节点未被访问过,则将current_index作为其父节点,并放入队列中
           {
               Node temp(current.val_ + neig.second,current_index);
               openlist[neig.first] = temp;
               state[neig.first] = 2;
           }
           else if (state[neig.first] == 2) //若该邻居节点已经在带扩展队列中了,则判断是否需要更新代价值和父节点
           {
               if (openlist[neig.first].val_ > (current.val_ + neig.second))
               {
                   openlist[neig.first].val_ = current.val_ + neig.second;
                   openlist[neig.first].parent_ = current_index;
               }
           } //若该邻居节点已经在带扩展完成队列中了,则无需任何处理
       }
   }
   return false;
}

   思考6:为什么当openlist中代价值最小的点是终点时,当前找到的路径就一定是最短路径?

   若当前openlist中代价值最小的点是终点,则说明openlist中存储的其他待扩展点的当前代价vi均大于终点的代价ve,图中所有边ei均为非负数,即从这些点到终点必然需要再累加一个或多个大于0的代价,即vi大于ve,ei大于0,vi+ei也必然大于ve,那么必然不存在到终点的更近路径了,此时已经找到最短路径。


   3、通过路径回溯提取最短路径

   思考7:closelist为什么要以传引用的形式传入findpath函数,而不是像openlist一样直接在findpath函数中定义? 每个节点为什么要记录其父节点?findpath函数的返回值仅提供了是否存在可行路径信息,如何获得找到的最短路径?

   以上三个问题实际上可以看做是一个问题,之所以findpath函数的返回值是bool类型的表示是否找到可行路径的标志,而不是直接返回路径,是因为有些题目仅仅需要输出是否存在可行路径,而不关心具体的路径,有些题目仅需要输出最短路径的代价值是多少,而同样不关心最短路径具体是什么。那么对于需要输出最短路径的题目,我们又如何获取最短路径呢?

   答案是通过节点记录的父节点信息进行路径回溯来提取路径,这些路径点储存在哪里呢? 答案是closelist,因为如果一个节点有子节点,那么他一定扩展过了,必然存在于closelist中。所以我们直接从终点开始根据其记录的父节点信息,就可以得到终点的前一个点,根据这个点记录的父节点信息,又可以得到这个点前面的一个点,以此类推,直至回溯到起点。即可得到完整的路径。这也是为什么每个点要记录其父节点信息,以及为什么closelist要以引用的形式传入,并将其返回的原因。

   路径回溯,并输出最短路径的程序

   由于需要从终点开始向起点进行路径回溯,而在输出的时候一般是输出从起点到终点的顺序来输出路径,所以,我们并不能直接输出路径,而需要先将回溯过程的路径存储起来,考虑到先入后出的特性,自然也就想到了stack,且stack的入栈和出栈操作都是高效的,其时间复杂度为 O ( 1 ) O(1) O(1) ,因此该部分主要分为路径回溯部分和路径打印部分两部分内容,其中路径打印部分的程序需要根据题目要求的格式进行细微的调整,参考程序如下:

// 输出最短路径的函数
void printPath(const int start_i, const int end_i, unordered_map<int, Node>& closelist) {
    stack<int> path;  //创建一个空栈存储回溯得到的路径
    int current = end_i;
    // 从终点向前追溯到起点
    while (current != start_i) {
        path.push(current);
        current = closelist[current].parent_;
    }
    path.push(start_i); // 将起点加入路径
    // 输出路径
    while (!path.empty()) {
        cout << path.top(); path.pop();
        if (!path.empty())  cout << " -> ";
    }
    cout << endl;
}

   4、完整的通用模板程序

   至此,解决图论中有向图、无向图、非负加权图的单源最短路径问题的通用模板程序就设计和编写完成了,如下所示,无论是是有向图,无向图,加权图、非加权图等,无论是寻找是否存在可行路径,还是寻找最短路径或者代价值。本文提供的程序模板的核心部分也就是findpath函数都无需更改,直接调用即可,程序模板主函数中的读取给定输入数据,并构建搜索图grid部分,根据题目要求和本文第1部分末尾处提供的各种示例进行细微的修改即可,如果需要打印出最短路径,则根据题目要求的输出格式对printPath函数的输出路径部分进行细微的修改即可。

#include <iostream>
#include <vector>
#include <unordered_map>
#include <limits.h>
#include <stack>
using namespace std;

// 创建节点数据类型,记录节点累计代价值,父节点索引值,节点状态等信息
struct Node
{
    int val_;
    int parent_;
    Node(int val, int parent) : val_(val), parent_(parent) {}
    Node() : val_(INT_MAX), parent_(0){}
};

// 寻找最短路径
// 形参依次为 起点索引start_i,目标点索引end_i,搜索图grid,已经扩展过的节点列表closelist
// 返回值表示是否找到可行路径
bool findpath( const int start_i, const int end_i, const vector<vector<pair<int, int>>>& grid, unordered_map<int, Node>& closelist)
{  
   closelist.clear(); //清空closelist列表,准备存储规划结果
   unordered_map<int, Node> openlist; // 创建哈希表存放待扩展点
   vector<int> state(grid.size(),1);  // 记录节点状态 1:未被访问过 2;在优先级队列中等待扩展 3:已经扩展过了

   Node start(0, -1); //初始化起点,代价值为0,起点无父节点设置为-1
   openlist[start_i] = start; //将起点放入openlist列表
   state[start_i] = 2; //更新起点状态,完成初始化

   while (!openlist.empty())
   {
       //第一步:从openlist列表中选取代价值val最小的节点current_index
       int current_index = -1;
       int min_val = INT_MAX;
       for (const auto& item : openlist) {
           if (item.second.val_ < min_val) {
               min_val = item.second.val_;
               current_index = item.first;
           }
       }
       //第二步:将节点current_index从带扩展队列openlist中取出,并添加到扩展完成队列closelist
       Node current = openlist[current_index];
       openlist.erase(current_index); 
       closelist[current_index] = current;
       state[current_index] = 3;
       //第三步: 判断是否发现到目标点的最短路径
       //若当前扩展点是目标点,说明待扩展点队列中其他点的当前代价均大于目标点的代价,图中所有边为非负数,此时已经找到最短路径
       if (current_index == end_i) return true;
       //第四步: 扩展当前节点current_index
       for (const auto &neig: grid[current_index])
       {
           if (state[neig.first] == 1) //若该邻居节点未被访问过,则将current_index作为其父节点,并放入队列中
           {
               Node temp(current.val_ + neig.second,current_index);
               openlist[neig.first] = temp;
               state[neig.first] = 2;
           }
           else if (state[neig.first] == 2) //若该邻居节点已经在带扩展队列中了,则判断是否需要更新代价值和父节点
           {
               if (openlist[neig.first].val_ > (current.val_ + neig.second))
               {
                   openlist[neig.first].val_ = current.val_ + neig.second;
                   openlist[neig.first].parent_ = current_index;
               }
           } //若该邻居节点已经在带扩展完成队列中了,则无需任何处理
       }
   }
   return false;
}


// 输出最短路径的函数
void printPath(const int start_i, const int end_i, unordered_map<int, Node>& closelist) {
    stack<int> path;  //创建一个空栈存储回溯得到的路径
    int current = end_i;
    // 从终点向前追溯到起点
    while (current != start_i) {
        path.push(current);
        current = closelist[current].parent_;
    }
    path.push(start_i); // 将起点加入路径
    // 输出路径
    while (!path.empty()) {
        cout << path.top(); path.pop();
        if (!path.empty())  cout << " -> ";
    }
    cout << endl;
}

int main() {

    // 读取输入数据,并构建搜索图grid
    int n, m, p1, p2, val,si,ei;
    cin >> n >> m;
    vector<vector<pair<int, int>>> grid(n + 1);
    for (int i = 0; i < m; i++) {
        cin >> p1 >> p2 >> val;
        grid[p1].push_back({ p2,val });
    }
    si = 1; ei = n; //默认起点为1,终点为n
    // 调用findpath函数寻找最短路径
    unordered_map<int, Node> closelist;
    bool suc = findpath(si, ei, grid, closelist);
    // 判断是否成功找到最短路径,并根据需要输出最短路径值、最短路径等信息
    if (suc) cout << closelist[ei].val_ << endl;
    else cout << -1 << endl;
    // 如果成功,则打印路径
    if (suc) printPath(si, ei, closelist); //根据题目输出要求决定是否需要输出最短路径,不需要就注释掉该行内容
    return 0;
}

   当输入数据为以下内容时:

7 9
1 2 1
1 3 4
2 3 2
2 4 5
3 4 2
4 5 3
2 6 4
5 7 4
6 7 9

   本文给定的通用模板程序的输出如下所示:



   二、真题示例

   1、参加科学大会

   小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。

   输入:第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。 接下来为 M 行,每行包括三个整数,S、E 和 V,代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。

   输出:输出一个整数,代表小明从起点到终点所花费的最小时间。

   示例真题链接:https://kamacoder.com/problempage.php?pid=1047

   参考程序:

#include <iostream>
#include <vector>
#include <unordered_map>
#include <limits.h>
#include <stack>
using namespace std;

// 创建节点数据类型,记录节点累计代价值,父节点索引值,节点状态等信息
struct Node
{
    int val_;
    int parent_;
    Node(int val, int parent) : val_(val), parent_(parent) {}
    Node() : val_(INT_MAX), parent_(0){}
};

// 寻找最短路径
// 形参依次为 起点索引start_i,目标点索引end_i,搜索图grid,已经扩展过的节点列表closelist
// 返回值表示是否找到可行路径
bool findpath( const int start_i, const int end_i, const vector<vector<pair<int, int>>>& grid, unordered_map<int, Node>& closelist)
{  
   closelist.clear(); //清空closelist列表,准备存储规划结果
   unordered_map<int, Node> openlist; // 创建哈希表存放待扩展点
   vector<int> state(grid.size(),1);  // 记录节点状态 1:未被访问过 2;在优先级队列中等待扩展 3:已经扩展过了

   Node start(0, -1); //初始化起点,代价值为0,起点无父节点设置为-1
   openlist[start_i] = start; //将起点放入openlist列表
   state[start_i] = 2; //更新起点状态,完成初始化

   while (!openlist.empty())
   {
       //第一步:从openlist列表中选取代价值val最小的节点current_index
       int current_index = -1;
       int min_val = INT_MAX;
       for (const auto& item : openlist) {
           if (item.second.val_ < min_val) {
               min_val = item.second.val_;
               current_index = item.first;
           }
       }
       //第二步:将节点current_index从带扩展队列openlist中取出,并添加到扩展完成队列closelist
       Node current = openlist[current_index];
       openlist.erase(current_index); 
       closelist[current_index] = current;
       state[current_index] = 3;
       //第三步: 判断是否发现到目标点的最短路径
       //若当前扩展点是目标点,说明待扩展点队列中其他点的当前代价均大于目标点的代价,图中所有边为非负数,此时已经找到最短路径
       if (current_index == end_i) return true;
       //第四步: 扩展当前节点current_index
       for (const auto &neig: grid[current_index])
       {
           if (state[neig.first] == 1) //若该邻居节点未被访问过,则将current_index作为其父节点,并放入队列中
           {
               Node temp(current.val_ + neig.second,current_index);
               openlist[neig.first] = temp;
               state[neig.first] = 2;
           }
           else if (state[neig.first] == 2) //若该邻居节点已经在带扩展队列中了,则判断是否需要更新代价值和父节点
           {
               if (openlist[neig.first].val_ > (current.val_ + neig.second))
               {
                   openlist[neig.first].val_ = current.val_ + neig.second;
                   openlist[neig.first].parent_ = current_index;
               }
           } //若该邻居节点已经在带扩展完成队列中了,则无需任何处理
       }
   }
   return false;
}


// 输出最短路径的函数
void printPath(const int start_i, const int end_i, unordered_map<int, Node>& closelist) {
    stack<int> path;  //创建一个空栈存储回溯得到的路径
    int current = end_i;
    // 从终点向前追溯到起点
    while (current != start_i) {
        path.push(current);
        current = closelist[current].parent_;
    }
    path.push(start_i); // 将起点加入路径
    // 输出路径
    while (!path.empty()) {
        cout << path.top(); path.pop();
        if (!path.empty())  cout << " -> ";
    }
    cout << endl;
}

int main() {

    // 读取输入数据,并构建搜索图grid
    int n, m, p1, p2, val,si,ei;
    cin >> n >> m;
    vector<vector<pair<int, int>>> grid(n + 1);
    for (int i = 0; i < m; i++) {
        cin >> p1 >> p2 >> val;
        grid[p1].push_back({ p2,val });
    }
    si = 1; ei = n; //默认起点为1,终点为n
    // 调用findpath函数寻找最短路径
    unordered_map<int, Node> closelist;
    bool suc = findpath(si, ei, grid, closelist);
    // 判断是否成功找到最短路径,并根据需要输出最短路径值、最短路径等信息
    if (suc) cout << closelist[n].val_ << endl;
    else cout << -1 << endl;
    // 如果成功,则打印路径
    // if (suc) printPath(si, ei, closelist); //根据题目输出要求决定是否需要输出最短路径
    return 0;
}

   2、寻找存在的路径

   给定一个包含 n 个节点的无向图中,节点编号从 1 到 n (含 1 和 n )。你的任务是判断是否有一条从节点 source 出发到节点 destination 的路径存在。

   输入:第一行包含两个正整数 N 和 M,N 代表节点的个数,M 代表边的个数。 后续 M 行,每行两个正整数 s 和 t,代表从节点 s 与节点 t 之间有一条边。 最后一行包含两个正整数,代表起始节点 source 和目标节点 destination。

   输出:输出一个整数,代表是否存在从节点 source 到节点 destination 的路径。如果存在,输出 1;否则,输出 0。

   示例真题链接:https://kamacoder.com/problempage.php?pid=1179

   参考程序:

#include <iostream>
#include <vector>
#include <unordered_map>
#include <limits.h>
using namespace std;

// 创建节点数据类型,记录节点累计代价值,父节点索引值
struct Node
{
    int val_;
    int parent_;
    Node(int val, int parent) : val_(val), parent_(parent) {}
    Node() : val_(INT_MAX), parent_(0){}
};

// 寻找最短路径
// 形参依次为 起点索引start_i,目标点索引end_i,搜索图grid,已经扩展过的节点列表closelist
// 返回值表示是否找到可行路径
bool findpath( const int start_i, const int end_i, const vector<vector<pair<int, int>>>& grid, unordered_map<int, Node>& closelist)
{  
   closelist.clear(); //清空closelist列表,准备存储规划结果
   unordered_map<int, Node> openlist; // 创建哈希表存放待扩展点
   vector<int> state(grid.size(),1);  // 记录节点状态 1:未被访问过 2;在优先级队列中等待扩展 3:已经扩展过了

   Node start(0, -1); //初始化起点,代价值为0,起点无父节点设置为-1
   openlist[start_i] = start; //将起点放入openlist列表
   state[start_i] = 2; //更新起点状态,完成初始化

   while (!openlist.empty())
   {
       //第一步:从openlist列表中选取代价值val最小的节点current_index
       int current_index = -1;
       int min_val = INT_MAX;
       for (const auto& item : openlist) {
           if (item.second.val_ < min_val) {
               min_val = item.second.val_;
               current_index = item.first;
           }
       }
       //第二步:将节点current_index从带扩展队列openlist中取出,并添加到扩展完成队列closelist
       Node current = openlist[current_index];
       openlist.erase(current_index); 
       closelist[current_index] = current;
       state[current_index] = 3;
       //第三步: 判断是否发现到目标点的最短路径
       //若当前扩展点是目标点,说明待扩展点队列中其他点的当前代价均大于目标点的代价,图中所有边为非负数,此时已经找到最短路径
       if (current_index == end_i) return true;
       //第四步: 扩展当前节点current_index
       for (const auto &neig: grid[current_index])
       {
           if (state[neig.first] == 1) //若该邻居节点未被访问过,则将current_index作为其父节点,并放入队列中
           {
               Node temp(current.val_ + neig.second,current_index);
               openlist[neig.first] = temp;
               state[neig.first] = 2;
           }
           else if (state[neig.first] == 2) //若该邻居节点已经在带扩展队列中了,则判断是否需要更新代价值和父节点
           {
               if (openlist[neig.first].val_ > (current.val_ + neig.second))
               {
                   openlist[neig.first].val_ = current.val_ + neig.second;
                   openlist[neig.first].parent_ = current_index;
               }
           } //若该邻居节点已经在带扩展完成队列中了,则无需任何处理
       }
   }
   return false;
}


int main() {

    // 读取输入数据,并构建搜索图grid
    int n, m, p1, p2, val,si,ei;
    cin >> n >> m;
    vector<vector<pair<int, int>>> grid(n + 1);
    for (int i = 0; i < m; i++) {
        cin >> p1 >> p2;
        grid[p1].push_back({ p2,1 });
        grid[p2].push_back({ p1,1 });
    }
    cin>>si>>ei;
    // 调用findpath函数寻找最短路径
    unordered_map<int, Node> closelist;
    bool suc = findpath(si, ei, grid, closelist);
    // 判断是否成功找到最短路径,并根据需要输出最短路径值、最短路径等信息
    if (suc) cout << 1<< endl;
    else cout << 0 << endl;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/754772.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaSE期末复习速成笔记

面向对象 1. 面向对象的概念 面向对象编程&#xff08;OOP&#xff09;是一种编程范式&#xff0c;它将现实世界的事物抽象为对象&#xff0c;通过类和对象来创建各种功能模块&#xff0c;以此来设计和开发软件。 2. 类与对象 类&#xff1a;是对象的模板&#xff0c;定义了…

Ros2学习中的话题通信-自定义接口消息:在VScode中不能补全的问题

在学习ros2的过程中&#xff0c;当学习到话题通信-自定义接口消息时&#xff0c;当消息的具体类型并未指定时&#xff0c;常见的操作是在base_interfaces_demo下创建msg文件夹及文件。同理&#xff0c;在动作通信等其他类型的通信中也需要这么做&#xff0c;只是创建的文件夹的…

KV260视觉AI套件--开箱报告

目录 1. 简介 2. 与 Zynq 的渊源 3. 官方的入门步骤 4. 总结 1. 简介 传统的ARMFPGA或DSPFPGA控制方案在软件、逻辑、硬件以及系统工程的协同调试中&#xff0c;往往需要团队成员之间严格按照预定计划和接口规范进行分工合作&#xff0c;这不仅增加了测试过程的复杂性&…

【ElementPlus源码】Container 布局容器

文章目录 index.tsContainerheaderutilswithInstallwithNoopInstall hooksuseNamespace 单元测试 看源码时候做的笔记。如有错误请指出&#xff01; 关于路径的省略&#xff0c;详见button&#xff1a;【ElementPlus源码】Button按钮-CSDN博客 index.ts 导入一堆组件&#xff…

Centos7配置支持ftp文件传输功能

报错信息 适用于不支持ftp协议的centos7的系统。 报错信息&#xff1a;An error occurred while executing the action Connect. Details: No connection could be made because the target machine actively refused it. 解决办法 安装及启动等命令 # 安装vsftpd sudo yum…

Spark SQL 的总体工作流程

Spark SQL 是 Apache Spark 的一个模块,它提供了处理结构化和半结构化数据的能力。通过 Spark SQL,用户可以使用 SQL 语言或 DataFrame API 来执行数据查询和分析。这个模块允许开发者将 SQL 查询与 Spark 的数据处理能力结合起来,实现高效、优化的数据处理。下面是 Spark S…

高级运维工程师讲述银河麒麟V10SP1服务器加固收回权限/tmp命令引起生产mysql数据库事故实战

高级运维工程师讲述银河麒麟V10SP1服务器加固收回权限/tmp命令引起生产MySql数据库事故实战 一、前言 作为运维工程师经常会对生产服务器进行安全漏洞加固&#xff0c;一般服务厂商、或者甲方信息安全中心提供一些安全的shell脚本&#xff0c;一般这种shell脚本都是收回权限&…

C++实现简化版Qt信号槽机制(2):增加内存安全保障

在上一篇文章中《C实现一个简单的Qt信号槽机制》&#xff0c;我们基于前面的反射代码实现了信号槽的功能。 但是上一篇的代码中没有对象生命周期管理的机制&#xff0c;如果在对象的生命周期结束后还存在未断开的信号和槽连接&#xff0c;那么信号触发时可能会尝试访问已经被析…

Redis 7.x 系列【11】数据类型之位图(Bitmap)

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 概述2. 基本命令2.1 SETBIT2.2 GETBIT2.3 BITCOUNT2.4 BITPOS2.5 BITFIELD2.6 BITF…

【产品经理】订单处理10-分配快递策略

本次主要讲解下在订单处理过程中分配快递的策略以及分配快递中需要用到的设置。 一、建立快递档案 在ERP系统中&#xff0c;需要建立快递档案&#xff0c;设置所属快递、快递的服务类型、支持的打印模版以及快递在各个平台的电子面单支持情况。 二、仓库绑定快递 仓库需要设…

【动态规划】279.完全平方数

279. 完全平方数 难度&#xff1a;中等 力扣地址&#xff1a;https://leetcode.cn/problems/perfect-squares/ 没有刷过的小伙伴们请一定要先去刷一次&#xff0c;然后如果感兴趣的话再阅读以下内容&#xff0c;便于交流 ~ 多谢支持 ~ 问题描述 给你一个整数 n &#xff0c;返…

C# 入门—基本语法

一、数据类型 C# 语言中内置了一些基本的数据类型&#xff0c;数据类型用来指定程序中变量可以存储的数据的类型&#xff0c;C# 中的数据类型可以大致分为三类&#xff1a; 值类型&#xff08;Value types&#xff09;&#xff1b;引用类型&#xff08;References types&…

[推荐]有安全一点的网贷大数据信用查询网站吗?

在互联网金融日益发展的今天&#xff0c;网贷大数据查询网站成为了许多人申贷前的必备工具。随着使用这些网站的人群越来越多&#xff0c;安全问题也逐渐浮出水面。最近&#xff0c;就有许多用户反馈自己的个人信息在网贷大数据查询网站上被泄露。为了解决这一问题&#xff0c;…

Spring Cloud Gateway3.x自定义Spring Cloud Loadbalancer负载均衡策略以及实现动态负载均衡策略的方案

目录 前言 1.原理分析 1.1 ReactiveLoadBalancerClientFilter源码分析 1.2 LoadBalancerClientFactory源码分析 2.代码实现 2.1 扩展原生RoundRobinLoadBalancer轮询策略 2.1.1 自定义实现RoundRobinLoadBalancer 2.1.2 配置自定义的RoundRobinLoadBalan…

idea2024使用springboot3.x系列新建java项目,使用jdk17,启动项目报错

身为一名开发人员&#xff0c;敲代码无数&#xff0c;竟被一个小小启动给我卡了大半天&#xff0c;太丢脸了 报错一&#xff1a;Field infoSysRepository in com.erectile.Impl.PersonalInfoServiceImpl required a bean of type ‘com.erectile.jpa.repository.InfoSysReposit…

前端vue使用onlyoffice控件实现word在线编辑、预览(仅列出前端部分需要做的工作,不包含后端部分)

简介 ONLYOFFICE 文档 是一个开源办公套件&#xff0c;包括文本文档、电子表格、演示文稿和可填写表单的编辑器。 它提供以下功能&#xff1a; 创建、编辑和查看文本文档、电子表格、演示文稿和可填写表单&#xff1b; 与其他队友实时协作处理文件。 基于这个控件&#xff0c;…

微信小程序根据蓝牙RSSI信号强度测试设备距离

背景 在做小程序连接蓝牙设备的时候&#xff0c;有需求表明在搜索到0.5米之内的设备时自动连接 问题&#xff1a; 蓝牙模组只提供了RSSI信号强度&#xff0c;那又该如何计算蓝牙设备距离小程序的距离呢&#xff1f; 解决方案 通过以下公式做大量测试&#xff1a;求 A、n 的平均…

【深度学习】卷积神经网络CNN

李宏毅深度学习笔记 图像分类 图像可以描述为三维张量&#xff08;张量可以想成维度大于 2 的矩阵&#xff09;。一张图像是一个三维的张量&#xff0c;其中一维代表图像的宽&#xff0c;另外一维代表图像的高&#xff0c;还有一维代表图像的通道&#xff08;channel&#xff…

【LeetCode】四、栈相关:有效的括号 + 下一个更大的元素

文章目录 1、栈结构2、Java中的栈3、leetcode20&#xff1a;有效的括号4、leetcode496&#xff1a;下一个更大元素 1、栈结构 和队列相反&#xff0c;栈先进后出 时间复杂度&#xff1a;访问、插入、删除都在栈顶进行操作&#xff0c;时间复杂度为O(1)&#xff0c;搜索需要遍…

技术分享:分布式数据库DNS服务器的架构思路

DNS是企业数字化转型的基石。伴随微服务或单元化部署的推广&#xff0c;许多用户也开始采用分布式数据库将原来的单体数据库集群服务架构拆分为大量分布式子服务集群&#xff0c;对应不同的微服务或服务单元。本文将从分布式数据库DNS服务器的架构需求、架构分析两方面入手&…