算法基础知识总结(搜索与图论)

三、搜索与图论

1、树与图的深度优先遍历

1、基本思想:利用深度优先搜素

2、树与图的存储与时间复杂度:

(1)邻接矩阵: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

(2)邻接表: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+ |E|) O(V+E) V 为 vertex 表示节点数,E 为 edge 表示边数。用邻接表存储图时类似于哈希表的拉链法,只不过不需要用哈希函数得到存储的位置

通常稠密图用邻接矩阵,稠密图是指边数与点数的平方大致为一个数量级的图。

3、作用:求树的重心、返回每个节点对应的子树中包含的节点的个数。

4、代码注意点:

(1)无论是DFS还是BFS都只会把每个节点遍历一遍不能重复。因此需要用一个状态数组如 st[N] 来记录当前节点是否已经被遍历过。

(2)树是一种特殊的图,一种无向连通图。

(3)无向图建边需要对称建立。

(4)用邻接表存储时要注意给 e[N]; ne[N]分配两倍于节点个数的空间,防止TLE。

(5)用邻接表存储时记得初始化。

(6)从任意一个节点开始搜索都可以。

2、树与图的广度优先遍历

1、基本思想:利用的广度优先搜索。BFS可以用于无权图的最短路搜索!有权图则需要用其他相应的最短路算法

2、作用:搜索最短距离

3、代码注意点:

(1)不要忘记队列的初始化。

(2)用邻接表存储时记得初始化。

3、拓扑排序

1、概念:拓扑排序是把一个有向无环图 (DAG)的所有节点排列成一个线性序列,并且对于图中的每条边 (u, v) ,在线性序列中节点 u 在节点 v 之前。

2、时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(V+E)

3、代码注意点:

(1) 每插入一条有向边就要给相应的节点增加一个入度。

(2)用数组模拟队列比较方便,因为出队、入队操作本质上是 tt 和 hh 指针的动作,队列中的元素并不会受到影响,这样最后队列中的元素就是一个拓扑序列。

(3)拓扑排序的结果不唯一。

(4)在最开始应该把所有的入度为0的节点全都放到队列中,因为入度为零的点都可以作为起点。

4、Dijkstra

1、概念:Dijkstra算法是一种求解单源最短路的算法,他可以在带权有向图中找到每一个点到起点的最短距离。

2、注意点:

(1)Dijkstra算法不可以用于有负权边的图,因为它有可能访问不到有负权的边;也不能处理有负环(环上权重的和为负数)的图,因为它会一直在负环中,得到的距离越来越小无法停止。

(2)Dijkstra算法用到了类似贪心的思想,它每次都从未访问的节点中选择到起点距离最短的作为下一个研究对象。

(3)BFS可以看作Dijkstra算法在无权图上的特例。

3、基本步骤:

(1)初始化每个节点到起点的距离,用 dist[] 数组来存储:起点初始化为0,其余点为正无穷。

(2)从起点开始,把起点作为当前研究对象。考虑“当前点”的所有邻接点并根据邻边上的权重计算他们到起点的距离。

(3)如果计算得到的距离小于该邻接点原本到起点的距离则更新它,否则不要更新

  • 松弛操作(relaxation process) 想象一张图,图中每个节点之间用一根弹簧相连,弹簧上的力表示到起点的距离。由于起初我们设定出了起点之外所有点到起点的距离为正无穷,相当于每一根弹簧都拉得很紧,那么每当找到一个更近的距离更新之后弹簧上的力就减小一些,就像是弹簧松弛了一些。显然Dijkstra算法会对“当前点”的每一个出变进行松弛操作。

(4)在剩下来的未访问过的节点中选择到起点距离最小的节点作为下一个研究对象即下一个“当前点”。重复上述过程。

(5)如果抵达了目标节点则停止搜索。

4、核心:Dijkstra算法是严格按照递增(非递减)的顺序找到各个顶点到起点的最短路。

5、时间复杂度:

(1)朴素Dijkstra: O ( ∣ V ∣ 2 + ∣ E ∣ ) O(|V|^2 + |E|) O(V2+E) 邻接矩阵存储

(2)堆优化Dijkstra: O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV) 邻接表存储 + 小根堆

6、朴素Dijkstra代码注意点:

(1)在写代码的时候为了方便思考和记忆可以把Dijkstra的步骤精简成三步:

​ 1°:初始化节点距离。起点为0,其余点为正无穷

​ 2°:在未访问过的节点集合中找到距离最小的点赋给变量 t 并把它加入到集合中表示已经访问过

