BFS,DFS带图详解+蓝桥杯算法题+经典例题

1.BFS和DFS的定义与实现方式

1.1 深度优先搜索(DFS)

基本概念:DFS 是一种用于遍历或搜索图或树的算法。它从起始节点开始,沿着一条路径尽可能深地探索下去,直到无法继续或者达到目标节点,然后回溯到上一个节点,继续探索其他路径。

实现方式

通常使用递归或者栈(Stack)来实现。在递归实现中,函数不断调用自身来深入探索下一个节点;在使用栈的实现中,将节点压入栈中,每次从栈顶取出节点进行处理,并将其未访问的邻接节点压入栈中。

例如,对于一个图结构 

graph = {"A": ["B", "C"], 
        "B": ["D"], 
        "C": ["E"], 
        "D": [], 
        "E": []}

从节点 "A" 开始 DFS 搜索,首先访问 "A",然后选择 "B" 进行深入探索,访问 "B" 后再访问 "D",当 "D" 没有未访问的邻接节点时,回溯到 "B",由于 "B" 的邻接节点都已访问,再回溯到 "A",接着选择 "C" 进行深入探索,以此类推。

1.2 广度优先搜索(BFS)

基本概念:BFS 也是一种用于遍历或搜索图或树的算法。它从起始节点开始,首先访问起始节点的所有邻接节点,然后再依次访问这些邻接节点的邻接节点,以此类推,一层一层地向外扩展,直到找到目标节点或者遍历完整个图或树。

实现方式

通常使用队列(Queue)来实现。将起始节点放入队列中,每次从队列头部取出一个节点进行处理,并将其未访问的邻接节点依次放入队列尾部。

例如,对于图结构 

graph = {"A": ["B", "C"], 
        "B": ["D"], 
        "C": ["E"], 
        "D": [], 
        "E": []}

从节点 "A" 开始 BFS 搜索,首先将 "A" 放入队列,然后取出 "A" 并访问它,将 "A" 的邻接节点 "B" 和 "C" 放入队列;接着从队列中取出 "B" 并访问它,将 "B" 的邻接节点 "D" 放入队列;再取出 "C" 并访问它,将 "C" 的邻接节点 "E" 放入队列,依此类推。

 2.基本代码实现

将上图转化为graph邻接表

graph={ "A":["B","C"],
        "B":["A","C","D"],
        "C":["A","B","D","E"],
        "D":["B","C","E","F"],
        "E":["C","D"],
        "F":["D"]
        }

2.1 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 bfs(graph,s):
    stack=[]
    seen=set()
    #建立一个空集合,用于后续检查元素是否已经存在于栈中

    stack.append(s)
    seen.add(s)
    while len(stack)>0:
        key=stack.pop()
        nodes=graph[key]
        for i in nodes:
            if i not in seen:
                stack.append(i)
                seen.add(i)
            # 如果不在集合中,向栈中添加,并在集合中标记

        print(key)
bfs(graph,"A")

# A
# C
# E
# D
# F
# B

区别:queue.append()统一在右侧添加元素,DFS是利用pop()删除最右端的元素,而BFS是利用pop(0) 删除最左端的元素

2.2 BFS 实现对上图的寻找

graph={ "A":["B","C"],
        "B":["A","C","D"],
        "C":["A","B","D","E"],
        "D":["B","C","E","F"],
        "E":["C","D"],
        "F":["D"]
        }
def bfs(graph,s):
    queue=[]
    seen=set()
    queue.append(s)
    seen.add(s)
    while len(queue)>0:
        key=queue.pop(0)
        nodes=graph[key]
        for i in nodes:
            if i not in seen:
                queue.append(i)
                seen.add(i)
                
        print(key)
bfs(graph,"A")
# A
# B
# C
# D
# E
# F

3.BFS和DFS的区别,优缺点

3.1 区别

  • 搜索方式
    • DFS:从起始节点开始,沿着一条路径尽可能深地探索下去,直到无法继续或达到目标节点,然后回溯到上一个未完全探索的节点,继续探索其他路径,就像走迷宫时一直沿着一条路走到死胡同再回头。
    • BFS:从起始节点开始,先访问其所有相邻节点,然后再依次访问这些相邻节点的相邻节点,一层一层地向外扩展,如同水波一样向四周扩散。
  • 数据结构
    • DFS:通常使用递归或栈来实现。递归实现时,系统会自动使用栈来保存函数调用的上下文信息;也可以手动使用栈来存储待访问的节点。
    • BFS:使用队列来实现,先将起始节点放入队列,然后每次从队列中取出一个节点,访问其相邻节点并将未访问过的相邻节点放入队列。
  • 遍历顺序
    • DFS:遍历顺序是深度优先的,会先深入探索某一条路径上的节点,然后再回溯到其他分支。
    • BFS:遍历顺序是广度优先的,按照距离起始节点的远近依次访问节点,先访问距离近的节点,再访问距离远的节点。

