9.2 三色标记DFS

子节点不剪枝模板代码

  图的深度搜索(DFS)与BFS一样,也是三色标记,基本代码相似,只不过是把FIFO先进先出队列换成FILO先进后出栈。这与树的BFS/DFS是一样的,树也是BFS使用队列,DFS使用栈。阅读本文需要有树的BFS与DFS作为基础,这里我提供我写的树的BFS/DFS博文的链接
  但是既然用了三色,就有不一样的地方。三色的巧妙使用可以实现对递归的替换。我举个图的递归最通用的代码:

    def dfs_vertex(self, node, visited):
        if visited[node]:
        	self.repeat(node)
            return
        visited[node] = True

        self.code1(node)
        for neighbour in self.__edges[node]:
            self.code2(node, neighbour)
            self.dfs_vertex(neighbour, visited)
            self.code3(node, neighbour)
        self.code4(node)

    def repeat(self, node):
        print("重复: ", self.__vertices[node])

    def code1(self, node):
        print("code1: ", self.__vertices[node])

    def code2(self, node, neighbour):
        print("code2: ", self.__vertices[node], self.__vertices[neighbour])

    def code3(self, node, neighbour):
        print("code3: ", self.__vertices[node], self.__vertices[neighbour])

    def code4(self, node):
        print("code4: ", self.__vertices[node])

  上面的递归有四处代码,如何替换为非递归代码,还能保持执行结果正确?以下图为例子:
在这里插入图片描述
  执行结果如下:

code1:  A
code2:  A B
code1:  B
code2:  B C
code1:  C
code2:  C D
code1:  D
code4:  D
code3:  C D
code4:  C
code3:  B C
code2:  B D
重复:  D
code3:  B D
code4:  B
code3:  A B
code4:  A
重复:  B
重复:  C
重复:  D

  可以看出由于A是根,所以A在头尾出现。如果我们要用栈替换这个递归。需要注意以下几点:
  1. code3和code4是两个参数的,所以栈的节点是一个数据对
  2. B和C都指向D,说明栈要支持重复放入,这时候三色的含义变了。白色还是未访问,但是灰色不是代表已经入栈,而是代表所有子节点已经入栈,黑色代表已经出栈。
  3. 每个节点不是访问完马上出栈,而是所有子节点都访问完才出栈
  4. 哪怕重复访问,code2和code3也要执行。
  所以python代码如下:

    def dfs_tricolor(self):
        # 着色
        colors = [UnweightedGraph.WHITE for _ in self.__vertices]

        # d代表discover f代表finish,发现时间戳和完成时间戳
        n = len(self.__vertices)
        for root in range(n):
            stack = [[None, root]]
            while len(stack) > 0:
                v = stack[-1]
                parent = v[0]
                this = v[1]
                if colors[this] == UnweightedGraph.GRAY:
                    stack.pop()
                    self.code4(this)
                    if parent is not None:
                        self.code3(parent, this)
                    colors[this] = UnweightedGraph.BLACK
                elif colors[this] == UnweightedGraph.BLACK:
                    if parent is not None:
                        self.code2(parent, this)
                    self.repeat(this)
                    if parent is not None:
                        self.code3(parent, this)
                    stack.pop()
                else:
                    if parent is not None:
                        self.code2(parent, this)
                    self.code1(this)
                    for neighbor in reversed(self.__edges[this]):
                        stack.append([this, neighbor])
                    colors[this] = UnweightedGraph.GRAY

  无递归代码测试结果:

code1:  A
code2:  A B
code1:  B
code2:  B C
code1:  C
code2:  C D
code1:  D
code4:  D
code3:  C D
code4:  C
code3:  B C
code2:  B D
重复:  D
code3:  B D
code4:  B
code3:  A B
code4:  A
重复:  B
重复:  C
重复:  D

  与递归代码结果一模一样,完美复刻。

子节点剪枝模板代码

  剪枝,就是访问邻居(或者子节点)时,判断一下。如下列python代码:

    def dfs_cut(self):
        visited = [False for _ in self.__vertices]
        for root, _ in enumerate(self.__vertices):
            self.dfs_cut_vertex(root, visited)

    def dfs_cut_vertex(self, node, visited):
        if visited[node]:
            self.repeat(node)
            return
        visited[node] = True

        self.code1(node)
        for neighbour in self.__edges[node]:
            if not visited[neighbour]:
                self.code2(node, neighbour)
                self.dfs_cut_vertex(neighbour, visited)
                self.code3(node, neighbour)
        self.code4(node)

  以上面的例子运行结果就是:

code1:  A
code2:  A B
code1:  B
code2:  B C
code1:  C
code2:  C D
code1:  D
code4:  D
code3:  C D
code4:  C
code3:  B C
code4:  B
code3:  A B
code4:  A
重复:  B
重复:  C
重复:  D

  减少了三行输出,也就是 B → D B\to D BD的路径被剪了。这种场景也可以使用三色标记。

