图上的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主正月点灯笼

posted @ 2020-09-26 13:27  剪剪  阅读(216)  评论(0编辑  收藏  举报