【图论/树】算法「DFS/BFS」思想,附两道道手撕题

51 阅读7分钟

在图论和树结构中,深度优先遍历(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 时,该 1 被反转为 0,同时相邻的上、下、左、右,以及左上、左下、右上、右下 8 个方向的 1 (如果存在 1)均会自动反转为 0; 

  2. 进一步地,一个位置上的 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。

解题步骤

  1. 初始化:定义一个dfs函数,用于深度优先搜索,将连通区域内的所有1标记为已访问(例如,将它们设置为0)。
  2. 遍历矩阵:逐个检查矩阵中的每个元素,对于每个未被访问的1,执行dfs函数,并增加连通分量的计数。
  3. 输出结果:连通分量的计数即为最少点击次数。
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)  # 输出连通块的数量