下篇:回溯与剪枝的「智慧寻宝人」——DFS 进阶与网格 / 图论应用

掌握了基础回溯之后,DFS 的真正威力体现在二维网格、图结构、约束满足类问题中。本篇聚焦 DFS 在网格遍历、图连通性、复杂约束回溯中的高阶应用,带你学会「Flood Fill 泛洪算法」和「约束剪枝」两大杀器。

例题 1:岛屿数量

题目 :给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和 / 或竖直方向上相邻的陆地连接形成。

思路讲解 这是最经典的 Flood Fill(泛洪填充)问题。遍历网格每一个格子,只要遇到未访问的陆地,就以它为起点启动 DFS,把所有相连的陆地都标记为已访问(直接改成水即可),每启动一次 DFS 就代表发现一座岛屿。

完整代码(C++)

cpp

运行

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution {
private:
    int m, n; // 网格的行数和列数
    
    // 从 (i,j) 出发,把所有相连的陆地淹没(标记为已访问)
    void dfs(vector<vector<char>>& grid, int i, int j) {
        // 越界或当前不是陆地,终止递归
        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != '1') {
            return;
        }
        
        grid[i][j] = '0'; // 标记为已访问(淹没)
        
        // 向上下左右四个方向深度优先遍历
        dfs(grid, i - 1, j); // 上
        dfs(grid, i + 1, j); // 下
        dfs(grid, i, j - 1); // 左
        dfs(grid, i, j + 1); // 右
    }

public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty()) return 0;
        m = grid.size();
        n = grid[0].size();
        int count = 0;
        
        // 遍历每个格子
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    count++;       // 发现新岛屿
                    dfs(grid, i, j); // 淹没整座岛屿
                }
            }
        }
        return count;
    }
};

代码详解

  • 方向数组思想:这里显式写出四个方向,也可以用方向数组 dirs = {``{-1,0},{1,0},{0,-1},{0,1}} 简化代码。
  • 原地修改技巧:直接把访问过的陆地改成 '0',省去了额外的 visited 数组,空间复杂度降为 O (1)(不计递归栈)。
  • 计数时机:每次进入 DFS 前计数,因为一次 DFS 对应一整座岛屿。

例题 2:单词搜索

题目 :给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则,返回 false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中相邻单元格是水平相邻或垂直相邻的单元格。

思路讲解 这是二维网格上的回溯问题。遍历网格中每个字符作为起点,若与单词首字母匹配,就启动 DFS 向四周探索,逐位匹配单词字符;匹配失败则回溯,注意要标记已访问的位置避免重复使用。

完整代码(C++)

cpp

运行

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Solution {
private:
    int m, n;
    vector<vector<bool>> visited;
    // 上下左右四个方向
    int dirs[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
    
    // index:当前匹配到单词的第几个字符
    bool dfs(vector<vector<char>>& board, string& word, int i, int j, int index) {
        // 匹配到单词最后一个字符,成功
        if (index == word.size() - 1) {
            return board[i][j] == word[index];
        }
        
        // 当前字符匹配,才继续深入
        if (board[i][j] == word[index]) {
            visited[i][j] = true; // 标记已访问
            
            // 遍历四个方向
            for (auto& dir : dirs) {
                int ni = i + dir[0];
                int nj = j + dir[1];
                
                // 坐标合法且未访问过
                if (ni >= 0 && ni < m && nj >= 0 && nj < n && !visited[ni][nj]) {
                    if (dfs(board, word, ni, nj, index + 1)) {
                        return true; // 找到一条路径就直接返回
                    }
                }
            }
            
            visited[i][j] = false; // 回溯:撤销访问标记
        }
        return false;
    }

public:
    bool exist(vector<vector<char>>& board, string word) {
        m = board.size();
        n = board[0].size();
        visited.resize(m, vector<bool>(n, false));
        
        // 枚举所有起点
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, word, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }
};

代码详解

  • 提前返回:只要找到一条合法路径就立刻返回 true,无需遍历所有可能,大幅提速。
  • 回溯本质:四个方向都探索完后,必须取消当前格子的访问标记,因为它可能属于其他路径。
  • 边界处理:先判断 index 是否到末尾,再判断字符是否匹配,逻辑清晰且避免越界。

例题 3:被围绕的区域

题目 :给你一个 m x n 的矩阵 board,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充。被围绕的区间不会存在于边界上。

思路讲解 正向找「被包围的 O」比较复杂,逆向思维更简单:边界上的 O 以及和边界相连的 O 都不会被包围 。我们从四条边界的 O 出发做 DFS,把所有不被包围的 O 标记成特殊字符(如 '#');最后遍历整个矩阵,把剩余的 O 改成 X,把 # 还原成 O 即可。

完整代码(C++)

cpp

运行

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution {
private:
    int m, n;
    int dirs[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
    
    void dfs(vector<vector<char>>& board, int i, int j) {
        if (i < 0 || i >= m || j < 0 || j >= n) return;
        if (board[i][j] != 'O') return; // 不是 O 或者已经标记过,终止
        
        board[i][j] = '#'; // 标记为「与边界连通,不被包围」
        
        for (auto& dir : dirs) {
            dfs(board, i + dir[0], j + dir[1]);
        }
    }

public:
    void solve(vector<vector<char>>& board) {
        if (board.empty()) return;
        m = board.size();
        n = board[0].size();
        
        // 1. 遍历左右边界
        for (int i = 0; i < m; i++) {
            if (board[i][0] == 'O') dfs(board, i, 0);
            if (board[i][n-1] == 'O') dfs(board, i, n-1);
        }
        
        // 2. 遍历上下边界
        for (int j = 0; j < n; j++) {
            if (board[0][j] == 'O') dfs(board, 0, j);
            if (board[m-1][j] == 'O') dfs(board, m-1, j);
        }
        
        // 3. 二次遍历:O 变 X,# 变回 O
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                } else if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }
    }
};