3.2 优缺点

  • DFS
    • 优点:实现相对简单,对于某些问题,如寻找图中的连通分量、拓扑排序等,DFS 的递归性质使其代码简洁明了。在一些情况下,如果目标节点位于较深的层次,DFS 可能会更快地找到目标,因为它会优先沿着一条路径深入探索。
    • 缺点:可能会陷入无限循环,特别是在处理有环的图时,需要额外的机制来标记已访问的节点以避免重复访问。而且它不一定能找到最短路径,因为它是深度优先探索,可能会先找到一条较长的路径到达目标节点。
  • BFS
    • 优点:一定能找到从起始节点到目标节点的最短路径(如果存在),因为它是按照层次依次访问节点的。在处理无权图或边权都相等的图时,BFS 是寻找最短路径的理想算法。
    • 缺点:需要更多的空间来存储队列中的节点,特别是在图的规模较大时,可能会占用大量的内存。而且对于某些问题,其实现可能相对复杂一些,因为需要维护队列和处理节点的访问顺序。

4.DFS经典例题

4.1 矩阵最长递减路径问题

题意:计算每一个格子,由大递减到小所占的格子数

#矩阵最长递减路径
lis=[[1,1,3],
     [2,3,4],
     [1,0,5]]
m,n=3,3
def dfs(i,j,val):
     if i<0 or i==m:    #行出界
          return 0
     if j<0 or j==n:    #列出界
          return 0
     if lis[i][j]>=val: #递增,不符合题意
          return 0
     if (i,j) in dp:
          return dp[(i,j)]
     res=1
     res=max(res,1+dfs(i-1,j,lis[i][j]))  #向上
     res=max(res,1+dfs(i+1,j,lis[i][j]))  #向下
     res=max(res,1+dfs(i,j-1,lis[i][j]))  #向左
     res=max(res,1+dfs(i,j+1,lis[i][j]))  #向右
     dp[(i,j)]=res   #以键值对的形式保存到集合中
     return res
dp={}
for i in range(m):
     for j in range(n):
          res=dfs(i,j,100)
          print(res,end=' ')
     print( )
print(dp)
print(f'最短路径格子数为{max(dp.values())}')
#将取出全部键值对中的值,并输出最大值
 
#输出结果
1 1 2 
3 4 5 
2 1 6 
{(0, 0): 1, (0, 1): 1, (0, 2): 2, (2, 1): 1,
 (2, 0): 2, (1, 0): 3, (1, 1): 4, (1, 2): 5, (2, 2): 6}
最长路径格子数为6

 下图为从递减路径最长的一条,即从右下角的单元格出发的遍历流程图。黑色笔标注的数字代表单元格内的数字,红色笔标注的数字为走过的格子数(画图潦草,请各位读者见谅)

4.2 海域污染问题

N,M=list(map(int,input().split(',')))
lis=[list(map(int,input()))for _ in range(N)]
def dfs(x,y):
        if x<0 or x==N:  # 行出界
                return 0
        if y<0 or y==M:  # 列出界
                return 0
        if lis[x][y]==0: # 坐标值为0
                return 0
        lis[x][y]=0    #将遍历到的不是0的数都变成0
        dfs(x-1,y)     # 向上
        dfs(x+1,y)     # 向下
        dfs(x,y-1)     # 向左
        dfs(x,y+1)     # 向右
count=0
for i in range(N):
        for j in range(M):
                if lis[i][j]==1:
                        dfs(i,j)
                        count+=1
print(count)

输入

4,5
11001
10001
00100
01100

 输出

3

4.3 最大的油田问题

lis=[[1,1,0,0,1],
     [0,0,0,1,0],
     [0,0,0,1,1],
     [1,1,0,1,1]]
