【C++研发面试笔记】20. 常用算法-路径搜索算法(图算法)

来源:互联网 发布:整理收集文档软件 编辑:程序博客网 时间:2024/05/16 05:06

【C++研发面试笔记】20. 常用算法-路径搜索算法(图算法)

20.1 BFS与DFS

  • BFS:这是一种基于队列这种数据结构的搜索方式,它的特点是由每一个状态可以扩展出许多状态,然后再以此扩展,直到找到目标状态或者队列中头尾指针相遇,即队列中所有状态都已处理完毕。
  • DFS:基于递归的搜索方式,它的特点是由一个状态拓展一个状态,然后不停拓展,直到找到目标或者无法继续拓展结束一个状态的递归。

BFS:对于解决最短或最少问题特别有效,而且寻找深度小,但缺点是内存耗费量大(需要开大量的数组单元用来存储状态)。
DFS:对于解决遍历和求所有问题有效,对于问题搜索深度小的时候处理速度迅速,然而在深度很大的情况下效率不高。
总结:不管是BFS还是DFS,它们虽然好用,但由于时间和空间的局限性,以至于它们只能解决数据量小的问题。
这里写图片描述


20.2 Floyd

求多源、无负权边的最短路。用矩阵记录图。时效性较差。
Floyd-Warshall算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题。Floyd-Warshall算法的时间复杂度为O(N^3),空间复杂度为O(N^2)。边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行V次SPFA算法。

20.2.1 算法思想原理

Floyd算法是一个经典的动态规划算法。用通俗的语言来描述的话,首先我们的目标是寻找从点i到点j的最短路径。从动态规划的角度看问题,我们需要为这个目标重新做一个诠释(这个诠释正是动态规划最富创造力的精华所在)
从任意节点i到任意节点j的最短路径不外乎2种可能:

  1. 直接从i到j;
  2. 从i经过若干个节点k到j。

假设Dis(i,j)为节点i到节点j的最短路径的距离。

  1. 对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立。
  2. 如果成立,证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j)。
  3. 这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。

20.2.2 算法描述

  1. 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。   
  2. 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

20.2.3 具体实现

这里写图片描述


20.3 Dijkstra

Dijkstra算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。注意该算法要求图中不存在负权边。
问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径。(单源最短路径)

20.3.1算法思想

设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组:
第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了);
第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。
在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。
每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。

20.3.2 算法步骤

  1. 初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则

20.3.3 具体实现

这里写图片描述
这里写图片描述


20.4 Bellman-Ford

Dijkstra算法无法判断含负权边的图的最短路。如果遇到负权,在没有负权回路存在时(负权回路的含义是,回路的权值和为负。)即便有负权的边,也可以采用Bellman - Ford算法正确求出最短路径。
Bellman-Ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。其原理为持续地进行松弛,可以在更普遍的情况下(存在负权边和环路的情况)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=(V,E), 其源点为s,加权函数 w是 边集 E 的映射。对图G运行Bellman - Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。
Bellman-Ford算法是求解单源最短路径问题的一种算法。

20.4.1 适用范围

  1. 单源最短路径(从源点s到其它所有顶点v);
  2. 有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);
  3. 边权可正可负(如有负权回路输出错误提示);
  4. 差分约束系统;

20.4.2 算法描述

  1. 初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;
  2. 迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
  3. 检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。

20.4.3 具体实现

这里写图片描述
上面的代码有些问题!

考虑:为什么要循环V-1次?因为最短路径肯定是个简单路径,不可能包含回路的,如果包含回路,且回路的权值和为正的,那么去掉这个回路,可以得到更短的路径。如果回路的权值是负的,那么肯定没有解了。图有n个点,又不能有回路,所以最短路径最多n-1边,又因为每次循环,至少relax一边,所以最多n-1次就行了。


20.5 SPFA

