在NTU课程:MAS714 (3)Graph Algorithms_UQI-LIUWJ的博客-CSDN博客中,我们讲了图中点遍历的问题,其中,我们讲到SmartExplore:
正如之前分析的那样,它的时间复杂度是O(m+n)【n是顶点数,m是边数】
那么,我们应该用什么数据结构来实现这个呢?
1 队列与栈
1.1 队列
先进先出 FIFO (First In First Out)
一般用于BFS(广度优先遍历)(Breath First Search)
1.1.1 队列的操作
enqueue(Q,x)——将x加入Q的末尾
dequque(Q)——移除队列Q的第一个元素
1.1.2 队列的实现
- 链表,同时记录链表的头节点和尾节点
插入节点:tail.next=new; tail=tail.next
删除节点: head=head.next
2. 队列,记录头节点和尾节点
1.2 栈
后进先出 LIFO(Last In First Out)
一般用于DFS(深度优先遍历)(Depth First Search)
1.2.1 栈的操作
push(S,x):将元素x放至stack的栈顶
pop(S):将stack栈顶的元素弹出栈
peek(S):查看stack栈顶的元素(但不弹出)
1.2.2 栈的实现
只需要维护一个栈顶指针即可
2 广度优先搜索
其本质上还是SmartExplore,只是用了特定的数据结构来表示之。所以时间复杂度仍然是SmartExplore的O(m+n)
2.1 广度优先实例说明
以此图为例:
一开始从1节点开始,队列中推入1(此时队列【1】) ,将1标记为visited(绿色节点)
左边就是目前索引到的数,右边是BFS对应的搜索树T(后同)
然后弹出1节点,将1节点的unvisited邻居2和3插入队列(此时队列【2,3】),将2和3标记为visited
然后弹出2节点,将2节点的unvisited邻居4和5插入队列(此时队列【3,4,5】),将4和5标记为visited
然后弹出节点3,将节点3的unvisited邻居7和8插入队列(此时队列【4,5,7,8】),将7和8标记为visited
然后弹出4,因为4的邻居都是visited了,所以没有需要入队列的(此时队列【5,7,8】)
然后弹出5,将5的unvisited邻居6入队列,并设置为visited(此时队列【6,7,8】)
以此类推,最终有:
2.2 带距离的广度优先
和前面的广度优先一样,只不过在其基础上多了一步设置和初始点之间的distance这一操作
时间复杂度依旧是O(m+n)
2.2.1 dist的性质
2.3 一致代价
当所有搜索步骤的花费相同时,宽度优先是最优算法,因为它永远只展开最浅的节点
当各搜索步骤的花费不一致时,我们能可以将BFS拓展到“一致代价问题”,我们将展开最浅一层的节点改成展开当前路径花费g(n)最小的节点
此时我们就不是使用BFS的数据结构 队列了,我们这里使用优先队列,存储开节点集(开节点指的是还没有被展开,但是即将要被展开的节点)
但是注意一点,目标测试(判断是否找到了target),是在展开某一个子节点的时候进行,而不是生成这个节点的时候——>第一次遇到这个节点的时候,未必是最优的情况
第二点需要注意的是,当发现一个子节点已经在优先队列中了,但是此次到这各子节点的路径花费更少——>可以用更短的花费代替子节点在优先队列中存储的原有花费。
2.3.1 伪代码
2.4 BFS总结
- 从u开始,“一圈一圈”向外计算
- 使用邻接列表的话,时间复杂度为O(m+n)
- 可以到达所有和u有路径的点
- 可以用来计算在无权重图中和u的最短距离
3 DFS
DFS算法和BFS算法的时间复杂度一致,那为什么要DFS呢?因为空间复杂性。广搜算法是一圈一圈向外搜索的,所以理论上最差需要存储所有的节点;而DFS只需要存储从根节点到叶节点的一条路径,加上路径上每个节点未展开的节点即可
3.1 手动维护栈的DFS
3.2 使用递归的DFS
3.2.1 RAM,编译器与递归
在RAM和硬件中,理论上是不支持递归的。那么递归是怎么实现的呢?
在编译器编译的时候,如果需要递归,那么编译器自己会维护一个栈,来将递归的状态保存起来(不用人为去维护栈)
比如我们先调用DFS(X),然后DFS(X)中会调用DFS(Y):
那么我们在进行DFS(X)的过程中遇到了DFS(Y),那么我们就先把DFS(X)的状态(变量,寄存器状态等)先保存起来,放到栈里面去,然后执行DFS(Y)。当DFS(Y)执行完毕之后,将DFS(X)的状态弹出,恢复到DFS(X)未完成的那时候,然后继续DFS(X)的剩余语句。
3.3 DFS执行过程举例
假如有这样一个有向图
我们从1开始,那么1 先进栈,并将1设置为visited
然后看栈顶元素1,有没有unvisited的邻居,发现有(2),那么将2进栈,并将2设置为visited(此时栈的情况为【1,2】
然后看栈顶元素2,看他有没有unvisited的邻居,发现有,是5,于是将5弹入栈中,并将5设置为visited(此时的栈情况为【1,2,5】)
然后看栈顶元素5,看看他有没有unvisited的邻居,发现有,是4,于是把4弹入栈中,并将4设置为visited(此时栈的情况为【1,2,5,4】)
然后看栈顶元素4,看看他有没有unvisited的元素,发现没有,那么将4pop出来
然后看栈顶元素5,看看他有没有unvisited的元素,发现没有,那么将5pop出来
以此类推,直到栈为空为止。
此时我们从1出发可以到达的点就都遍历好了。
3.4 DAG
相当于把无向图中树的概念推广到有向图中
3.4.1 DAG的判别
当一个图G的DFS树没有back edge的时候(3.3例子中的红边),那么此时G是一个GAF
3.5 DFS的总结
- 时间复杂度也是O(m+n) (因为也相当于是用另一种数据结构来实现SmartExplore
- 帮助理解有向图结构
4 迭代加深算法 iterative deepening search
把宽度优先和深度优先相结合,以便发现最好的搜索深度。这种方法一次设置搜索深度,从0,1,2,一直到找到解的深度
5 双向搜索
同时运行两个搜索程序,一个是从初始状态向后搜索,一个是从目标状态向前搜索