DFS / BFS 进阶(图搜索 + 回溯 + 扩散)

分享:
算法DFSBFS回溯拓扑排序

DFS / BFS 进阶(图搜索 + 回溯 + 扩散)

📖 学习路径: 01 树遍历 → 02 DFS/BFS 进阶 → 03 动态规划

⚠️ 本专题承接 01,不再重复树的遍历题。01 已覆盖:前/中/后序遍历、层序、最大/最小深度、路径总和、右视图、对称树。本专题聚焦回溯、网格搜索、扩散、拓扑排序、最短路径

一、代码框架(本专题统一模板)

约定: 所有代码使用统一风格。

  • 类型注解完整、变量名统一
  • 条件判断用 while (queue.length > 0)
  • 网格方向数组统一命名为 DIRS
  • DFS 内部函数统一命名为 dfs
  • 结果变量统一命名为 result

框架一:DFS 回溯模板(全排列、组合、子集)

const backtrack = (选择列表) => {
    const result: 结果类型[] = [];
    const path: 元素类型[] = [];          // ① 共享路径容器(引用类型)

    const dfs = (start: number) => {
        if (满足终止条件) {
            result.push([...path]);        // ② 深拷贝收集结果
            return;
        }

        for (let i = start; i < 选择列表.length; i++) {
            path.push(选择列表[i]);        // ③ 做选择
            dfs(下一个起点);               // ④ 递归进入下一层
            path.pop();                    // ⑤ 撤销选择(回溯)
        }
    };

    dfs(0);
    return result;
};

💡 回溯三要素: ① 共享 path 数组 → ② push 选择 → ③ 递归 → ④ pop 撤销

框架二:网格 DFS/BFS 模板(岛屿、扩散)

const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];  // 四方向:下、上、右、左

// DFS 版:原地标记
const dfs = (i: number, j: number) => {
    if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] !== 有效值) return;
    grid[i][j] = 已访问值;  // 原地标记
    for (const [di, dj] of DIRS) {
        dfs(i + di, j + dj);
    }
};

// BFS 版:队列扩散
const queue: [number, number][] = [[startI, startJ]];
grid[startI][startJ] = 已访问值;  // 入队前标记

while (queue.length > 0) {
    const [i, j] = queue.shift()!;
    for (const [di, dj] of DIRS) {
        const ni = i + di, nj = j + dj;
        if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] === 有效值) {
            grid[ni][nj] = 已访问值;  // 入队前标记
            queue.push([ni, nj]);
        }
    }
}

框架三:多源 BFS 扩散模板(腐烂橘子、01矩阵)

const queue: [number, number][] = [];
// ① 收集所有初始源点
for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
        if (grid[i][j] === 源值) queue.push([i, j]);
    }
}

let steps = 0;
// ② 按层扩散
while (queue.length > 0 && 还有未处理的目标) {
    const size = queue.length;
    let changed = false;  // 本轮是否有变化

    for (let k = 0; k < size; k++) {
        const [i, j] = queue.shift()!;
        for (const [di, dj] of DIRS) {
            const ni = i + di, nj = j + dj;
            if (有效且是目标值) {
                grid[ni][nj] = 源值;    // 标记
                queue.push([ni, nj]);
                changed = true;
            }
        }
    }
    if (changed) steps++;  // 有变化才计数
}

框架四:BFS 拓扑排序模板

// ① 建图 + 统计入度
const graph: number[][] = Array.from({ length: n }, () => []);
const indegree: number[] = new Array(n).fill(0);
for (const [to, from] of edges) {
    graph[from].push(to);
    indegree[to]++;
}

// ② 入度为 0 的节点入队
const queue: number[] = [];
for (let i = 0; i < n; i++) {
    if (indegree[i] === 0) queue.push(i);
}

// ③ BFS 剥离
let count = 0;
while (queue.length > 0) {
    const cur = queue.shift()!;
    count++;
    for (const next of graph[cur]) {
        indegree[next]--;
        if (indegree[next] === 0) queue.push(next);
    }
}
// ④ 判断:count === n → 无环

框架五:BFS 最短路径模板(单词接龙、转盘锁)

