深度优先搜索策略从递归型结构中获得了一些最重要的属性。一旦我们在一个节点上启动了这一个操作,我们就得确保自己能在相关操作继续写去之前遍历完其他所有我们能到达的节点。其实任何递归函数都是可以用迭代操作来重写的。一种方法就是用我们自己的栈来模拟调用栈。
先来看一个迭代版的DFS:
# 迭代版深度优先搜索
def iter_dfs(G, s):
S, Q = set(), []
Q.append(s)
while Q:
u = Q.pop()
if u in S:
continue
S.add(u)
Q.append()
yield u
无论是DFS还是其他遍历算法,都可以一样适用于有向图。但是对于有向图来说,DFS就不太可能探索出一个完整的连通分量。
下面是一个通用性的图的遍历函数:
# 通用性的图的遍历函数
def traverse(G, s, qtype=set):
S, Q = set(), qtype()
Q.add()
while Q:
u = Q.pop()
if u in S:
continue
S.add(u)
for v in G[u]:
Q.add(v)
yield u
这里的默认类型是set。我们很容易就可以给他定义成stack类型。
class Stack(list):
add = list.append
使用每个版本的遍历方法都可以,我们都能用大致相同的形式表现他们。
回避之前访问过的节点会让我们不会陷入环路,以及能在无环路遍历中自然形成一个树结构。根据不同的树,有着不同的构造方法。DFS中,你可以把她叫做深度优先树。
在这种树的结构中,任意节点u下所有后代节点都会在u开始时被探索到完成回溯操作之间的这段时间被处理。
为了方便持续跟踪回溯操作,我们可以给他加一个时间戳,其中一种代表他被探索的时间(探索开始时,标示为d),另一种是我们回溯到该节点的时间(探索完成,用f来表示):
# 带时间戳的深度优先搜索
def dfs(G, s, d, f, S=None, t=0):
if S is None:
S = set()
d[s] = t
t += 1
S.add(s)
for u in G[s]:
if u in S:
continue
t = dfs(G, u, d, f, S, t)
f[s] = t
t += 1
return t
在这里d和f都应该是映射型的。根据DFS的属性,每个节点在DFS树中该节点的各个后代节点探索之前被发现(1),以及他们完成处理之后被完成(2)。(可用归纳法验证,也可以用递归推导)。
由此根据这个属性,可以直接用DFS进行拓扑排序。根据递减的完成时间来对节点进行排序。这种排序就是拓扑排序。
# 基于深度优先搜索的拓扑排序
def dfs_topsort(G):
S, res = set(), []
def recurse(u):
if u in S:
return
S.add()
for v in G[u]:
recurse(v)
res.append()
for u in G:
recurse(u)
res.reverse()
return res
for循环涉及到所有节点,确认所有节点都会被遍历到。
除了以上的几种方法外,还可以引入另外一个有意思的问题。无限迷宫和最短(不加权)路径问题。这时候就诞生了一个IDDFS算法。这是一种运行深度受到限制的、有限深度递增的DFS算法。
# 迭代深度的深度优先搜索
def iddfs(G, s):
yielded = set()
def recurse(G, s, d, S=None):
if s not in yielded:
yield s
yielded.add(s)
if d == 0:
return
if S in None:
S = set()
S.add(s)
for u in G[s]:
if u in S:
continue
for v in recurse(G, u, d-1, S):
yield v
n = len(G)
for d in range(n):
if len(yielded) == n:
break
for u in recurse(G, s, d):
yield u
再说另外的一种搜索方式,BFS算法要比IDDFS容易的多,只需在一般性的遍历框架上采用先进先出的队列类型即可。
结果就是先被反问道的节点会率先完成探索,这是我们能像在IDDFS算法中那样对图结构进行逐层进行探索。不过不用再对任何点和边进行多次访问,进而就能回复对于该算法线性级性能的保证。不顾这也可能使我们真实迷宫中用一种无法实现的方式在节点来回跳跃。
下面是一个一般性的遍历框架:
# 广度优先搜索
from collections import deque
def bfs(G, s):
P, Q = {s:None}, deque([s])
while Q:
u = Q.popleft()
for v in G[u]:
if v in P:
continue
P[v] = u
Q.append(v)
return P
如果搜索目标是一个巨大的树结构或者类似的状态空间时。IDDFS情况会好于BFS。因为不存在环路问题。IDDFS只需要存储从起点出发的单条路径就好。而BFS要在内存中持有全体的前沿节点。只有带有分叉,前沿节点就会随着与根节点距离之间的增大而指数级的上涨。
另外,Python中的list类型可以胜任stack的角色,但并不胜任queue的角色。尽管append是常数级别的操作,但是从前段的pop操作则是线性级的。
而在BFS操作中,经常使用两端式的队列。所以,这种队列通常都是用链表或者环状缓存区实现的。后者类似于动态列表。
Python中collections中提供了deque类。
在内部,deque的实现其实是一个块空间的双向链表,其中每个独立元素都是一个数组。这样比纯独立元素组成的链表近乎相等,但是能减低开销并且更加有效。
如果普通链表d[k]可以访问到d队列中第k个元素。但是如果deque对象中每个块空间都有b个元素,我们只需要遍历地k//b块空间就够了。
其实还有一些是讲SCC算法,即查找强连通分量算法,一般是用Kosaraju算法实现,这里只给出一个算法:
# Kosaraju的查找强连通分量算法
from os import walk
def tr(G):
GT = dict()
for u in G:
GT[u] = set()
for u in G:
for v in G[u]:
GT[v].add(u)
return GT
def scc(G):
GT = tr(G)
sccs, seen = [], set()
for u in dfs_topsort(G):
if u in seen:
continue
C = walk(GT, u, seen)
seen.update(C)
sccs.append(C)
return sccs
其中dfs_topsort()函数是前面基于深度优先搜索的拓扑排序中的。线性时间就可完成。
谢谢各位关注。祝大家天天快乐,每天都有进步。
天气转冷。注意保暖。