code1:  A
code2:  A B
code1:  B
code2:  B C
code1:  C
code2:  C D
code1:  D
code4:  D
code3:  C D
code4:  C
code3:  B C
code2:  B D
重复:  D
code3:  B D
code4:  B
code3:  A B
code4:  A
重复:  B
重复:  C
重复:  D

  修改的时候,千万不要以为是在入栈时剪枝,我试了这样做,没有作用。其实是在判断重复的代码段那里,判断如果没有parent,就什么都不做,这样才能剪去递归的前后代码。Python代码如下:

    def dfs_tricolor_cut(self):
        # 着色
        colors = [UnweightedGraph.WHITE for _ in self.__vertices]

        # d代表discover f代表finish,发现时间戳和完成时间戳
        n = len(self.__vertices)
        for root in range(n):
            stack = [[None, root]]
            while len(stack) > 0:
                v = stack[-1]
                parent = v[0]
                this = v[1]
                if colors[this] == UnweightedGraph.GRAY:
                    stack.pop()
                    self.code4(this)
                    if parent is not None:
                        self.code3(parent, this)
                    colors[this] = UnweightedGraph.BLACK
                elif colors[this] == UnweightedGraph.BLACK:
                    if parent is None:
                        self.repeat(this)
                    stack.pop()
                else:
                    if parent is not None:
                        self.code2(parent, this)
                    self.code1(this)
                    for neighbor in reversed(self.__edges[this]):
                        stack.append([this, neighbor])
                    colors[this] = UnweightedGraph.GRAY

DFS时间戳

  在实际应用中,需要计算出各个节点的发现时间戳与完成时间戳,也就是在上述递归模板中的code1和code4部分分别记录时间戳。把模板替换下就完成了:

    def dfs_tricolor_time(self):
        # 着色
        colors = [UnweightedGraph.WHITE for _ in self.__vertices]

        # d代表discover f代表finish,发现时间戳和完成时间戳
        time = 1
        times = [{"d": 0, "f": 0} for _ in self.__vertices]
        n = len(self.__vertices)
        for root in range(n):
            stack = [[None, root]]
            while len(stack) > 0:
                v = stack[-1]
                parent = v[0]
                this = v[1]
                if colors[this] == UnweightedGraph.GRAY:
                    stack.pop()
                    times[this]['f'] = time
                    time += 1
                    colors[this] = UnweightedGraph.BLACK
                elif colors[this] == UnweightedGraph.BLACK:
                    stack.pop()
                else:
                    times[this]['d'] = time
                    time += 1
                    for neighbor in reversed(self.__edges[this]):
                        stack.append([this, neighbor])
                    colors[this] = UnweightedGraph.GRAY

        return times

  用上面的图测试,测试结果完全正确:

[{'d': 1, 'f': 8}, {'d': 2, 'f': 7}, {'d': 3, 'f': 6}, {'d': 4, 'f': 5}]

完整代码