const bfsShortest = (start: 状态类型, target: 状态类型): number => {
    const visited = new Set<状态类型>();
    const queue: 状态类型[] = [start];
    visited.add(start);
    let steps = 0;

    while (queue.length > 0) {
        const size = queue.length;
        for (let k = 0; k < size; k++) {
            const cur = queue.shift()!;
            if (cur === target) return steps;  // 到达目标

            // 生成所有合法下一状态
            for (const next of 生成下一状态(cur)) {
                if (!visited.has(next)) {
                    visited.add(next);  // 入队前标记
                    queue.push(next);
                }
            }
        }
        steps++;
    }
    return -1;  // 无法到达
};

二、典型例题

🟢 回溯入门

1. 全排列(LeetCode 46)

问题: 给定不含重复数字的数组 nums,返回所有可能的全排列。

思路: 回溯模板 + used[] 标记已选元素。

const permute = (nums: number[]): number[][] => {
    const result: number[][] = [];
    const path: number[] = [];
    const used: boolean[] = new Array(nums.length).fill(false);

    const dfs = () => {
        // 终止条件:路径长度 == 数组长度
        if (path.length === nums.length) {
            result.push([...path]);  // 深拷贝
            return;
        }

        for (let i = 0; i < nums.length; i++) {
            if (used[i]) continue;   // 跳过已选的
            path.push(nums[i]);      // 选择
            used[i] = true;
            dfs();                   // 递归
            path.pop();              // 撤销选择(回溯)
            used[i] = false;
        }
    };

    dfs();
    return result;
};

执行过程(nums=[1,2,3]):

                     []
           /         |         \
         1           2           3
       /   \       /   \       /   \
     2      3     1     3     1     2
    /        \    |     |     |      \
   3          2   3     1     2       1

path 变化:  [1] → [1,2] → [1,2,3] → 收集 → pop(3) → [1,2] → pop(2) → [1]
             → [1,3] → [1,3,2] → 收集 → ...

2. 子集(LeetCode 78)

问题: 给定不含重复元素的数组 nums,返回所有可能的子集(幂集)。

思路: 回溯,用 start 索引控制不重复选。

const subsets = (nums: number[]): number[][] => {
    const result: number[][] = [];
    const path: number[] = [];

    const dfs = (start: number) => {
        // 每个节点都是一个有效子集(包括空集)
        result.push([...path]);

        for (let i = start; i < nums.length; i++) {
            path.push(nums[i]);     // 选择
            dfs(i + 1);             // 从 i+1 开始,保证不重复
            path.pop();             // 回溯
        }
    };

    dfs(0);
    return result;
};

执行过程(nums=[1,2,3]):

                     []              ← start=0: 收集 []
            /         |         \
          [1]        [2]        [3]  ← start=1,2,3: 分别收集
         /   \        |
     [1,2]  [1,3]   [2,3]           ← 继续深入
      /
  [1,2,3]

result = [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]

💡 子集 vs 全排列的区别:子集用 start 保证只往后选,全排列用 used[] 保证不重复选。


3. 组合总和(LeetCode 39)

问题: 给定无重复元素的数组 candidates 和目标值 target,找出所有和为 target 的组合。数字可无限次重复选取。

思路: 回溯 + 剪枝。关键:递归时传 i 而非 i+1,允许重复选当前元素。

const combinationSum = (candidates: number[], target: number): number[][] => {
    const result: number[][] = [];
    const path: number[] = [];

    const dfs = (start: number, remaining: number) => {
        if (remaining < 0) return;           // 剪枝:超过目标
        if (remaining === 0) {
            result.push([...path]);          // 找到一组解
            return;
        }

        for (let i = start; i < candidates.length; i++) {
            path.push(candidates[i]);        // 选择
            dfs(i, remaining - candidates[i]); // ← 传 i,允许重复选自己
            path.pop();                      // 回溯
        }
    };

    dfs(0, target);
    return result;
};

