在图论和树结构中,深度优先遍历(DFS)和广度优先遍历(BFS)是两种基本的搜索算法,它们在解决各种算法问题时有着广泛的应用。本文将详细介绍这两种算法的原理、特点以及它们在解决特定问题时的应用。
深度优先遍历(DFS)
算法原理
深度优先遍历(DFS)是一种利用回溯思想的搜索算法。它从起始节点开始,沿着一条路径尽可能深入地访问节点,直到无法继续前进时为止,然后回溯到上一个未访问的节点,继续深入搜索,直到完成整个搜索过程。
实现方式
DFS可以通过两种方式实现:栈(非递归)和递归。
- 栈(非递归):手动使用栈来模拟递归过程,将待访问的节点入栈,然后出栈访问,继续将下一个节点入栈。
- 递归:递归函数在到达路径的末端时自动回溯,继续搜索其他路径。
特点
- 先进后出:由于DFS的回溯特性,它遵循栈的LIFO(后进先出)原则。
- 一路到底,逐层回退:DFS会沿着一条路径深入直到尽头,然后逐层回退。
应用场景
DFS适用于需要找到所有解的问题,例如迷宫寻路、路径计数、N皇后问题等。
广度优先遍历(BFS)
算法原理
广度优先遍历(BFS)是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外扩张,直到完成整个搜索过程。
实现方式
BFS通常使用队列来实现,将起始节点入队,然后出队访问,将所有相邻未访问节点入队,直到队列为空。
特点
- 先进先出:BFS遵循队列的FIFO(先进先出)原则。
- 全面扩散,逐层递进:BFS会逐层访问所有节点,直到找到目标或遍历完所有节点。
应用场景
BFS适用于需要找到最短路径的问题,例如最短路径问题、社交网络中的影响力传播等。
算法比较与选择
- 空间复杂度:DFS通常比BFS更节省空间,因为DFS不需要存储所有层级的节点。
- 时间复杂度:在最坏情况下,两者的时间复杂度相同,都是O(V+E),其中V是顶点数,E是边数。
- 适用问题:DFS适合于需要遍历所有可能路径的问题,而BFS适合于需要找到最短路径的问题。
实例题
N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 **n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
输入: n = 4
输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释: 如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入: n = 1
输出: [["Q"]]
解题思路
由于每行恰好放一个皇后,记录每行的皇后放在哪一列,可以得到一个 [0,n−1] 的排列 queens。示例 1 的两个图,分别对应排列 [1,3,0,2] 和 [2,0,3,1]。
所以此题本质上是在枚举列号的全排列。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
ans = []
queens = [0] * n # 皇后放在 (r,queens[r])
col = [False] * n
diag1 = [False] * (n * 2 - 1)
diag2 = [False] * (n * 2 - 1)
def dfs(r: int) -> None:
if r == n:
ans.append(['.' * c + 'Q' + '.' * (n - 1 - c) for c in queens])
return
# 在 (r,c) 放皇后
for c, ok in enumerate(col):
if not ok and not diag1[r + c] and not diag2[r - c]: # 判断能否放皇后
queens[r] = c # 直接覆盖,无需恢复现场
col[c] = diag1[r + c] = diag2[r - c] = True # 皇后占用了 c 列和两条斜线
dfs(r + 1)
col[c] = diag1[r + c] = diag2[r - c] = False # 恢复现场
dfs(0)
return ans
开心消消乐
描述
给定一个 N 行 M 列的二维矩阵,矩阵中每个位置的数字取值为 0 或 1,矩阵示例如:
1 1 0 0
0 0 0 1
0 0 1 1
1 1 1 1
现需要将矩阵中所有的 1 进行反转为 0,规则如下:
-
当点击一个 1 时,该 1 被反转为 0,同时相邻的上、下、左、右,以及左上、左下、右上、右下 8 个方向的 1 (如果存在 1)均会自动反转为 0;
-
进一步地,一个位置上的 1 被反转为 0 时,与其相邻的 8 个方向的 1 (如果存在 1)均会自动反转为 0。
按照上述规则示例中的矩阵只最少需要点击 2 次后,所有均值 0 。
请问,给定一个矩阵,最少需要点击几次后,所有数字均为 0?
输入描述
第一行输入两个整数,分别表示矩阵的行数 N 和列数 M,取值范围均为 [1,100]
接下来 N 行表示矩阵的初始值,每行均为 M 个数,取值范围 [0,1]
输出描述
输出一个整数,表示最少需要点击的次数
用例输入 1
3 3
1 0 1
0 1 0
1 0 1
用例输出 1
1
解题思想
给定一个由0和1组成的二维矩阵,我们的目标是确定最少需要点击多少次,以将矩阵中的所有1变为0。每次点击会将选定位置的1及其周围8个方向上的1同时反转为0。这个问题可以转化为统计矩阵中1的连通分量的数量,因为每个连通分量内的1可以通过单次点击全部变为0。
解题步骤
- 初始化:定义一个
dfs
函数,用于深度优先搜索,将连通区域内的所有1标记为已访问(例如,将它们设置为0)。 - 遍历矩阵:逐个检查矩阵中的每个元素,对于每个未被访问的1,执行
dfs
函数,并增加连通分量的计数。 - 输出结果:连通分量的计数即为最少点击次数。
def dfs(matrix, x, y):
"""
深度优先搜索函数,用于标记矩阵中连通的1块。
:param matrix: 二维列表,表示矩阵。
:param x: 当前节点的行索引。
:param y: 当前节点的列索引。
"""
# 将当前位置标记为已访问(值为0)
matrix[x][y] = 0
rows, cols = len(matrix), len(matrix[0]) # 获取矩阵的行数和列数
# 定义八个方向的偏移量
directions = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]
for dir in directions: # 遍历所有方向
nextX, nextY = x + dir[0], y + dir[1] # 计算下一个节点的坐标
# 检查下一个节点是否在矩阵内且未被访问(值为1)
if 0 <= nextX < rows and 0 <= nextY < cols and matrix[nextX][nextY] == 1:
dfs(matrix, nextX, nextY) # 递归访问下一个节点
# 读取矩阵的行数和列数
rows, cols = map(int, input().split())
matrix = [] # 初始化矩阵列表
# 读取矩阵的每一行数据
for i in range(rows):
row = list(map(int, input().split()))
matrix.append(row)
result = 0 # 初始化连通块数量
# 遍历矩阵,寻找值为1的节点并进行深度优先搜索
for i in range(rows):
for j in range(cols):
if matrix[i][j] == 1: # 如果当前节点值为1
result += 1 # 连通块数量加1
dfs(matrix, i, j) # 进行深度优先搜索
print(result) # 输出连通块的数量