代码详解

  • 逆向思维:从边界入手,标记所有「安全的 O」,剩下的 O 自然就是被包围的。
  • 两次遍历:第一次 DFS 标记,第二次修改结果,逻辑清晰且时间复杂度仍为 O (mn)。
  • 边界 DFS 起点:只从四条边上的 O 出发,避免了遍历整个矩阵启动 DFS。

例题 4:解数独

题目:编写一个程序,通过填充空格来解决数独问题。数独的解法需遵循:数字 1-9 在每一行、每一列、每个 3x3 宫格内都只能出现一次。

思路讲解 数独是典型的「约束满足型回溯」问题。我们按格子顺序逐个填空,每个位置尝试 1-9 所有合法数字;填入后递归填下一个格子,若后续无解则回溯换数字。通过行、列、宫格三个数组快速判断数字是否合法,实现强力剪枝。

完整代码(C++)

cpp

运行

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Solution {
private:
    // 三个标记数组:行、列、3x3 宫格中数字是否已使用
    vector<vector<bool>> row;
    vector<vector<bool>> col;
    vector<vector<bool>> box;
    
    // 找到一个解就返回 true,停止继续搜索
    bool dfs(vector<vector<char>>& board, int pos) {
        // 所有 81 个格子都填完了,找到解
        if (pos == 81) return true;
        
        int i = pos / 9; // 当前行号
        int j = pos % 9; // 当前列号
        int boxIdx = (i / 3) * 3 + j / 3; // 所在宫格编号
        
        // 如果当前格子已经有数字,直接跳下一个
        if (board[i][j] != '.') {
            return dfs(board, pos + 1);
        }
        
        // 尝试填入 1-9
        for (int num = 1; num <= 9; num++) {
            // 剪枝:行、列、宫格中只要有一个出现过,就不能填
            if (row[i][num] || col[j][num] || box[boxIdx][num]) {
                continue;
            }
            
            // 填入数字
            board[i][j] = num + '0';
            row[i][num] = true;
            col[j][num] = true;
            box[boxIdx][num] = true;
            
            // 递归填下一个格子,如果成功直接返回
            if (dfs(board, pos + 1)) {
                return true;
            }
            
            // 回溯:撤销填入
            board[i][j] = '.';
            row[i][num] = false;
            col[j][num] = false;
            box[boxIdx][num] = false;
        }
        
        return false; // 1-9 都试完都不行,返回失败
    }

public:
    void solveSudoku(vector<vector<char>>& board) {
        // 初始化三个标记数组(下标 0 不用,1-9 对应数字)
        row.assign(9, vector<bool>(10, false));
        col.assign(9, vector<bool>(10, false));
        box.assign(9, vector<bool>(10, false));
        
        // 先统计已有数字
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    int num = board[i][j] - '0';
                    int boxIdx = (i / 3) * 3 + j / 3;
                    row[i][num] = true;
                    col[j][num] = true;
                    box[boxIdx][num] = true;
                }
            }
        }
        
        dfs(board, 0); // 从第 0 个格子开始填
    }
};

代码详解

  • 位置编码:用 pos 从 0 到 80 代表 81 个格子,通过除法和取模换算出行列,简化递归参数。
  • 三维约束剪枝:行、列、宫格三重校验,不合法的数字直接跳过,大幅减少搜索分支。
  • 提前终止:找到第一个解就立刻返回,因为题目保证只有唯一解,无需继续搜索。

谢谢

相关推荐
2601_954526753 小时前
【工业传感与算法实战】温漂补偿与零点抗漂破局:基于二阶多项式拟合的 C/C++ 边缘校准算法,深度拆解“压力变送器什么牌子好”的技术硬指标
c语言·c++·算法
code_pgf4 小时前
`unordered_map` 详解
c++
一拳一个呆瓜5 小时前
【STL】iostream 编程:检测提取操作产生的错误
c++·stl
稚南城才子,乌衣巷风流5 小时前
判断素数与拓展应用
c++
众少成多积小致巨5 小时前
C++ 规范参考(下)
c++
c238565 小时前
把 C++ 内存分配拆透:new 与 malloc 的三层血缘
开发语言·c++·算法
txzrxz7 小时前
最短路问题——Dijkstra 算法
数据结构·c++·算法·最短路·优先队列
noipp7 小时前
推荐题目:洛谷 P6231 [JSOI2013] 公交系统
c语言·数据结构·c++·算法·游戏·洛谷·luogu
c238567 小时前
C/C++每日一练6
c语言·c++·算法
念恒123068 小时前
网络基础
linux·网络·c++