执行过程(candidates=[2,3,5], target=8):

                           (start=0, remaining=8)
                    /              |              \
             选2                   选3              选5
        (0, 6)                (1, 5)           (2, 3)
       /   |   \              /   \              ✗ 5>3
     2     3    5           3      5
  (0,4) (1,3) (2,1)      (1,2)   (2,0) ← 收集 [3,5]
   /  \    ✗ 3>1         ✗ 3>2
  2    3
(0,2) (1,-1)✗
  |
  2
(0,0) ← 收集 [2,2,2,2]

最终:[[2,2,2,2], [2,3,3], [3,5]]

🟡 网格搜索

4. 岛屿数量(LeetCode 200)

问题: 给定 '1'(陆地)和 '0'(水)的二维网格,计算岛屿数量。岛屿是水平/垂直相邻的陆地组成的连通区域。

思路: 遍历网格,发现陆地就 DFS 淹没整个岛,计数 +1。

const numIslands = (grid: string[][]): number => {
    const m = grid.length;
    const n = grid[0].length;
    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    let count = 0;

    const dfs = (i: number, j: number) => {
        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] === '0') return;

        grid[i][j] = '0';  // 淹没 = 标记已访问

        for (const [di, dj] of DIRS) {
            dfs(i + di, j + dj);
        }
    };

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === '1') {
                count++;       // 发现新岛屿
                dfs(i, j);     // 淹没整个岛
            }
        }
    }
    return count;
};

BFS 版(同思路,队列实现):

const numIslandsBFS = (grid: string[][]): number => {
    const m = grid.length, n = grid[0].length;
    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    let count = 0;

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === '1') {
                count++;
                const queue: [number, number][] = [[i, j]];
                grid[i][j] = '0';  // 入队前标记

                while (queue.length > 0) {
                    const [r, c] = queue.shift()!;
                    for (const [dr, dc] of DIRS) {
                        const nr = r + dr, nc = c + dc;
                        if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] === '1') {
                            grid[nr][nc] = '0';  // 入队前标记
                            queue.push([nr, nc]);
                        }
                    }
                }
            }
        }
    }
    return count;
};

5. 岛屿的最大面积(LeetCode 695)

问题: 给定 0(水)和 1(陆地)的二维网格,求最大的岛屿面积(连通陆地数量)。

思路: DFS 递归返回面积:1 + 四方向面积之和

const maxAreaOfIsland = (grid: number[][]): number => {
    const m = grid.length, n = grid[0].length;
    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    let maxArea = 0;

    const dfs = (i: number, j: number): number => {
        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] === 0) return 0;

        grid[i][j] = 0;  // 标记已访问

        // 当前格子 + 四个方向的面积
        let area = 1;
        for (const [di, dj] of DIRS) {
            area += dfs(i + di, j + dj);
        }
        return area;
    };

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === 1) {
                maxArea = Math.max(maxArea, dfs(i, j));
            }
        }
    }
    return maxArea;
};

为什么要把四个方向的结果加起来? DFS 递归返回的是以当前格子为起点向四个方向能连通的所有陆地数量,累加起来就是整个岛的面积。


6. 被围绕的区域(LeetCode 130)

问题: 给定 'X''O' 的二维矩阵,将所有被 'X' 包围的 'O' 翻转为 'X'。边界的 'O' 不被包围(或与边界 'O' 相连的也不被包围)。

思路: 逆向思维——从边界 'O' 出发 DFS 标记为 '#',最后遍历:'#' → 'O''O' → 'X'

原始:        边界DFS标记:      最终:
X X X X      X X X X          X X X X
X O O X  →   X O O X      →   X X X X
X X O X      X X O X          X X X X
X O X X      X # X X          X O X X
              ↑边界O标记#
const solve = (board: string[][]): void => {
    const m = board.length, n = board[0].length;
    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];

    const dfs = (i: number, j: number) => {
        if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] !== 'O') return;
        board[i][j] = '#';  // 标记为"安全的O"
        for (const [di, dj] of DIRS) {
            dfs(i + di, j + dj);
        }
    };

    // ① 从四边界出发,标记所有不被包围的 'O'
    for (let i = 0; i < m; i++) {
        if (board[i][0] === 'O') dfs(i, 0);
        if (board[i][n - 1] === 'O') dfs(i, n - 1);
    }
    for (let j = 0; j < n; j++) {
        if (board[0][j] === 'O') dfs(0, j);
        if (board[m - 1][j] === 'O') dfs(m - 1, j);
    }

    // ② 最终处理:'#' 恢复为 'O','O' 翻转为 'X'
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (board[i][j] === '#') board[i][j] = 'O';
            else if (board[i][j] === 'O') board[i][j] = 'X';
        }
    }
};