与Bellman-ford算法类似,SPFA算法采用一系列的松弛操作以得到从某一个节点出发到达图中其它所有节点的最短路径。所不同的是,SPFA算法通过维护一个队列,使得一个节点的当前最短路径被更新之后没有必要立刻去更新其他的节点,从而大大减少了重复的操作次数。
SPFA算法可以用于存在负数边权的图,这与dijkstra算法是不同的。
与Dijkstra算法与Bellman-ford算法都不同,SPFA的算法时间效率是不稳定的,即它对于不同的图所需要的时间有很大的差别。
在最好情形下,每一个节点都只入队一次,则算法实际上变为广度优先遍历,其时间复杂度仅为O(E)。另一方面,存在这样的例子,使得每一个节点都被入队(V-1)次,此时算法退化为Bellman-ford算法,其时间复杂度为O(VE)。
SPFA算法在负边权图上可以完全取代Bellman-ford算法,另外在稀疏图中也表现良好。但是在非负边权图中,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法,以及它的使用堆优化的版本。通常的SPFA算法在一类网格图中的表现不尽如人意。
这里写图片描述


20.6 最大流问题

最大流问题主要解决这样一类问题:

在一个有向图中,有一个源点s和一个汇点t,图内每个相邻边都有一个容量(表示从一点流向另一点的最大量),我们要找到从源点s流向汇点t的最大流量。我们可以将边视为管道,每条管道都有其最大容量,而我们要求的就是从s流向t的最大流量。
这里写图片描述
下图是上图的一个最大流
这里写图片描述

20.6.1 流网络定义

如上图流网络可以定义为G(V,E),已知图内结点v,u∈V,f(v,u)表示从v流向u的流量,c(v,u)表示能从v流向u的最大流量(容量)。对于流网络有主要有性质:
这里写图片描述

20.6.2 Ford-Fulkerson算法

Ford-Fulkerson算法的思想:迭代求残存网络,找到残存网络的一条增广路径,然后将增广路径加入到流量网络中,直到找不到这样一条增广路径为止。
如下图所示,左图为流网络G,每条边标识了流量/容量,右图是一个残存网络,图中阴影路径是一条增广路径。
这里写图片描述

残存网络:
给定流网络G和流量f,Gf表示流网络G中仍可以调整的边结构。
残存网络Gf由残存容量构成,残存容量cf(v,u)表示某条边E(v,u)还能容纳的流量。其定义为:
这里写图片描述
可以看出残存网络Gf中的边包含两条边。

增广路径:
给定流网络G,流量f和残存网络Gf,增广路径表示在残存网络Gf中从源点s到汇点t的一条简单路径。
这里写图片描述

网络切割:
这里写图片描述

具体算法:
这里写图片描述


20.7 最小生成树问题

对于一连通的无向图G(V,E)来说,图内每条边(v,u)∈E,都有一个权值。找到无环子集就所有的结点都相连起来,且使得权重和最小,这样的问题就是最小生成树问题。下图描述了一个最小生成树:
这里写图片描述
最小生成树都是要
解决最小生成树问题主要有Kruskal和Prim算法两种。

20.7.1 Kruskal算法

Kruskal算法找到安全边的方法,是在所有连接森林中不同两棵树的边中,找到最小的边加入,两棵新树也形成了一棵新树。显然这一类贪心算法。
具体伪代码(这里要改为FIND-SET(v)≠FIND-SET(u),表示边的两个结点分别在不同两棵树中)
这里写图片描述

20.7.1 Prim算法

Prim算法同Dijkstra算法类似,其从某一源点出发,维持一个集合A(A内结点构成一棵树),算法每次选择从A出发到A外的结点中的一条权重最小的边,然后将这条边加入集合A中。
具体伪代码:
这里写图片描述

这篇博文是个人的学习笔记,内容许多来源于网络(包括CSDN、博客园及百度百科等),博主主要做了微不足道的整理工作。由于在做笔记的时候没有注明来源,所以如果有作者看到上述文字中有自己的原创内容,请私信本人修改或注明来源,非常感谢>_<

0 0