搜索与图论笔记

#搜索#
1.BFS最短路性质
边的权重为1时,BFS有最短路性质,DFS不具有
2.应用场景
BFS:最小步数,最短距离,最少操作几次
DFS:算法思路比较奇怪,对空间要求比较高

3.DFS回溯和剪枝
DFS,又称爆搜,搜索顺序可以看成一棵树
DFS回溯(恢复现场):每完成一次DFS之后紧接着就是恢复现场,即dfs和回溯要在一个区域块之内
DFS剪枝:提前判断当前方案是否合法,如果不合法就没必要往下搜了,字节回溯
有最优性剪枝,可行性剪枝
4.BFS一般需要用到queue

#图#
1.树是一种特殊的图,无环连通图
2.图:有向图,无向图,无向图可以看做特殊的有向图(一条无向变可以看做是两条有向边),所以我们一般考虑有向图就行了
3.有向图的存储:
(1)邻接矩阵(二维数组):空间复杂度O(n^2),比较浪费空间,使用较少,且不能体现重边,因此比较适合存贮不需要考虑重边的稠密图
(2)邻接表(单链表):空间复杂度O(n),每个点都有一个单链表,用来存贮从这个点出发可以走到那些点,插入时使用头插法,单链表的次序是无关紧要的,适合稀疏图
注意:区分顶点和顶点下标
基本上邻接表存储的都是顶点下标,而最短路径dist数组以及状态数组st,队列中的元素存储的都是顶点,不要搞混了。

4.树和图的遍历(因为数可以看做特殊的图,所以我们只考虑图就行了)
(1)DFS、BFS
(2)因为深度和宽度有限搜索只会遍历一次,所以要设置一个bool数组保存某个点有没有遍历过

5.拓扑序列
(1)图的宽度优先遍历的一个应用
(2)针对有向图,无向图没有拓扑序列概念
(3)所有的边都是从前指向后,不可能有环
(4)拓扑图:有向无环图

6.最短路
(1)源点:起点    汇点:终点
(2)稠密图:m~n^2    稀疏图:m~n    (m表示边,n表示点)
(3)考察核心:建图(把一个问题抽象成一个最短路问题,如何定义点,如何定义边的含义)
(4)只考虑有向图,无向图的一条无向边看成两条有向边
(5)堆优化djisktra算法    
    1.将以一个节点放入优先队列
    2.只要队列不为空,取出队头
    3.如果队头已经找到最小距离,退出,否则拓展队列
    4.拓展所有与队头相连的点,如果可以更新距离,就加入队列
(6)如果有负权回路(整个回路的权值和为负数)的话,最短路不一定存在
解释(叠buff):
假设存在这样一个负权回路abc,现在求顶点(1, v)的最短距离,如果到达v的最短路径必然要经过abc中的至少一条边,那么会在这个回路无限转圈,因为,每走一次这个回路,最短距离就会变小,这样1就始终无法到达v。
如何判断是否存在负环:
假设图中有n个点,如果在第n次操作,最短路更新,说明此时有n条边,而n条边就对应n+1个点,n+1>n,说明有顶点重复出现了,又因为每次更新都是选择的最短路径,所以只能是负环。简而言之,如果存在一条边数为n的路径,就存在负环。
应用:找负环(时间复杂度较高,几乎不用)
注意:不是存在负环就一定没有最短路,只有当负环在要求的最短路的路径当中时才没有最短路
(7)spfa
    1.只要图当中没有负环,就可以使用spfa
    2.99.9的最短路问题是没有负环的
    3.spfa其实就是对Bellman-Ford算法的优化

(8)正权图优先djikstra,负权图优先spfa,正权图也可以使用spfa算法,并且一般情况下时间效率更高,但是spfa的最坏时间复杂度是O(mn)级别的,有时候会被卡(被卡的情况可能是一个稀疏网格图)
7.最小生成树
(1)最小生成树研究的是无向图
(2)因为最小生成树没有环,所以正权边和负权边都可以
(3)朴素Prime算法堆优化:
    1.用堆(优先队列)来维护最小值,每次找出最小值的时间复杂度        为O(1),一共有n次操作,时间复杂度总共为O(n)
    2.用t来更新其他点到集合的距离O (n)
    3.遍历每个点的所有边O(m),将这个元素插入堆O(logn)
    4.所以总共时间复杂度为O(mlogn)