🟠 多源 BFS 扩散

7. 腐烂的橘子(LeetCode 994)

问题: 网格中 0=空、1=新鲜橘子、2=腐烂橘子。每分钟腐烂橘子让四方向相邻新鲜橘子腐烂。求所有橘子腐烂的最少分钟数,不可能则返回 -1

思路: 多源 BFS——所有烂橘子同时入队,按分钟(层)扩散。

const orangesRotting = (grid: number[][]): number => {
    const m = grid.length, n = grid[0].length;
    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    const queue: [number, number][] = [];
    let fresh = 0;

    // ① 统计初始状态
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === 2) queue.push([i, j]);
            else if (grid[i][j] === 1) fresh++;
        }
    }
    if (fresh === 0) return 0;  // 没有新鲜橘子

    let minutes = 0;
    // ② 多源 BFS
    while (queue.length > 0 && fresh > 0) {
        const size = queue.length;
        let changed = false;

        for (let k = 0; k < size; k++) {
            const [i, j] = queue.shift()!;
            for (const [di, dj] of DIRS) {
                const ni = i + di, nj = j + dj;
                if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] === 1) {
                    grid[ni][nj] = 2;      // 腐烂
                    fresh--;
                    queue.push([ni, nj]);
                    changed = true;
                }
            }
        }
        if (changed) minutes++;  // 只有真的腐烂了新橘子才计数
    }

    return fresh === 0 ? minutes : -1;
};

执行过程:

初始:             第1分钟:           第2分钟:
2 1 1           2 2 1             2 2 2
1 1 0      →    2 1 0        →    2 2 0
0 1 1           0 1 1             0 2 1
fresh=6          fresh=3            fresh=1

第3分钟:          第4分钟:
2 2 2           2 2 2
2 2 0      →    2 2 0
0 2 2           0 2 2
fresh=0 ✅       答案=4

💡 changed 的作用:防止空转一轮(当前层没有腐烂任何橘子)也 +1。


🟣 拓扑排序

8. 课程表(LeetCode 207)

问题: numCourses 门课,prerequisites[i] = [a, b] 表示先修 b 才能修 a。判断能否修完所有课(即是否有环)。

思路: BFS 拓扑排序三步:建图+统计入度 → 入度0入队 → 逐层剥离。

const canFinish = (numCourses: number, prerequisites: number[][]): boolean => {
    // ① 建图 + 统计入度
    const graph: number[][] = Array.from({ length: numCourses }, () => []);
    const indegree: number[] = new Array(numCourses).fill(0);

    for (const [course, prereq] of prerequisites) {
        // [1, 0] 表示 0 → 1(学完0才能学1)
        graph[prereq].push(course);
        indegree[course]++;
    }

    // ② 入度为 0 的课程入队(没有前置课,可以直接学)
    const queue: number[] = [];
    for (let i = 0; i < numCourses; i++) {
        if (indegree[i] === 0) queue.push(i);
    }

    // ③ BFS 逐层剥离
    let count = 0;  // 已完成的课程数
    while (queue.length > 0) {
        const cur = queue.shift()!;
        count++;

        for (const next of graph[cur]) {
            indegree[next]--;
            if (indegree[next] === 0) {  // 前置课全部学完
                queue.push(next);
            }
        }
    }

    // ④ 完成的课程数 == 总课程数 → 无环,可以完成
    return count === numCourses;
};

执行过程(4门课,[[1,0],[2,0],[3,1],[3,2]]):

建图:              入度:
  0 → [1, 2]        课程0: 0  ← 入队
  1 → [3]           课程1: 1
  2 → [3]           课程2: 1
  3 → []            课程3: 2

