八数码深度优先搜索_深度优先搜索(二)

1)上一篇给出了深度优先搜索遍历一个连通图时的简易代码,我们用不同的颜色代表当前图中结点所处的状态:未被访问的结点均为白色(WHITE),在发现一个新的未被访问过的结点时,将其标记为灰色(GRAY)。其实,在遍历过程中,结点除了上述两种状态外,还有第三种。考虑将发现一个白色结点a并将其标记为GRAY之后,我们开始对它的邻接点进行遍历,并在完成了对邻接点的遍历之后,将结点a标记为黑色(BLACK),也就是黑色代表着结束。

def DFS(begin_node):
    # 将当前顶点标记为GRAY,代表已经访问
    begin_node.color = GRAY
    # 访问当前顶点的邻接顶点
    for adj_node in begin_node.adj_node_list:
        if adj_node.color == WHITE:
            DFS(adj_node)
    # 新增的状态:在完成邻接点的遍历后,标记为黑色
    begin_node.color = BLACK

上一篇中没有提到将结点标记为BLACK这一步,因为当时这一步对解题并没有什么作用。但是这一步在某些时候是必须的,考虑一种经典算法:拓扑排序。

2)我们可以将拓扑排序看作是序列化一个有向图的方法。排序后得到的序列应满足:如果图G包含从v到u的边,则结点u在拓扑排序中位于v的前面。比如下图,它的拓扑排序为a, b, c, d, e或者a, b, d, c, e。

6d3d219f7ef1aeabcd9e26f9332149ae.png

如果图中的结点代表事件,事件a指向事件b代表:完成事件b之前必须先完成事件a,那么这个图的拓扑排序就代表了图中所有事件完成的一种可能的先后顺序。当然拓扑排序不是所有有向图都存在的,一种判别的方法是:有向无环图一定存在拓扑排序。因为一旦a和b之间存在环,则a,b谁也不能在谁的前面,则拓扑排序不存在,下面讨论时假设序列存在。

3)使用深度优先搜索求拓扑序列。

使用深度优先搜索求解拓扑排序,就要用到我们先前说的BLACK这一状态。一个结点的颜色是BLACK说明了这样一件事情:以它为根结点的所有子结点都已经被访问到并被标记为了黑色。这是因为完成结点a的深度优先搜索前,会先对它的所有邻接点依次递归地进行深度优先搜索,完成以后才会将a标记为黑色,而此时a的邻接点已经完成了深度优先搜索,先于a变成了黑色。简单一点就是:对a的邻接点的深度优先搜索要先于a结束。这一点就是用来求解拓扑排序的方法。

拓扑排序规定:若a指向b,则a应在b之前。而深度优先搜索具有这么一个特性:若a指向b,则b先于a结束深度优先搜索。虽然目前顺序好像反了,但是问题不大。得到一个逆序的序列,只要再求一次逆序即可。这个过程可以用下面这个简图描述:

58df04f3219e0a3c057d33a05000e7b6.png

图中四个结点的结束顺序为:d, c, b, a正好对应一种逆序的拓扑序列:a, b, c, d。好了,原理大概就是这个样子,代码的话我们只要把标记当前结点为BLACK,改为把当前结点加入当前逆拓扑序列的末尾即可:

def DFS(begin_node, res):
    # 将当前顶点标记为GRAY,代表已经发现,并开始访问
    begin_node.color = GRAY
    # 访问当前顶点的邻接顶点
    for adj_node in begin_node.adj_node_list:
        if adj_node.color == WHITE:
            DFS(adj_node, res)
    # 在完成邻接点的遍历后,加入序列res的末尾 
    res.append(begin_node)

当然得到的res是逆序的拓扑序列,需要再一次对它求逆。

4)一道例题:

LeetCode中的第802。

题述比较啰嗦,我在这里概括一下,给出一个有向图G,如果从图中的结点a开始进行遍历最终一定会到达一个终端结点(没有任何邻接点的结点),那么就称a是安全的结点。要求设计一个算法找出所有的安全结点。

可以先看一个示例,输入的图G如下:

584b551f8192429a5fb934fd5ac730f4.png

最终输出为:[2,4,5,6]。以2,4,5,6为起点最终一定会到一个“死胡同”也就是终端结点。0,1,3不行是因为它们或者它们和其他结点可以组成环,之后一直在环里打转而进不了“死胡同”,故它们是不安全的。

还有一点就是图G是以邻接表的形式给出的:N个结点的图中结点的标号为:0到N-1。邻接表graph[i]中存储了结点i的所有邻接点的序号。

这道题的思路是这样的:如果一个结点是安全的,即以它为起点最终一定到达一个终端结点,那么以它为起点(根结点)的子图一定不能有环。这时仅靠GRAY和WHITE两种结点状态是不行的,我们需要明确指出代表结束的BLACK状态。

这样,如果在搜索过程中发现了某个邻接点是灰色的,则可说明图中存在环路。因为灰色代表正在对该结点的邻接点等一系列“子结点”进行递归搜索,而此时又发现“子结点”指向了“父节点”,则可说明有环路,大概是下面这种情况:

05dbb731faf21fe59c91c3abe1c5de99.png

细节可以看代码:

def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
    WHITE, GRAY, BLACK = 0, 1, 2
    # 初始化所有结点的颜色为白色
    node_color = [WHITE] * len(graph)
    # 以root为根结点进行深度优先搜索
    def dfs(root):
        node_color[root] = GRAY
        # 遍历邻接点
        for adj_node in graph[root]:
            # 邻接点是黑色的说明是安全的,不需递归判断
            if node_color[adj_node] == BLACK:
                continue
            # 邻接点为灰色,说明存在环路,则返回假
            # 只有邻接点为白色时,才会递归进行判断
            if node_color[adj_node] == GRAY or not dfs(adj_node):
                return False
        # 在没有遇到环路的情况下完成遍历,说明root是安全的,标记为黑色
        node_color[root] = BLACK
        return True
    res = []
    for i in range(len(graph)):
        # 将符合条件的结点放入res中
        if dfs(i):
            res.append(i)
    return res

5)总结:这一篇是对上一篇的补充,主要是指出深度优先搜索时结点的BLACK状态的作用,这其实对应着,在结束邻接点的遍历后,对当前结点进行一些操作以达到某种目的,有些时候这些操作是不可或缺的。之后用拓扑排序和一道例题讲解了应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值