8.二分图
(1)定义:
简而言之,就是顶点集V可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻.
(2)无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数,即图中不含有奇数环。
(3)由(2)可知,不含环的图一定是二分图。
(4)二分图判断方法:染色法

#模板的易错点#

1.队列对应邻接表

在所有需要用到队列或者优先队列的数据结构的模板中,存储边的数据结构都是邻接表,因为我们需要用到边与边之间的关系

2.初始化

(1)邻接表(三步走)

  • (1)设置h[],e[],ne[],idx有时候还有w[]。

    (2)设置add()函数。

    (3)初始化h[]数组。

  • 为了防止忘记初始化h[]数组等情况,所以如果需要使用邻接表,就一下走完三步。
  • 另外需要注意的是初始化h[]数组要在我们执行add()操作之前。

(2)所有的最短路都有两行代码:

memset(dist, 0x3f, sizeof dist);//初始化最短路

dist[1] = 0;        //标记1为起点

(3)邻接数组

  • i == j,g[i][j] = 0
  • i != j, g[i][j] = 0x3f3f3f3f

3.spfa算法两点注意事项

  • spfa求负环的时候,需要把所有边都先放入队列,因为负环不一定是从1作为起点开始的
  • cnt数组表示从某个点出发,到点i的距离,我们需要知道是从那个点作为起点
    因为我们知道他的前驱(邻接表),假如有一条i指向j的边,那么dist【j】=dist【i】+1
  • 很容易犯得错误写法:dist【j】++ ;     //误认为每次更新都是增加了一条边,而忽视了j也是是由i决定的 

4.区分djikstra算法和spfa算法中st数组的作用

  • djikstra算法的朴素版和堆优化版的st数组都只标记一次,用被标记的点来更新最短路,如果遇到被标记的点,就跳过执行下一次循环。
  • 注意:在堆优化djikstra算法中,一个点只要还没有被使用,就可以一直入队,因为只要入队就说明它的距离被更新了,而我们使用优先队列只会使用最短的哪一个。
  • 这一点要区别于spfa算法,spfa算法保存的值是点,而不是pair<距离,点>,所以不能重复入队。
  • 但spfa算法的st数组是可以多次标记的,它的主要是减少元素重复入队执行更新操作。
  • 两种算法st数组不一样的根本原因在于:
  • djikstra算法的核心是找到当前状态下的最短路径,用这条路径来更新其他路径,当我们使用这条路径的时候,这条路径肯定就是最短路了,我们就不能再动它了,即我们只用该边执行一次松弛操作,所以st数组只能标记一次
  • 而spfa算法的核心是通过执行多次松弛操作找到最短路,所以一条路径可以使用多次,st数组也就可以多次标记

5.松弛操作

  • 在单源最短路径的求解算法中,往往涉及松弛操作,而松弛操作是证明单源最短路径算法的基石。

  • 相关概念
    最短路径:假设从结点u到结点v的最短路径权重为δ(u,v)δ(u,v),那么从结点u到结点v的最短路径定义为任何一条权重ω(p)=δ(u,v)ω(p)=δ(u,v)的从u到v的路径p。
    最短路径估计:对于每个结点v来说,我们维持一个属性v.d,用来记录从源结点s到结点v的最短路径权重的上界,称v.d为s到v的最短路径估计。
    前驱结点:对于每个结点v,我们维持一个前驱结点v.πv.π,它表示扫描v.πv.π结点时发现结点v。
    单源最短路径算法进行之前,都必须对最短路径估计和前驱结点进行初始化,伪代码如下:
    INITIALIZE-SINGLE-SOURCE(G,s)

for each vertex v \in G.V
  v.d=INFTY
  v.pi=NIL