BFS:
  出队0 → count=1 → 解锁1(入度1→0,入队), 解锁2(入度1→0,入队)
  出队1 → count=2 → 解锁3(入度2→1)
  出队2 → count=3 → 解锁3(入度1→0,入队)
  出队3 → count=4
  count=4 == numCourses=4 → true ✅

如果有环 [[0,1],[1,0]]:
  入度: 0→1, 1→1  → 没有入度为0的课
  队列为空 → count=0 → false ❌

9. 课程表 II(LeetCode 210)

问题: 与课程表类似,但需要返回一种可行的上课顺序。

思路: 拓扑排序,出队顺序就是上课顺序。

const findOrder = (numCourses: number, prerequisites: number[][]): number[] => {
    // 建图 + 统计入度(与上题完全相同)
    const graph: number[][] = Array.from({ length: numCourses }, () => []);
    const indegree: number[] = new Array(numCourses).fill(0);

    for (const [course, prereq] of prerequisites) {
        graph[prereq].push(course);
        indegree[course]++;
    }

    const queue: number[] = [];
    for (let i = 0; i < numCourses; i++) {
        if (indegree[i] === 0) queue.push(i);
    }

    const order: number[] = [];  // ← 记录上课顺序
    while (queue.length > 0) {
        const cur = queue.shift()!;
        order.push(cur);  // 出队顺序 = 上课顺序

        for (const next of graph[cur]) {
            indegree[next]--;
            if (indegree[next] === 0) queue.push(next);
        }
    }

    return order.length === numCourses ? order : [];  // 有环返回 []
};

🟣 最短路径(BFS)

10. 单词接龙(LeetCode 127)

问题: 每次改变一个字母,且改变后的单词必须在词表中。求从 beginWordendWord 的最短转换序列长度。

思路: BFS 最短路径——逐字符变换 26 个字母,首次到达 endWord 即最短。

const ladderLength = (beginWord: string, endWord: string, wordList: string[]): number => {
    const wordSet = new Set(wordList);
    if (!wordSet.has(endWord)) return 0;

    const queue: string[] = [beginWord];
    wordSet.delete(beginWord);  // 标记已访问
    let steps = 1;

    while (queue.length > 0) {
        const size = queue.length;

        for (let k = 0; k < size; k++) {
            const word = queue.shift()!;

            // 尝试改变每个位置的字母
            for (let i = 0; i < word.length; i++) {
                for (let c = 97; c <= 122; c++) {  // a ~ z
                    const newChar = String.fromCharCode(c);
                    if (newChar === word[i]) continue;

                    const newWord = word.slice(0, i) + newChar + word.slice(i + 1);

                    if (newWord === endWord) return steps + 1;  // 到达终点

                    if (wordSet.has(newWord)) {
                        queue.push(newWord);
                        wordSet.delete(newWord);  // BFS首次到达即最短,无需再访问
                    }
                }
            }
        }
        steps++;
    }
    return 0;
};

执行过程:

beginWord="hit", endWord="cog"
wordList=["hot","dot","dog","lot","log","cog"]

hit (steps=1)
  → hot 入队(删hot)
hot (steps=2)
  → dot 入队, lot 入队(删dot, lot)
dot (steps=3)
  → dog 入队(删dog)
lot (steps=3)
  → log 入队(删log)
dog (steps=4)
  → cog = endWord → return 5 ✅

序列:hit → hot → dot → dog → cog (5步)

11. 打开转盘锁(LeetCode 752)

问题: 4 位圆形密码锁,初始 "0000",每次可拨动一位数字(+1 或 -1,0→9 或 9→0)。给定死锁列表 deadends 和目标 target,求最少拨动次数。

思路: BFS 最短路径——每次 8 种拨动(4位×2方向),用 Set 记录 visited。

