本博客就蓝桥杯中所涉及的搜索算法基础问题进行讲解,包括:深度优先搜索(DFS)、广度优先搜索(BFS)和剪枝。每一种搜索算法都在给出定义的同时,也给出了示例代码,以供低年级师弟师妹们学习和练习。
前序知识:
(1)Python基础语法
(2)动态规划(基础)
一、深度优先搜索(DFS)
-
定义:DFS 是一种"不撞南墙不回头"的算法,沿着路径尽可能深入探索,直到无法继续再回溯。
-
原理与步骤:
(1)使用栈结构(递归本质也是栈);
(2)从起点出发选择一个方向深入;
(3)遇到死路时回退到最近的分叉点;
(4)重复直到找到解或遍历所有可能。 -
适用场景:
(1)路径存在性判断;
(2)全排列/组合问题;
(3)连通性问题。 -
代码示例(迷宫路径搜索):
def dfs(maze, start, end):
"""
:param maze: 二维数组表示的迷宫,0可走,1不可走
:param start: 起点坐标 (x,y)
:param end: 终点坐标 (x,y)
:return: 是否存在路径
"""
directions = [(-1,0), (1,0), (0,-1), (0,1)] # 上下左右四个方向
visited = set() # 记录已访问的位置
def _dfs(x, y):
# 递归终止条件:到达终点
if (x, y) == end:
return True
# 标记当前位置已访问
visited.add((x, y))
# 尝试四个方向
for dx, dy in directions:
nx, ny = x + dx, y + dy
# 检查新位置是否合法
if 0 <= nx < len(maze) and 0 <= ny < len(maze[0]) \
and maze[nx][ny] == 0 \
and (nx, ny) not in visited:
if _dfs(nx, ny): # 递归探索
return True
return False # 四个方向都不通
return _dfs(start[0], start[1])
# 示例迷宫
maze = [
[0,1,0,0],
[0,0,0,1],
[1,1,0,0],
[0,0,0,0]
]
print(dfs(maze, (0,0), (3,3))) # 输出:True
二、广度优先搜索(BFS)
-
定义:BFS 是"地毯式搜索",逐层向外扩展,保证第一次到达终点时路径最短。
-
原理与步骤:
(1)使用队列结构;
(2)将起点放入队列;
(3)取出队首元素,检查相邻节点;
(4)将未访问的相邻节点入队;
(5)重复直到队列为空或找到目标。 -
适用场景:
(1)最短路径问题;
(2)层次遍历;
(3)扩散类问题。 -
代码示例(迷宫最短路径):
from collections import deque
def bfs(maze, start, end):
"""
:return: 最短步数,不可达返回-1
"""
directions = [(-1,0), (1,0), (0,-1), (0,1)]
queue = deque()
visited = set()
queue.append((start[0], start[1], 0)) # (x, y, 步数)
visited.add((start[0], start[1]))
while queue:
x, y, steps = queue.popleft()
# 到达终点
if (x, y) == end:
return steps
# 探索四个方向
for dx, dy in directions:
nx = x + dx
ny = y + dy
# 检查新位置合法性
if 0 <= nx < len(maze) and 0 <= ny < len(maze[0]) \
and maze[nx][ny] == 0 \
and (nx, ny) not in visited:
visited.add((nx, ny))
queue.append((nx, ny, steps + 1))
return -1 # 不可达
print(bfs(maze, (0,0), (3,3))) # 输出:6
三、剪枝优化
-
定义:通过提前判断舍弃不可能的解,减少搜索空间的优化技巧。
-
常用的简直方法:
(1)可行性剪枝:当前路径明显不可行时终止;
(2)最优性剪枝:当前路径已不如已知最优解时终止;
(3)重复状态剪枝:避免重复处理相同状态。 -
代码示例(全排列的剪枝优化):
def permutation(nums):
res = []
used = [False] * len(nums) # 记录元素使用情况
def backtrack(path):
# 终止条件:路径长度等于数组长度
if len(path) == len(nums):
res.append(path.copy())
return
for i in range(len(nums)):
if used[i]: # 剪枝:已经使用过的元素跳过
continue
# 做选择
used[i] = True
path.append(nums[i])
# 递归探索
backtrack(path)
# 撤销选择
used[i] = False
path.pop()
backtrack([])
return res
print(permutation([1,2,3]))
# 输出:[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
四、综合练习题
- 问题描述——尝试用DFS+BFS+剪枝解决以下问题:
"""
给定一个8x8棋盘,求马从起点到终点的最短步数
(马走日字,可能有障碍物)
"""
# 提示:使用BFS记录步数,用三维数组记录不同状态
-
关键点说明:
(1)方向定义:通过8个偏移量表示马的日字型移动;
(2)访问标记:使用二维数组避免重复访问,保证时间复杂度为O(n²);
(3)队列结构:存储坐标和步数,保证层序扩展(BFS核心)
(4)剪枝条件:
①越界检查(0 <= nx < 8)
②障碍物检查(chessboard[nx][ny] == 0)
③访问状态检查(not visited[nx][ny]) -
示例代码:
from collections import deque
def min_knight_steps(chessboard, start, end):
"""
计算马从起点到终点的最短步数(BFS实现)
参数:
chessboard -- 8x8二维数组,0表示空地,1表示障碍物
start -- 起始坐标元组 (x, y)
end -- 终点坐标元组 (x, y)
返回:
整数:最短步数(不可达返回-1)
"""
# 马可走的八个方向(日字型移动)
directions = [
(-2, -1), (-2, 1), # 上移两格,左/右移一格
(-1, -2), (-1, 2), # 左移两格,上/下移一格
(1, -2), (1, 2), # 右移两格,上/下移一格
(2, -1), (2, 1) # 下移两格,左/右移一格
]
# ---------- 前置条件检查 ----------
# 检查起点或终点是否有障碍物
if chessboard[start[0]][start[1]] == 1 or chessboard[end[0]][end[1]] == 1:
return -1
# 起点和终点重合的情况
if start == end:
return 0
# ---------- BFS初始化 ----------
# 创建访问标记数组(记录已访问的位置)
visited = [[False for _ in range(8)] for _ in range(8)]
# 创建队列,元素格式:(x坐标, y坐标, 已走步数)
queue = deque()
queue.append((start[0], start[1], 0))
visited[start[0]][start[1]] = True # 标记起点已访问
# ---------- BFS主循环 ----------
while queue:
x, y, steps = queue.popleft() # 取出队首元素
# 找到终点,立即返回当前步数(BFS保证此时是最短路径)
if (x, y) == end:
return steps
# 遍历所有可能的移动方向
for dx, dy in directions:
nx = x + dx # 计算新坐标x
ny = y + dy # 计算新坐标y
# 检查新坐标是否在棋盘范围内
if 0 <= nx < 8 and 0 <= ny < 8:
# 同时满足以下条件才入队:
# 1. 目标位置是空地
# 2. 未被访问过
if chessboard[nx][ny] == 0 and not visited[nx][ny]:
visited[nx][ny] = True # 标记为已访问
queue.append((nx, ny, steps + 1)) # 步数+1入队
# 队列已空但未找到路径
return -1
# --------------------------
# 示例测试
# --------------------------
# 创建示例棋盘(全空)
chessboard = [[0 for _ in range(8)] for _ in range(8)]
# 测试案例1:无障碍最短路径
print(min_knight_steps(chessboard, (0,0), (1,2))) # 输出:1
# 测试案例2:设置障碍物
chessboard[1][2] = 1 # 堵住第一步的最短路径
chessboard[2][1] = 1 # 堵住另一个可能方向
print(min_knight_steps(chessboard, (0,0), (1,2))) # 输出:-1
# 测试案例3:跨棋盘移动
chessboard[1][2] = 0 # 恢复第一步的最短路径
chessboard[2][1] = 0
print(min_knight_steps(chessboard, (0,0), (7,7))) # 输出:6(无障碍时)