​ 3°:用 t 更新其他点的距离,也就是比较其他点原本的距离例如 dist[ j ] 和从起点经过 t 走到 j 的距离 dist[ t ] + g[ t ] [ j ]

(2)对于包含重边的图,在存储时只存储边权小的。

7、堆优化Dijkstra代码注意点:

(1)朴素Dijkstra算法在步骤 2° 找最小距离的点耗时最多,复杂度为 O ( V 2 ) O(V^2) O(V2) 。如果采用小根堆,则每一次找距离最小的点时间复杂度为 O ( 1 ) O(1) O(1) ,由于总共要循环 V 次所以复杂度为 O ( V ) O(V) O(V) 。最后还要更新 E 个距离所以最终的时间复杂度为 O ( E l o g V ) O(ElogV) O(ElogV)

(2)使用堆的时候可以采用手写堆,好处是可以实现在任意一个位置的增删改查;也可以直接使用 priority_queue,当用到 pair 时,c++ 默认对第一个元素排序!

(3)用邻接表存储时有重边对最终结果没有影响,因为Dijkstra算法保证了会找到最短路。

(4)用邻接表存储时,每个节点可能出现不止一次,这是冗余备份,在循环中需要做一个判断,只处理未访问过的点。

5、Bellman-Ford

1、概念:Bellman-Ford 算法是一个用来求解单源最短路的算法,他可以用于带权有向图,图中允许存在负权边,并且能够判断图中是否存在负环。

2、基本操作:如果需要找到图上每个点到起点的最短路径,Bellman-Ford算法会循环 | V | - 1 次,每次循环都会对每条边进行松弛操作。

3、注意点:

(1)如果要判断是否存在负环,则在迭代完 | V | - 1 次之后再迭代一次,如果还有边能够被松弛,则根据抽屉原理,一定存在负环。

(2)Bellman-Ford算法的循环次数有着特殊的意义。例如循环的次数为 k 则代表走 k 步的最短距离。换言之Bellman-Ford算法可以用来求解有步数限制的最短路问题。

4、Bellman-Ford 算法于 Dijkstra 算法的区别:

(1)Bellman-Ford 算法可以用于带有负权边的有向图,并且可以判断图中是否存在负环。Dijkstra则两点都做不到。

(2)从两个算法搜索最短路的过程来看,Dijkstra算法像是一个比较审慎的人,在每走出下一步之前都会思考一下应该怎么走,而它思考的依据则是严格遵循非递减的顺序找到各个节点到起点的最短距离。也正因如此Dijkstra算法不会遍历每一条边,所以如果图中有负权边的话Dijkstra算法可能就找不到最短路。Bellman-Ford算法则是比较鲁莽的人,它不会管下一步应该选择走哪个点但是它也不会漏掉任何一条边,它会对每条边进行松弛操作,所以不会漏掉负权边的影响。

(3)由于(2)Bellman-Ford算法的时间复杂度就会比Dijkstra算法更高。在最坏的情况下是 O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(VE) 而堆优化版的Dijkstra算法的时间复杂度是 O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV)

(4)Bellman-Ford 算法和堆优化 Dijkstra 算法的松弛操作也不同。堆优化Dijkstra算法是每次选择距离起点最近的节点,对该节点的所有出边进行松弛操作;Bellman-Ford算法是对所有边进行松弛操作。换言之在Bellman-Ford算法中,每个点不一定只更新一次,但是Dijkstra算法则是每个节点只走一边。这也是Dijkstra算法和BFS类似的一个地方。

5、代码注意点:

(1)由于Bellman-Ford算法只需要遍历所有的边,所以可以用结构体来存储边

(2)要注意距离数组的冗余备份操作,这样可以在迭代开始之前“冻结”距离,避免在松弛操作过程中距离数组发生更新产生其他影响。举个例子:(A, B) = 1, (B, C) = 1, (A, C) = 3, 如果边数限制为1,那么只能迭代一次,并且这次迭代要遍历每条边。最开始 dist[] = {0, inf, inf},遍历AB之后,dist[] = {0, 1, inf}, 如果用这个结果再去遍历下一条边,就成了 dist[] = {0, 1, 2},发生了串联更新,这是要避免的!

A
B
C

(3)最后在判断是否存在最短路的时候不能像Dijkstra算法一样判断 dist[n] == 0x3f3f3f3f因为有可能Bellman-Ford算法经过了几个负权边,使得最后得到的距离略微小于设定的值,但是又确实没有最短路径。所以正确的做法是看一下题目中给出的条件,计算最大的距离,以它作为判断标准。