const openLock = (deadends: string[], target: string): number => {
    const dead = new Set(deadends);
    const visited = new Set<string>();
    const start = '0000';

    if (dead.has(start)) return -1;
    if (start === target) return 0;

    const queue: string[] = [start];
    visited.add(start);
    let steps = 0;

    while (queue.length > 0) {
        const size = queue.length;

        for (let k = 0; k < size; k++) {
            const cur = queue.shift()!;

            // 8 种拨动:4 位 × 2 方向
            for (let i = 0; i < 4; i++) {
                for (const delta of [1, -1]) {
                    const digit = (Number(cur[i]) + delta + 10) % 10;  // 处理 0→9 和 9→0
                    const next = cur.slice(0, i) + digit + cur.slice(i + 1);

                    if (next === target) return steps + 1;
                    if (!dead.has(next) && !visited.has(next)) {
                        visited.add(next);
                        queue.push(next);
                    }
                }
            }
        }
        steps++;
    }
    return -1;
};

🔴 克隆图

12. 克隆图(LeetCode 133)

问题: 给定无向连通图中的一个节点,深拷贝整个图。

思路: DFS/BFS + Map 映射(原节点 → 克隆节点)。

class GraphNode {
    val: number;
    neighbors: GraphNode[];
    constructor(val?: number, neighbors?: GraphNode[]) {
        this.val = val ?? 0;
        this.neighbors = neighbors ?? [];
    }
}

// ====== DFS 版 ======
const cloneGraphDFS = (node: GraphNode | null): GraphNode | null => {
    if (!node) return null;
    const map = new Map<GraphNode, GraphNode>();  // 原节点 → 克隆节点

    const dfs = (n: GraphNode): GraphNode => {
        if (map.has(n)) return map.get(n)!;  // 已克隆,直接返回

        const clone = new GraphNode(n.val);
        map.set(n, clone);  // 先存入 map,防止死循环

        for (const neighbor of n.neighbors) {
            clone.neighbors.push(dfs(neighbor));
        }
        return clone;
    };

    return dfs(node);
};

// ====== BFS 版 ======
const cloneGraphBFS = (node: GraphNode | null): GraphNode | null => {
    if (!node) return null;
    const map = new Map<GraphNode, GraphNode>();
    const queue: GraphNode[] = [node];

    // 先克隆第一个节点
    map.set(node, new GraphNode(node.val));

    while (queue.length > 0) {
        const cur = queue.shift()!;
        const clone = map.get(cur)!;

        for (const neighbor of cur.neighbors) {
            if (!map.has(neighbor)) {
                map.set(neighbor, new GraphNode(neighbor.val));
                queue.push(neighbor);
            }
            clone.neighbors.push(map.get(neighbor)!);
        }
    }
    return map.get(node)!;
};

三、本专题总结

题目速查表

# 题目 方法 关键技巧
1 全排列 DFS回溯 used[] + push/pop
2 子集 DFS回溯 start 索引去重
3 组合总和 DFS回溯 i 允许重复选
4 岛屿数量 网格DFS/BFS 淹没标记
5 岛屿最大面积 网格DFS 递归返回面积累加
6 被围绕的区域 边界DFS 逆向标记 '#'
7 腐烂的橘子 多源BFS 所有源同时入队,按层扩散
8 课程表 BFS拓扑排序 入度表 + 逐层剥离
9 课程表II BFS拓扑排序 出队顺序即结果
10 单词接龙 BFS最短路径 逐字符变换,Set删除标记
11 打开转盘锁 BFS最短路径 8种拨动,visited Set
12 克隆图 DFS/BFS Map 映射原节点→克隆节点

统一模板回顾

场景 模板 核心要素
全排列/子集/组合 回溯 DFS push → 递归 → pop
网格连通 网格 DFS/BFS 四方向 + 原地标记
多源扩散 多源 BFS 所有源入队 + 按层扩散
拓扑排序 BFS + 入度表 建图 → 入度0入队 → 剥离
最短路径 BFS + visited 入队前标记 + 首次到达即返回

记忆口诀

  • 回溯三步走:push → 递归 → pop
  • 岛屿淹没法:遇1就计数,dfs全变0
  • 橘子多源BFS:烂的先入队,按分钟扩散
  • 拓扑看入度:入零就入队,出队数节点
  • 最短路径BFS:入队前标记,首次到达就返回

关联题库

以下题目与本文知识点相关,可以跳转到题库练习: