• 图上的BFS&DFS


    说出来可能不信,这已经是我第三次入门DFS和BFS了,大佬都救不了我的那种,所以我打算自救,这里我会慢慢整理对于DFS和BFS自己的理解,我不想再拿到DFS和BFS的题目之后只能和他大眼瞪小眼,大家见笑了。

    树也是一种无向无环图,所以DFS和BFS对于树的遍历也是适用的。

    如果是树,那么对于树的DFS和DFS很简单,因为起点永远是根节点!

    但是如果对于是别的二维数组或者这样一个结构,得出所有可能的解法或者求解一个答案,我们可能就要考虑对于每一个点的DFS和BFS了!

    看一下树的层序遍历(BFS):

    本质就是使用一个队列,初始化时,我们把根节点入队,队首用来pop()((如果是双端队列的话,那就leftpop()))出当前正在访问的节点,队尾用append()维护当前节点的左右子节点,将左右子节点加入队列:

     

    我们首先来看一下图的上的BFS

    1、图上的BFS(广度优先搜索):

    图和树的区别,就是图并没有根节点,因此我们需要选择一个节点作为我们的起始节点,以下图为例:

    假设我们从A出发(距离指的是图的边的数量)

    第一层为A

    第二层的元素,是和A距离为1的点,B和C

    第三层的元素,是和A距离为2的点,D和E

    第四层的元素,是和A距离为3的点,F

    那我我们可以得到一个顺序:ABCDEF

    如果BC变为了CB,得到的结果还正确吗?

    如果DE变为了ED,得到的结果还正确吗?

    BFS,实际上是通过队列来实现的

    队列初始化时,将A加入队列

    遍历第一层时,将A从队首移出,和A直接相连的BC(邻接点入队)加入到队列中,假设先加入的是B,后加入的是C

    遍历第二层时,此时B处于队首,那么我们也是先将B从队首移出,还要维护和B直接相连的三个点A,C,D,A已经被遍历过了C已经在队列中了,D加入队列是没问题的,那么A和C又要怎么处理呢?

    由此可见我们像树那样,找一条路径,仅仅只是用一个队列来维护我们的遍历是不够的,这里就可以看出,我们还需要一个很重要的东西来决定,我们当前是否需要对一个点进行搜索,我的理解是一个状态

    状态,状态,状态!和使用BFS和DFS解题有很大的关系,对于我来说,暂时还是只可意会不可言传,晚点填坑,万一哪天我突然悟了我就能和你们讲清楚了

    首先这个状态,可能是决定你是否要对当前的访问点进行搜索

    其次,这个状态,可能是用来维护你访问到当前节点所产生的结果,用于得出你的答案

    继续说我们把队首B移出,图上可见,E和B是不直接相连的,先不管我们的ACD要不要入队,如何入队,反正E肯定不会在D之前先入队

    如果把和A相连的B和C的入队顺序变为CB,C出队时,D和E是C直接相连的,此时的DE和ED就没有区别,所以BC顺序相反,不会对后面的访问路径产生影响

    如果维持了BC的顺序,那么就不可能是ED这样一个访问顺序,相应的结果也就是错误的

    队列,是用来保证层的顺序,队列就是这样一种先进先出的数据结构

     代码实现:

    # 构造一个图
    graph = {
        'A': ['B', 'C'],
        'B': ['A', 'C', 'D'],
        'C': ['A', 'B', 'D', 'E'],
        'D': ['B', 'C', 'E', 'F'],
        'E': ['C', 'D'],
        'F': ['D'],
    }
    
    # # 图上有多少个节点
    # print(graph.keys())
    # # 如何访问一个节点的相邻节点
    # print(graph['E'])
    
    def BFS(graph, s):
        """
        用一个队列 queue = [] 存放所有需要被访问的节点,是一个动态数组
        # queue.append(), 加入队尾
        # queue.pop(0), 从队首移出
    
        :param graph:
        :param s:
        :return:
        """
    
        queue = [s]  # 把起始点放入队列
        seen = {s}  # 用来维护当前已经被访问过的节点
        res = []  # 用来存放访问的结果
        while queue:  # 只要队列中还存在节点, 每次循环就从里面拿一个节点
            cur = queue.pop(0)  # cur节点为从队首取出的当前节点
            nodes = graph[cur]  # 获取cur节点的邻接点
            for node in nodes:  # 对于所有的邻接点
                if node not in seen:  # 当这个节点没有被访问过
                    queue.append(node)  # 就把这个节点入队
                    seen.add(node)  # 维持这个节点已经被访问
    
            res.append(cur)
        return res
    
    print(BFS(graph, 'A'))

    2、图上的DFS(深度优先搜索)

    假设我们从A出发,DFS会随便找一条路(前提是有相邻路径的),一直走到底,走到不能走为止,在往回走

    从A出发,有两条路可以走,一条是B,一条是C

    走B,在B上往前走,可以走的路由A,C,D,A已经走过了,C和D中选一条

    走D,在点D有B、C、E、F可以选择,B已经走过了,C、E、F中选一条,

    走F,在点F,发现已经没有路可以走了,那么就只能往回在跳到上一个走的点D

    回到点D,还是一样的选择,但是F走过了,B走过了,那没有哦走过点点就是C和E,那就在C和E之间选一条

    走E,在点E有C、D可以选择,D已经走过了,那就只剩下C没有走过

    走C

    所以DFS真正的遍历顺序为:ABDF(D)EC(EDBA),括号中表示回退的点

    DFS和递归一样都是栈结构,但是两者有很大的区别,递归是实现DFS的一个工具

    DFS是通过栈来实现的

    # 构造一个图
    graph = {
        'A': ['B', 'C'],
        'B': ['A', 'C', 'D'],
        'C': ['A', 'B', 'D', 'E'],
        'D': ['B', 'C', 'E', 'F'],
        'E': ['C', 'D'],
        'F': ['D'],
    }
    
    def DFS(graph, s):
        """
        使用一个栈 stack 来模拟DFS对于每个节点访问
        stack.pop() 从栈顶移除元素
        stack.append() 把元素入栈(栈顶)
    
        :param graph:
        :param s:
        :return:
        """
    
        stack = [s]
        seen = {s}
        res = []
    
        while stack:
            cur = stack.pop()
            res.append(cur)
            nodes = graph[cur]
            for node in nodes:
                if node not in seen:
                    stack.append(node)
                    seen.add(node)
    
        return res
    
    print(DFS(graph, 'A'))

    3、一点点扩展:

    在遍历每个节点时,可以构造一个映射关系,表示这个点的前一个节点(parent)是谁,用来得出一个点到另一点的最短路径

    # 构造一个图
    graph = {
        'A': ['B', 'C'],
        'B': ['A', 'C', 'D'],
        'C': ['A', 'B', 'D', 'E'],
        'D': ['B', 'C', 'E', 'F'],
        'E': ['C', 'D'],
        'F': ['D'],
    }
    
    # # 图上有多少个节点
    # print(graph.keys())
    # # 如何访问一个节点的相邻节点
    # print(graph['E'])
    
    def BFS(graph, s):
        """
        用一个队列 queue = [] 存放所有需要被访问的节点,是一个动态数组
        # queue.append(), 加入队尾
        # queue.pop(0), 从队首移出
    
        :param graph:
        :param s:
        :return:
        """
        res = []  # 用来存放访问的结果
    
        queue = [s]  # 把起始点放入队列
        seen = {s}  # 用来维护当前已经被访问过的节点
    
        parent = {s: None, }  # 构造当前访问的节点的上一个节点是谁(parent)
    
        while queue:  # 只要队列中还存在节点, 每次循环就从里面拿一个节点
            cur = queue.pop(0)  # cur节点为从队首取出的当前节点
            nodes = graph[cur]  # 获取cur节点的邻接点
            for node in nodes:  # 对于所有的邻接点
                if node not in seen:  # 当这个节点没有被访问过
                    queue.append(node)  # 就把这个节点入队
                    seen.add(node)  # 维持这个节点已经被访问
    
                    parent[node] = cur  # 当前节点, 是它所有邻接点的前一个节点
    
            res.append(cur)
        return parent
    
    # print(BFS(graph, 'A'))
    # for key, value in BFS(graph, 'A').items():
    #     print(key, value)
    
    node = 'B'
    while node:
        print(node)
        node = BFS(graph, 'A')[node]

    4、再再再来亿点点的扩展!!!

    一个BFS的应用:Dijkstra算法

    同样也是在图上求一个最短的距离,不过现在每条边都会有一个权值,代表着距离。

    BFS本身还是通过队列来实现的,不过这里是使用了一种特殊的数据结构,堆(heap),也是最小堆,最小堆是一个什么样的东西,请移步我的另外另外一片文章,相信我已经很努力的再把它讲清楚了,一点浅薄的认知和使用,多包涵。

    假设现在我们从A开始入队(A, 0),并且出队,他的领节点要进行入队,现在入队时同样也要带上一个权值(priority),这个priority就是与初始节点的距离

    现在入队的两个领节点分别是(B, 5),(C, 1),这是,基于两个点所带的权值,最小堆会维护自身的性质,(C, 1)会自动调整到(B, 5)的前面

    需要记住的一点是,这个priority,始终维持的是从点A出发到当前节点的距离

    所以如果到D,那么入队的就是(D, 5),而不是(D, 4)

    还有与普通队列非常非常有区别的一点,C的邻接点是A,B,E,在我们的普通队列中,访问到C之后,B已经存在与队列中了,那就不需要再对B做任何处理,但是,在这里不同,我们需要重新确认B通过当前出队的点C,到达A的距离,是不是比直接从A到达的那个priority要小

    A -> C -> B:3

    A-> B : 5

    从图上的权值可知,我们对于B的的状态也是要重新更新的,并且还需要维护,到达B走过的前一个节点,不再是A,而变成了C

    对于已经在队列中的(B, 5),不需要做任何改变,而是在队尾再入队一个(B, 3),根据最小堆的性质,这个新入队的(B, 3),会自动调整到队首

    有资格出队的点(A和C)才是真正走过的点,而队列中的点,都是待处理的点

    现在当B出队,那么和B直接相连的点A, C, D,其中A和C已经被访问过,就只剩下D还要被处理,那么(D, 4)入队,由于权值最小,自动调整到队首,准备下一个出队,并且出队时更新D的前一个parent为B

     再次出队,入队,并且进行调整

    接下来出队的是B和D,但是B和D已经被访问过了,因此可以直接丢弃,然后到E出队

    最后到F

    最后队列为空时,最短路径也就确定了,去parent这个数据结构中去找

    所以完成的访问结果如下:

    所以A到F的最短路径为:

    ACBDF

     

    先来看一下堆

    import heapq
    pqueue = []
    heapq.heappush(pqueue, (1, 'A'))
    heapq.heappush(pqueue, (7, 'B'))
    heapq.heappush(pqueue, (3, 'C'))
    heapq.heappush(pqueue, (6, 'D'))
    heapq.heappush(pqueue, (2, 'E'))
    
    print(heapq.heappop(pqueue))
    print(heapq.heappop(pqueue))
    print(heapq.heappop(pqueue))

    再来写一下代码,这里需要处理很多数据结构,并且数据的处理,队列的维护,都要做亿些些改变,并且,路径长度在进行初始化时,要设置为正无穷

    这里的状态有[节点没有被访问过], [节点的有最短的路径]两个状态需要维护,并且这两个状态也是邻节点入队是需要维护的

    import heapq   # 因为要用到堆结构,所以需要import heapq
    
    # 修改一下图的结构, 增加priority
    graph = {
        'A': {'B': 5, 'C': 1},
        'B': {'A': 5, 'C': 2, 'D': 1},
        'C': {'A': 1, 'B': 2, 'D': 4, 'E': 8},
        'D': {'B': 1, 'C': 4, 'E': 3, 'F': 6},
        'E': {'C': 8, 'D': 3},
        'F': {'D': 6},
    }
    
    # # 查看一下'A'和'B'的距离
    # print(graph['A']['B'])
    
    # # 仅仅拿到某一个节点的邻节点,不包括距离
    # print(graph['A'].keys())
    
    # 当A的邻节点C需要入栈时,会发现在原先distance中没有可以比较的值,因此需要对distance中所有的值进行初始化
    
    def init_distance(graph, s):
        distance = {s: 0}
        for node in graph:
            if node != s:
                distance[node] = 1 << 60
        return distance
    
    
    def dijkstra(graph, s):
        """
        使用最小堆来维持遍历每个点访问的先后顺序
        import heapq
        hqueue = []
        heapq.heappush(hqueue, (0, s))
        :param graph:
        :param s:
        :return:
        """
    
        hqueue = []
        heapq.heappush(hqueue, (0, s))  # 准备一个堆用来遍历
    
        seen = set()  # 当一个点出队时(在最小堆调整之后), 才可以认为被访问过
    
        parent = {s: None}  # 同样,在出队之后,才会更新邻节点的上一个节点
    
        distance = init_distance(graph, s)  # 始终维护被访问过的节点到其它节点的最短距离, 需要进行初始化
    
        while hqueue:
            pair = heapq.heappop(hqueue)  # 此时拿出来的是一个元组,再去构造访问的点和距离
    
            dis = pair[0]  # 获取当前访问点的距离
            cur = pair[1]  # 获取当点被访问的节点
    
            seen.add(cur)  # 当有一个节点出队时,才认为被访问过,所以放放在这里进行初始化
    
            nodes = graph[cur].keys()  # 获取当前节点的邻节点
            for node in nodes:
                # 如果当前节点没有被访问过,要做维护的状态有点多
                # 首先如果从刚出队的节点的邻节点到当前节点的距离+dis(已知到当前节点的距离)的距离之和要比已知的距离短
                # 那么需要更新, parent, distance
                if node not in seen:
                    if dis + graph[cur][node] < distance[node]:  # 如果
                        heapq.heappush(hqueue, (dis + graph[cur][node], node))  # 维护好距离之后,进行入队操作
                        parent[node] = cur  # 根据最小的距离更新邻节点的上一个节点
                        distance[node] = dis + graph[cur][node]  # 更新最短距离
    
        return parent, distance
    
    if __name__ == "__main__":
        parent, distance = dijkstra(graph, 'A')
        print(parent)
        print(distance)

    本文参考:B站UP主正月点灯笼

  • 相关阅读:
    Django使用manage.py test错误解决
    Notepad++的find result窗口恢复
    qrcode 配套 PIL 或者 Image + ImageDraw
    pymssql.OperationalError: (20017 问题解决
    ConfigParser使用:1.获取所有section为list,2.指定section具体值,并转换为dict
    selenium&Firefox不兼容问题:Message: Unable to find a matching set of capabilitie;Can't load the profile. Profile;Message: 'geckodriver' executable needs to be in PATH
    使用宏实现透视表部分功能,将AB列数据合并统计.
    反射
    类的多态
    封装
  • 原文地址:https://www.cnblogs.com/canaan233/p/13734760.html
Copyright © 2020-2023  润新知