s.d=0
  • 结点的v.d属性和v.πv.π属性发生变化是通过松弛操作完成的,对边(u,v)在O(1)时间内进行松弛操作的伪代码如下:
    RELAX(u,v,w)

if v.d>u.d+w(u,v)
  v.d=u.d+w(u,v)
  v.pi=u
  • 松弛操作的性质
    1。三角不等式性质:对于任何边(u,v)∈E(u,v)∈E,有δ(s,v)≤δ(s,u)+ω(u,v)δ(s,v)≤δ(s,u)+ω(u,v)。
    2。上界性质:对于所有结点v∈Vv∈V,我们有v.d≥δ(s,v)v.d≥δ(s,v)。一旦v.dv.d的取值达到δ(s,v)δ(s,v),其值将不再变化。
    3。非路径性质:若从结点s到结点v之间不存在路径,有v.d=δ(s,v)=∞v.d=δ(s,v)=∞。
    4。收敛性质:设结点u,v∈Vu,v∈V,如果s⇝u→vs⇝u→v是图G中的一条最短路径,且在对边(u,v)进行松弛前的任意时间有u.d=δ(s,u)u.d=δ(s,u),则在之后的所有时间有v.d=δ(s,v)v.d=δ(s,v)。
    5。路径松弛性质:若p=<v0,v1,...,vk>p=<v0,v1,...,vk>是从源结点s=v0s=v0到结点vkvk的一条路径,且我们对pp中所进行松弛的次序为(v0,v1),(v1,v2),...,(vk−1,vk)(v0,v1),(v1,v2),...,(vk−1,vk),则vk.d=δ(s,vk)vk.d=δ(s,vk)。
    6。前驱子图性质:对于所有结点v∈Vv∈V,一旦v.d=δ(s,v)v.d=δ(s,v),则前驱子图是一棵根结点为<script type="math/tex" id="MathJax-Element-5295">s</script>的最短路径树。

  • 松弛是改变最短路径和前趋的唯一方式。各个单源最短路径算法间区别在于对每条边进行松弛操作的次数,以及对边执行松弛操作的次序有所不同。在Dijkstra算法中,对每条边执行一次松弛操作。在Bellman-Ford算法,spfa算法,多源最短路Floyd算法中,每条边要执行多次松弛操作。

6.INF / 2

Floyd,Bellman_Ford,spfa算法判断最短路是否存在都是用的 dist[n] > INF / 2

而不是 dist[n] == INF,因为这三个算法都需要用到多次松弛操作

 7.优先队列

  • 不同于map和set的默认升序,优先队列是默认降序的

    priority<int, vector<int>, less<int> > p;        //降序

    priority<int, vector<int>, greater<int> > p;        //升序

    注意less 和 greatre 后面也要加 <type>

  • 对于优先队列,不可以使用heap.top() = 10这样的赋值方式,如果想要改变队头的值,先执行pop()再push()

8.最小生成树和最短路的更新方式

  • 最小生成树是用 {当前最小生成树集合} 中的一个点t到目标点的路径长去更新目标点到 {集合}的最短路,所以说最小生成树的松弛操作是 dist[a] = min(dist[a], g[t][a])
  • 而最短路是用起点到某个中间点t的路径长+中间点t到该点的路径长去更新目标点到起点的最短路径,所以说最短路的松弛操作是 dist[a] = min(dist[a], dist[t] + g[t][a]);
  • 我们发现最小生成树的松弛操作了一个dist[t],这是因为dis[t]这段距离已经包含在了最小生成树所在的集合当中,我们现在找到是从集合往外伸出去的一段路径,自然就不用再考虑这段路径了

9.bellman_ford 的backup数组

10.无向边的边数开两倍,在添加边的时候也要添加两条边

  • 邻接数组邻接表邻接矩阵 
  • 二分图的最大匹配除外

11.在判断是否要更新边的时候大于号小于号不要弄混了!

posted @ 2022-05-05 08:41  光風霽月  阅读(24)  评论(0编辑  收藏  举报