hang=len(lis)
lie=len(lis[0])
ans=0
def dfs(x,y):
    if x>=0 and x<hang and y>=0 and y<lie and lis[x][y]==1:
        lis[x][y]=0
        return 1+dfs(x-1,y)+dfs(x+1,y)+dfs(x,y-1)+dfs(x,y+1)
    else:
        return 0
for i in range(hang):
    for j in range(lie):
        area=dfs(i,j)
        if area>ans:
            ans=area
print(ans)
# 5

4.4 蓝桥杯模拟题--全球变暖 

 

 解题思路:

  1. 新建一个全为0的二维列表,用来标记是否走过,新建一个栈,放入初始坐标
  2. 若想要岛屿不被淹没,一个“#”四周都必须是“#”,索引上下左右四个位置来判断
  3. 判断一个“#”的四个方向,若为“#”且未走过,压入栈,标记为已走过
  4. for循环遍历,压入起始坐标(“#”且未走过)
n = int(input())
data = [input() for _ in range(n)]  # 地图数组

flag = [[0] * n for _ in range(n)]  # 标记数组,标记每个点是否访问
dx = [0, 1, 0, -1]
dy = [1, 0, -1, 0]  # 方向数组,方便判断四个方向的岛

def dfs(x, y):  # 使用栈模拟深度优先遍历dfs,减少递归调用的开销
    noHigh = True  # 是否有高地
    stack = [(x, y)]  # 使用列表模拟栈

    while stack:  # 栈不为空,持续弹出栈顶元素
        t = stack.pop()
        tx, ty = t[0], t[1]
        if data[tx][ty - 1] == "#" and data[tx][ty + 1] == "#" and data[tx - 1][ty] == "#" and data[tx + 1][ty] == "#":
            noHigh = False  # 该岛屿有高地

        for i in range(4):  # 判断该岛的上下左右四个方向是否有陆地
            nx, ny = tx + dx[i], ty + dy[i]
            if data[nx][ny] == "#" and flag[nx][ny] == 0:  # 是陆地且未被访问,压入栈,并标记访问
                flag[nx][ny] = 1
                stack.append((nx, ny))
    return noHigh


ans = 0  # 记录被淹没的岛屿数量
for i in range(n):  
    for j in range(n):
        if data[i][j] == "#" and flag[i][j] == 0:  # 陆地且没有遍历
            if dfs(i, j):  # 没有高地,被淹没,答案加一
                ans += 1

print(ans)

5.BFS经典例题

5.1 蓝桥杯算法模板题--走迷宫

from collections import deque
N, M = 5, 5
a = [
    [1, 0, 1, 1, 0]
    [1, 1, 0, 1, 1],
    [0, 1, 0, 1, 1],
    [1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1]
]
n, m, c, d = 1, 1, 5, 5
# 因为矩阵下标从 0 开始,将目标坐标转换为 0 索引
c -= 1
d -= 1
n-=1
m-=1

dx = [0, 1, 0, -1]  # 可以在行方向上移动的四个方向:右、下、左、上
dy = [1, 0, -1, 0]  # 可以在列方向上移动的四个方向:右、下、左、上

# 标记矩阵,用于记录节点是否已经访问过
visited = [[False] * M for _ in range(N)]

def bfs(x, y):
    # 创建队列,队列元素为 (x, y, steps)
    queue = deque([(x, y, 0)])
    visited[x][y] = True

    while queue:
        cur_x, cur_y, steps = queue.popleft()
        # 到达目标点
        if cur_x == c and cur_y == d:
            return steps

        for i in range(4):  # 在四个方向上尝试移动
            new_x = cur_x  + dx[i]
            new_y = cur_y  + dy[i]

            # 检查新坐标是否合法且未访问过
            if 0 <= new_x < N and 0 <= new_y < M and a[new_x][new_y] == 1 and not visited[new_x][new_y]:
                visited[new_x][new_y] = True
                queue.append((new_x, new_y, steps + 1))
    return -1 # 如果不满足上述情况返回-1

print(bfs(n,m))


标准提交代码:

from collections import deque

N,M=list(map(int,input().split()))
a=[list(map(int,input().split())) for _ in range(N)]
n,m,c,d=list(map(int,input().split()))
c -= 1
d -= 1
dx = [0, 1, 0, -1]  
dy = [1, 0, -1, 0]  

visited = [[False] * M for _ in range(N)]
def bfs(x, y):
    queue = deque([(x, y, 0)])
    visited[x][y] = True
    while queue:
        cur_x, cur_y, steps = queue.popleft()        
        if cur_x == c and cur_y == d:
            return steps
        for i in range(4):  
            new_x = cur_x  + dx[i]
            new_y = cur_y  + dy[i]          
            if 0 <= new_x < N and 0 <= new_y < M and a[new_x][new_y] == 1 and not visited[new_x][new_y]:
                visited[new_x][new_y] = True
                queue.append((new_x, new_y, steps + 1))
    return -1
print(bfs(n-1,m-1))


5.2 艰难旅程

from collections import deque

def bfs(val,st_row,st_col):
    # val为二维列表,st_row,st_col为起始坐标

    row=len(val)
    col=len(val[0])
    arrived=[[False for j in range(row)] for i in range(col)]
    # 二维列表存储地图上的每个方格是否到达过,初始标记为Flase

    moverow=[0,1,0,-1]
    movecol=[1,0,-1,0]
    # 定义四个方向,以实现向上下左右移动

    ans=1
    queue=deque([(st_row,st_col)])
    arrived[st_row-1][st_col-1]=True  # 因为python是0索引,坐标值减1
    # 将起始坐标放入队列中,记录此点已到达,标记为True

    while queue:
        # queue不为空的时候
        x,y=queue.popleft()
        # 从左端删除并返回位置坐标

        for i in range(4):
            newrow=x+moverow[i]
            newcol=y+movecol[i]
            # 向四个方向移动

            if newrow>row or newrow<=0 or newcol>col or newcol<=0:
                continue
                # 出界

            if not arrived[newrow-1][newcol-1] and val[newrow-1][newcol-1]!=val[x-1][y-1]:
                # 满足条件,未被标记,单元格内值不同

                    queue.append((newrow,newcol))
                    arrived[newrow-1][newcol-1]=True
                # 添加至队列并标记已经走过

                    ans+=1
    return ans
val= [
    [1, 0, 1, 1],
    [1, 0, 1, 0],
    [0, 1, 0, 1],
    [0, 0, 1, 1],
]
result = bfs(val,3,3)
print(result)

# 11

5.3 蓝桥杯模拟题--长草

解题思路:

  1. 新建一个队列,for循环遍历整个数组,将初始为“g”的坐标添加到队列中
  2. 队列中的初始坐标个数决定出队列的次数
  3. 每执行一次函数,实现地图中所有“g”向四方向长草,并把四个方向的坐标压入队列,以便于进行下次函数调用
  4. 利用for循环实现执行k次函数(k个月)
from collections import deque
n,m=list(map(int,input().split()))
a=[list(map(str,input())) for _ in range(n)]
num=int(input())

dx=[0,1,0,-1]
dy=[1,0,-1,0]
q=deque([])
for i in  range(n):
    for j in range(m):
        if a[i][j]=='g' :
            q.append((i,j))

def f():
    s=len(q)
    while s>0:
        cur_x,cur_y=q.popleft()
        for i in range(4):
            new_x=cur_x+dx[i]
            new_y=cur_y+dy[i]
            if 0<=new_x<n and 0<=new_y<m and a[new_x][new_y]==".":
                a[new_x][new_y]="g"
                q.append((new_x,new_y))
        s-=1

for _ in range(num):
    f()
for x in a:
    print(''.join(x))
流程解释:
a=[ ['.', 'g', '.', '.', '.'],
    ['.', '.', '.', '.', '.'],
    ['.', '.', 'g', '.', '.'],
    ['.', '.', '.', '.', '.']]
初始压入的位置坐标deque([(0, 1), (2, 2)])

执行一次函数后,初始两个坐标移除队列,对应的四个方向上的坐标进入队列
此时地图与队列变为
a=[ ['g', 'g', 'g', '.', '.'], 
    ['.', 'g', 'g', '.', '.'], 
    ['.', 'g', 'g', 'g', '.'], 
    ['.', '.', 'g', '.', '.'] ]
deque([(0, 2), (1, 1), (0, 0), (2, 3), (3, 2), (2, 1), (1, 2)])

再执行一次函数
队列中的坐标依次弹出,对应四个方向的位置变为“g”
a=[ ['g', 'g', 'g', 'g', '.'], 
    ['g', 'g', 'g', 'g', '.'], 
    ['g', 'g', 'g', 'g', 'g'], 
    ['.', 'g', 'g', 'g', '.'] ]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值