(4)初始化距离数组时要把第一个节点的距离设置成0 !!!

6、SPFA (Shortest Path Faster Algorithm)

1、简介:SPFA是Bellman-Ford算法的优化

2、SPFA与Bellman-Ford算法的区别:前面提到Bellman-Ford算法是一个比较鲁莽的人,它每次迭代都不管三七二十一就遍历所有边,事实上只有那些被松弛了的边才是对下一次迭代有效的。所以SPFA的思想就是创建一个队列,每次把被松弛的节点添加到队列中,每次也是从队列中弹出一个节点来做松弛操作。

3、时间复杂度:

(1)平均: O ( ∣ E ∣ ) O(|E|) O(E)

(2)最坏:退化成和Bellman-Ford算法一样为 O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(VE)

4、代码注意点:

(1)SPFA和堆优化版的Dijkstra算法十分相似,他们的一个重要区别就在于堆优化Dijkstra算法每个节点只访问一遍但是SPFA算法中每个节点有可能还会重新进入队列,是一个动态更新的过程!

(2)由于采用邻接表存储图,所以在向队列添加被松弛过的节点的时候需要避免重复添加同一个节点。

(3)用SPFA判断负环时,需要再开一个数组cnt[N] 用来存放当前最短路中的边数,然后再根据抽屉原理 (把多于n个的物体放到n个抽屉里,则至少有一个抽屉里的东西不少于两件) 判断负环是否存在。当 cnt[x] >= n 的时候说明至少经过了 n + 1 个点,那么至少有一个点经过了两次,由于我们是在松弛操作的时候更新最短路的边数的所以这个点能经过两次就意味着它这里是负环。

(4)用SPFA判断负环时,需要提前把所有节点都压入队列,因为我们并不知道负环的起点在哪。

7、Floyd

1、概念:Floyd算法可以用来解决带权有向图中的多源最短路问题,图中可以有负权边但是不能有负环。

2、核心思想:
 shortestPath  ( i , j , k ) = min ⁡ (  shortest  Path ⁡ ( i , j , k − 1 ) ,  shortestPath  ( i , k , k − 1 ) +  shortestPath  ( k , j , k − 1 ) ) \begin{array}{l} \text { shortestPath }(i, j, k)= \begin{array}{l} \min (\text { shortest } \operatorname{Path}(i, j, k-1), \text { shortestPath }(i, k, k-1)+\text { shortestPath }(k, j, k-1)) \end{array} \end{array}  shortestPath (i,j,k)=min( shortest Path(i,j,k1), shortestPath (i,k,k1)+ shortestPath (k,j,k1))

从起点 i 走到终点 j,如果这之间有 k 个中间点(1,2,3,……,k),我们可以经过点 k 也可以不经过点 k,所以只需要比较这两种情况哪个距离更短就行了。

3、时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)

4、代码注意点:

(1)Floyd算法通常采用邻接矩阵,初始化时,显然主对角线因该都是0,其余元素为正无穷

(2)当图中有重边时,要注意只考虑边权小的就行。

(3)注意一下题目里的节点编号,如果是从 1 到 n 那么存储的时候应该从下标 1 开始。

8、最短路问题小结

最短路算法
单源最短路
多源最短路
Floyd
所有边权都是正数
存在负权边
朴素Dijkstra
堆优化Dijkstra
有边数限制
Bellman-Ford
没有边数限制
SPFA

9、Prim

1、概念:Prim算法是一种在带权无向图中创建最小生成树的算法,它用到了贪心的思想。

2、基本操作:

(1)把所有点的距离(到)初始化成正无穷,随机选择一个点作为当前的“树”

(2)更新当前“树”的所有邻接点到树的距离,把距离最近的一个点和对应的边添加进来,树就生长了一步

(3)重复操作(2)直到所有顶点都在树中

3、核心:从顶点出发,扩展成一颗边权总和最小的生成树。

4、时间复杂度:

(1)用邻接矩阵存储: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

(2)用邻接表存储: O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV)

5、代码注意点:

(1)Pirm算法和朴素Dijkstra算法的代码十分相似,它们的一个重要区别就是,Prim算法在更新距离时比较的是目前顶点到当前生成树的距离,朴素Dijkstra算法比较的是目前顶点到起点的距离。换言之Prim算法中的 dist[] 数组存储的是节点到当前生成树的距离

(2)Prim算法初始化距离数组时每个点都初始化成了正无穷,然后理论上随机选择一个顶点都行,Dijkstra算法则是把起点初始化成0

