搜索与图论笔记
#搜索#
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.在判断是否要更新边的时候大于号小于号不要弄混了!