# 无权图,主要有两个属性,因为图不太可能完全一一映射,所以,用稀疏矩阵
class UnweightedGraph:
    # 定义几个静态的常量
    WHITE = 0
    GRAY = 1
    BLACK = 2

    def __init__(self):
        self.__vertices = []
        self.__edges = []

    @property
    def vertices(self):
        return self.__vertices

    @vertices.setter
    def vertices(self, value):
        self.__vertices = value

    @property
    def edges(self):
        return self.__edges

    @edges.setter
    def edges(self, value):
        self.__edges = value

    def dfs(self):
        visited = [False for _ in self.__vertices]
        for root, _ in enumerate(self.__vertices):
            self.dfs_vertex(root, visited)

    def dfs_vertex(self, node, visited):
        if visited[node]:
            self.repeat(node)
            return
        visited[node] = True

        self.code1(node)
        for neighbour in self.__edges[node]:
            self.code2(node, neighbour)
            self.dfs_vertex(neighbour, visited)
            self.code3(node, neighbour)
        self.code4(node)

    def repeat(self, node):
        print("重复: ", self.__vertices[node])

    def code1(self, node):
        print("code1: ", self.__vertices[node])

    def code2(self, node, neighbour):
        print("code2: ", self.__vertices[node], self.__vertices[neighbour])

    def code3(self, node, neighbour):
        print("code3: ", self.__vertices[node], self.__vertices[neighbour])

    def code4(self, node):
        print("code4: ", self.__vertices[node])

    def dfs_tricolor(self):
        # 着色
        colors = [UnweightedGraph.WHITE for _ in self.__vertices]

        # d代表discover f代表finish,发现时间戳和完成时间戳
        n = len(self.__vertices)
        for root in range(n):
            stack = [[None, root]]
            while len(stack) > 0:
                v = stack[-1]
                parent = v[0]
                this = v[1]
                if colors[this] == UnweightedGraph.GRAY:
                    stack.pop()
                    self.code4(this)
                    if parent is not None:
                        self.code3(parent, this)
                    colors[this] = UnweightedGraph.BLACK
                elif colors[this] == UnweightedGraph.BLACK:
                    if parent is not None:
                        self.code2(parent, this)
                    self.repeat(this)
                    if parent is not None:
                        self.code3(parent, this)
                    stack.pop()
                else:
                    if parent is not None:
                        self.code2(parent, this)
                    self.code1(this)
                    for neighbor in reversed(self.__edges[this]):
                        stack.append([this, neighbor])
                    colors[this] = UnweightedGraph.GRAY

    def to_dot_string(self):
        str = 'digraph{\n'
        for v in self.__vertices:
            str += f'{v};\n'
        for i in range(0, len(self.__edges)):
            edges = self.__edges[i]
            for e in edges:
                str += f'{self.__vertices[i]}->{self.__vertices[e]};\n'
        str += '}\n'
        return str

    def dfs_cut(self):
        visited = [False for _ in self.__vertices]
        for root, _ in enumerate(self.__vertices):
            self.dfs_cut_vertex(root, visited)

    def dfs_cut_vertex(self, node, visited):
        if visited[node]:
            self.repeat(node)
            return
        visited[node] = True

        self.code1(node)
        for neighbour in self.__edges[node]:
            if not visited[neighbour]:
                self.code2(node, neighbour)
                self.dfs_cut_vertex(neighbour, visited)
                self.code3(node, neighbour)
        self.code4(node)

    def dfs_tricolor_cut(self):
        # 着色
        colors = [UnweightedGraph.WHITE for _ in self.__vertices]

        # d代表discover f代表finish,发现时间戳和完成时间戳
        n = len(self.__vertices)
        for root in range(n):
            stack = [[None, root]]
            while len(stack) > 0:
                v = stack[-1]
                parent = v[0]
                this = v[1]
                if colors[this] == UnweightedGraph.GRAY:
                    stack.pop()
                    self.code4(this)
                    if parent is not None:
                        self.code3(parent, this)
                    colors[this] = UnweightedGraph.BLACK
                elif colors[this] == UnweightedGraph.BLACK:
                    if parent is None:
                        self.repeat(this)
                    stack.pop()
                else:
                    if parent is not None:
                        self.code2(parent, this)
                    self.code1(this)
                    for neighbor in reversed(self.__edges[this]):
                        stack.append([this, neighbor])
                    colors[this] = UnweightedGraph.GRAY

    def dfs_tricolor_time(self):
        # 着色
        colors = [UnweightedGraph.WHITE for _ in self.__vertices]

        # d代表discover f代表finish,发现时间戳和完成时间戳
        time = 1
        times = [{"d": 0, "f": 0} for _ in self.__vertices]
        n = len(self.__vertices)
        for root in range(n):
            stack = [[None, root]]
            while len(stack) > 0:
                v = stack[-1]
                parent = v[0]
                this = v[1]
                if colors[this] == UnweightedGraph.GRAY:
                    stack.pop()
                    times[this]['f'] = time
                    time += 1
                    colors[this] = UnweightedGraph.BLACK
                elif colors[this] == UnweightedGraph.BLACK:
                    stack.pop()
                else:
                    times[this]['d'] = time
                    time += 1
                    for neighbor in reversed(self.__edges[this]):
                        stack.append([this, neighbor])
                    colors[this] = UnweightedGraph.GRAY

        return times

完整测试代码

import unittest
from com.youngthing.graph.graph_dfs import UnweightedGraph


class MyTestCase(unittest.TestCase):

    def create_dag(self):
        vertices = ['A', 'B', 'C', 'D', ]
        edges = [
            [1, ],  # 0
            [2, 3],  # 1
            [3, ],  # 2
            [],  # 3

        ]
        graph = UnweightedGraph()
        graph.vertices = vertices
        graph.edges = edges
        return graph

    def test_dfs(self):
        """
        输出结果
        """
        # 定义一个图,测试BFS搜索
        graph = self.create_dag()
        print(graph.to_dot_string())
        graph.dfs()
        print("三色标记")
        graph.dfs_tricolor()

    def test_dfs_cut(self):
        """
        输出结果
        """
        # 定义一个图,测试BFS搜索
        graph = self.create_dag()
        print(graph.to_dot_string())
        graph.dfs_cut()
        print("三色标记")
        graph.dfs_tricolor_cut()

    def test_dfs_cut(self):
        """
        输出结果
        """
        # 定义一个图,测试BFS搜索
        graph = self.create_dag()
        print(graph.dfs_tricolor_time())


if __name__ == '__main__':
    unittest.main()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒过来摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值