10、Kruskal

1、概念:Kruskal算法也是用来在带权无向图中寻找最小生成树的。它也用到了贪心的思想。

2、基本步骤:

(1)首先创建一个森林( m( m ≥ 0 m\geq0 m0)棵树的集合 ),即图中的每一个顶点都是一棵树

(2)创建一个包含了图中所有边的集合 S

(3)每次从集合 S 中弹出一条边权最小的边(事先要对边权排序),如果这条边能连接两棵树,并且不构成环,就把这条边添加到森林中,即把两棵树合并成了一棵树

(4)重复步骤(2)、(3)直到图的每个连通分量中都只有一棵树。

3、核心思想:按照边权递增的顺序生成一棵树。

4、时间复杂度: ∣ E ∣ l o g ∣ E ∣ |E|log|E| ElogE

5、代码注意点:

(1)注意在构件边的结构体时要重载小于号

(2)用到了并查集的知识,记得对边的结构体排序

11、最小生成树小结

最小生成树算法的应用就是村村通问题,要在N个村子之间铺设道路,怎样才能花费最少的钱财。Prim算法和Kruskal算法分别从顶点出发和从边出发来解决这个问题,它们都用到了贪心的思想。对于Prim算法,每次都是把距离当前树最近的顶点添加进来,对于Kruskal算法每次则是把最短的路径添加进来。

12、染色法判断二分图

1、二分图:二分图中的所有顶点能够分成两个相互独立的集合 S , T S, T S,T ,并且所有边都在集合之间而集合之内没有边。二分图的一个重要性质是二分图中无奇数环

2、染色法的基本步骤:利用深度优先搜索,从任意一个顶点开始染色,共有两种颜色,保证每个顶点的颜色与它的父节点和子节点都不相同。

3、时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(V+E)

4、代码注意点:

(1)在DFS的时候需要做两个判断,如果下一个点的颜色已经和当前点的颜色相同了,则不正确;如果下一次染色不正确则整个过程也必定不正确。

(2)如果图是无向图,又是用的邻接表存储则要为边数组和值数组开两倍于顶点个数的空间,否则会TLE。

13、匈牙利算法求二分图最大匹配

1、二分图最大匹配:首先,二分图的匹配是指对于一个二分图 G G G,他可以分成两个子图 S , T S, T S,T 对于 S S S 中的所有点,每个点只有采用一条边。简而言之,就好比男生女生配对,男生为集合 M M M, 女生为集合 W W W ,一个男生可能认识多个女生,该男生和每个认识的女生之间都有一条边,但是在配对的时候只能一 一配对,不能妻妾成群。那么最大匹配就是求在这些男生、女生当中最多能凑成多少对。

2、基本思路:为了便于理解,假设匹配的过程是由男生选择女生并且女生不会拒绝。如图所示,现在有5个男生(蓝色),5个女生(粉色)。让所有男生排成一列,排在前面的是排在后面的大哥,大哥都会让着小弟。比方说大哥人脉很广(备胎很多),5个女生全都认识;二哥认识1、3、5号女生;三哥认识2、3号女生;四哥认识4、5号女生;五弟就比较菜了,只认识3号女生。

匹配的过程就是,首先从大哥开始,五个兄弟都是从自己认识的女生里按顺序选择,那么大哥自然和1号女生配对成功了(牵了红线)。接下来是二哥,二哥自然首选也是1号女生,但是她已经和大哥凑成一对儿了,不过大哥会让着二弟,所以就把1号让给二哥了(1号被绿了),自己去和2号女生配对了。二哥的问题解决了,接下来是三哥,好巧不巧,三哥首选居然是2号,又和大哥撞了,那大哥又把2号让给了三弟,自己去和3号配对了。接下来轮到四哥,万幸四哥首选是4号,可以顺利配对。最后是五弟,造化弄人,五弟只认得3号女生,3号又已经和大哥配对了,叵耐大哥路子广,还认识5号所以就把3号让给了五弟,自己和5号配对。最终兄弟5人都匹配成功了,所以这个图的最大匹配数就是5!

在这里插入图片描述

3、时间复杂度: O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(VE)

4、代码注意点:

(1)创建match数组用来存储第二个集合中每个点当前匹配到的在第一个集合中的点

(2)创建st数组用来记录第二个集合中的点是否已经被考虑过了,每一次为“男生”匹配时都要清空 st 数组!

(3)搜索时需要判断当前点是否已经又匹配的点了,或者是否能为当前已经匹配的点找另一个点,如果能的话就返回true

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值