迷宫问题指南

分享:
算法迷宫网格DFSBFS

迷宫问题指南

目录


核心概念

迷宫问题本质上是二维网格上的搜索问题,核心技巧:

要素 说明
网格表示 grid[row][col]grid[y][x](先 row 再 col)
方向数组 [[1,0], [-1,0], [0,1], [0,-1]](上下左右)
越界检查 x<0 || x>=cols || y<0 || y>=rows
访问标记 原地修改(grid[y][x]='0')或 visited[][]
两种搜索 DFS(递归/栈)适合连通区域;BFS(队列)适合最短路径

通用解题模板

DFS 模板(递归)

function solveDFS(grid: string[][]): number {
    const rows = grid.length;
    if (rows === 0) return 0;
    const cols = grid[0].length;

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

    function dfs(x: number, y: number): void {
        // 1. 越界检查
        if (x < 0 || x >= cols || y < 0 || y >= rows) return;
        // 2. 障碍/已访问检查
        if (grid[y][x] === '0') return;

        // 3. 处理当前格子
        grid[y][x] = '0'; // 标记已访问(淹没法)

        // 4. 四个方向递归
        for (const [dx, dy] of DIRS) {
            dfs(x + dx, y + dy);
        }
    }

    // 5. 主逻辑:遍历每个格子
    for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
            if (grid[y][x] === '1') {
                // 处理入口逻辑
                dfs(x, y);
            }
        }
    }
}

BFS 模板(队列)

function solveBFS(grid: string[][]): number {
    const rows = grid.length;
    if (rows === 0) return 0;
    const cols = grid[0].length;

    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];

    function bfs(startX: number, startY: number): void {
        const queue: [number, number][] = [[startX, startY]];
        grid[startY][startX] = '0'; // 入队即标记

        while (queue.length > 0) {
            const [x, y] = queue.shift()!;

            for (const [dx, dy] of DIRS) {
                const nx = x + dx;
                const ny = y + dy;

                // 越界 + 障碍检查
                if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue;
                if (grid[ny][nx] === '0') continue;

                grid[ny][nx] = '0'; // 标记
                queue.push([nx, ny]);
            }
        }
    }

    for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
            if (grid[y][x] === '1') {
                bfs(x, y);
            }
        }
    }
}

DFS vs BFS 选择指南

需要统计连通块数量/面积  →  DFS(代码更短)
需要求最短路径/最少步数  →  BFS(逐层扩散,天然最短)
网格极大可能栈溢出       →  BFS(迭代,无递归栈风险)
需要记录路径本身          →  BFS(配合 parent 映射回溯)

例题 1:岛屿数量

LeetCode 200 — 统计网格中连通 ‘1’ 区域的数量

输入: grid = [
  ['1','1','0','0','0'],
  ['1','1','0','0','0'],
  ['0','0','1','0','0'],
  ['0','0','0','1','1']
]
输出: 3

思路: 遍历网格,遇到 '1' 计数 +1,然后 DFS/BFS 淹没整个岛屿(把相连的 '1' 全改成 '0')。

      DFS 淹没法图解:

      遇 '1' → count=1      淹没整个岛           继续扫描
      ┌─────────┐          ┌─────────┐          ┌─────────┐
      │1 1 0 0 0│          │0 0 0 0 0│          │0 0 0 0 0│
      │1 1 0 0 0│   →      │0 0 0 0 0│   →      │0 0 1 0 0│  count=2
      │0 0 1 0 0│          │0 0 1 0 0│          │0 0 0 0 0│
      │0 0 0 1 1│          │0 0 0 1 1│          │0 0 0 1 1│
      └─────────┘          └─────────┘          └─────────┘
function numIslands(grid: string[][]): number {
    const rows = grid.length;
    if (rows === 0) return 0;
    const cols = grid[0].length;
    let count = 0;

    function dfs(x: number, y: number): void {
        if (x < 0 || x >= cols || y < 0 || y >= rows) return;
        if (grid[y][x] === '0') return;

        grid[y][x] = '0'; // 淹没

        dfs(x + 1, y);
        dfs(x - 1, y);
        dfs(x, y + 1);
        dfs(x, y - 1);
    }

    for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
            if (grid[y][x] === '1') {
                count++;
                dfs(x, y);
            }
        }
    }

    return count;
}

复杂度: 时间 O(m×n),空间 O(m×n)(递归栈最坏情况)。


例题 2:迷宫是否有路径

判断从起点 (0,0) 能否走到终点 (m-1, n-1),0 可走,1 是墙

输入: maze = [
  [0, 0, 1, 0],
  [1, 0, 0, 1],
  [0, 0, 0, 0],
  [0, 1, 0, 0]
]
输出: true(存在路径)

思路: 从起点 DFS/BFS,标记访问过的格子,看能否到达终点。

function hasPath(maze: number[][]): boolean {
    const rows = maze.length;
    if (rows === 0) return false;
    const cols = maze[0].length;
    const visited: boolean[][] = Array.from({ length: rows }, () =>
        new Array(cols).fill(false)
    );

    function dfs(x: number, y: number): boolean {
        // 越界或撞墙或已访问
        if (x < 0 || x >= cols || y < 0 || y >= rows) return false;
        if (maze[y][x] === 1 || visited[y][x]) return false;

        // 到达终点
        if (x === cols - 1 && y === rows - 1) return true;

        visited[y][x] = true;

        // 四方向搜索,任一方向到达即返回 true
        return (
            dfs(x + 1, y) ||
            dfs(x - 1, y) ||
            dfs(x, y + 1) ||
            dfs(x, y - 1)
        );
    }

    return dfs(0, 0);
}

注意: 这里用 visited[][] 而非原地修改,因为需要保留原迷宫数据。DFS 的返回值用 || 短路求值,一旦找到终点就不再继续搜索。


例题 3:迷宫最短路径

求从起点到终点的最短路径长度(步数),0 可走,1 是墙,无法到达返回 -1

输入: maze = [
  [0, 0, 0, 0],
  [1, 1, 0, 1],
  [0, 0, 0, 0],
  [0, 1, 1, 0]
]
输出: 6  (最短路径需要 6 步)

思路: 必须用 BFS,因为 BFS 逐层扩散,第一次到达终点就是最短路径。

function shortestPath(maze: number[][]): number {
    const rows = maze.length;
    if (rows === 0) return -1;
    const cols = maze[0].length;

    // 起点或终点是墙
    if (maze[0][0] === 1 || maze[rows - 1][cols - 1] === 1) return -1;

    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
    const queue: [number, number, number][] = [[0, 0, 0]]; // [x, y, steps]
    const visited: boolean[][] = Array.from({ length: rows }, () =>
        new Array(cols).fill(false)
    );
    visited[0][0] = true;

    while (queue.length > 0) {
        const [x, y, steps] = queue.shift()!;

        // 到达终点
        if (x === cols - 1 && y === rows - 1) return steps;

        for (const [dx, dy] of DIRS) {
            const nx = x + dx;
            const ny = y + dy;

            if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue;
            if (maze[ny][nx] === 1 || visited[ny][nx]) continue;

            visited[ny][nx] = true;
            queue.push([nx, ny, steps + 1]);
        }
    }

    return -1; // 无法到达
}
BFS 逐层扩散过程(数字 = 步数):

    0  →  1  →  2  →  3

          →  4  →  5  →  6(终点!)

为什么 DFS 不行? DFS 找到的第一条路径不一定是最近的,需要遍历所有路径取最小值,效率低。


例题 4:岛屿的最大面积

LeetCode 695 — 返回网格中最大的岛屿面积(连通 ‘1’ 的数量)

输入: grid = [
  [0,0,1,0,0],
  [0,1,1,1,0],
  [0,0,1,0,0],
  [1,1,0,0,0]
]
输出: 5  (中间十字形岛屿面积=5)
function maxAreaOfIsland(grid: number[][]): number {
    const rows = grid.length;
    if (rows === 0) return 0;
    const cols = grid[0].length;
    let maxArea = 0;

    function dfs(x: number, y: number): number {
        if (x < 0 || x >= cols || y < 0 || y >= rows) return 0;
        if (grid[y][x] === 0) return 0;

        grid[y][x] = 0; // 淹没

        // 返回当前格子(1) + 四个方向的面积之和
        return (
            1 +
            dfs(x + 1, y) +
            dfs(x - 1, y) +
            dfs(x, y + 1) +
            dfs(x, y - 1)
        );
    }

    for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
            if (grid[y][x] === 1) {
                maxArea = Math.max(maxArea, dfs(x, y));
            }
        }
    }

    return maxArea;
}

关键技巧: DFS 返回 int 类型,累加四方向面积 + 当前格子 1,天然得到连通块大小。


例题 5:传送门迷宫

迷宫中有若干传送门,进入传送门会瞬间传送到另一个传送门位置。求从起点到终点的最短路径。

输入: maze = [
  [0, 0, 0, 0],
  [0, 9, 0, 1],
  [1, 0, 0, 0],
  [0, 0, 9, 0]
]
起点: (0,0), 终点: (3,3)
0=空地, 1=墙, 9=传送门
图解:
    (0,0) → (0,1) → (0,2) → (1,2)
                ↓               ↓
            (1,1)[9] ←←←←←← 传送 ←←←← (3,2)[9]

            (2,1) → (2,2) → (2,3) → (3,3)终点

    不走传送门:约 9 步
    走传送门:  7 步(进入 9 后瞬移到另一个 9)

思路: 先记录所有传送门坐标,BFS 时遇到传送门,除了走四方向外,还能零代价传送到其他传送门

关键点

  1. 传送门预处理:遍历迷宫,把所有 9 的坐标收集到一个数组
  2. BFS 扩展:当前格子是传送门时,将其他所有传送门坐标入队(步数 +1)
  3. 传送门只能使用一次:传送到目标传送门后,从目标出发时不再传送(避免无限循环),标记已使用
function shortestPathWithPortal(maze: number[][]): number {
    const rows = maze.length;
    if (rows === 0) return -1;
    const cols = maze[0].length;

    const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];

    // 1. 收集所有传送门坐标
    const portals: [number, number][] = [];
    for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
            if (maze[y][x] === 9) {
                portals.push([x, y]);
            }
        }
    }

    // 起点或终点是墙
    if (maze[0][0] === 1 || maze[rows - 1][cols - 1] === 1) return -1;

    const queue: [number, number, number][] = [[0, 0, 0]]; // [x, y, steps]
    const visited: boolean[][] = Array.from({ length: rows }, () =>
        new Array(cols).fill(false)
    );
    visited[0][0] = true;

    // 标记传送门是否已使用(入队过一次就不再传送)
    let portalUsed = false;

    while (queue.length > 0) {
        const [x, y, steps] = queue.shift()!;

        // 到达终点
        if (x === cols - 1 && y === rows - 1) return steps;

        // 2. 当前格子是传送门,且传送门还没被用过
        if (maze[y][x] === 9 && !portalUsed) {
            portalUsed = true;
            for (const [px, py] of portals) {
                // 跳过自己
                if (px === x && py === y) continue;
                if (!visited[py][px]) {
                    visited[py][px] = true;
                    queue.push([px, py, steps + 1]); // 传送花费 1 步
                }
            }
        }

        // 3. 正常的四方向移动
        for (const [dx, dy] of DIRS) {
            const nx = x + dx;
            const ny = y + dy;

            if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue;
            if (maze[ny][nx] === 1 || visited[ny][nx]) continue; // 墙或已访问

            visited[ny][nx] = true;
            queue.push([nx, ny, steps + 1]);
        }
    }

    return -1; // 无法到达
}

// 测试
const maze = [
    [0, 0, 0, 0],
    [0, 9, 0, 1],
    [1, 0, 0, 0],
    [0, 0, 9, 0]
];
console.log(shortestPathWithPortal(maze)); // 输出: 7

传送门变体讨论

┌──────────────────────────────────────────────────────────────┐
│  变体                      处理方式                            │
├──────────────────────────────────────────────────────────────┤
│  传送门可反复使用           去掉 portalUsed 标记                 │
│  多组传送门(不同颜色配对)  用 Map<color, coords[]> 分组        │
│  传送花费 0 步              入队时 steps 不加 1                  │
│  传送门单次使用后消失        传送后把 maze[y][x] 改为 0           │
│  传送门是单向的              portals 存为 {from: [x,y], to: [x,y]}│
└──────────────────────────────────────────────────────────────┘

复杂度分析

项目 复杂度
时间 O(m×n + P²),P 为传送门数量(传送时遍历所有传送门)
空间 O(m×n)(visited 数组 + 队列)

优化: 如果传送门很多,可以建图将传送门之间的关系预处理为邻接表,BFS 时直接查表 O(1)。


常见变形与技巧

1. 八方向(含对角线)

const DIRS_8 = [
    [1, 0], [-1, 0], [0, 1], [0, -1],   // 四方向
    [1, 1], [1, -1], [-1, 1], [-1, -1],  // 对角线
];

2. 用 visited 数组 vs 原地修改

方式 优点 缺点
原地修改 grid[y][x]='0' 省空间 破坏原数据
visited[][] 布尔数组 保留原数据 额外 O(m×n) 空间

3. 避免递归栈溢出

网格极大(如 1000×1000 全是 ‘1’)时,DFS 递归可能栈溢出:

// 方案1:改用 BFS
// 方案2:手动栈模拟 DFS
function dfsStack(grid: string[][], startX: number, startY: number): void {
    const stack: [number, number][] = [[startX, startY]];
    while (stack.length > 0) {
        const [x, y] = stack.pop()!;
        // ... 四方向处理
    }
}

4. 多源 BFS(如“腐烂的橘子”)

// 多个起点同时入队,并行扩散
const queue: [number, number][] = [];
for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
        if (grid[y][x] === 2) { // 所有腐烂橘子入队
            queue.push([x, y]);
        }
    }
}
// 然后统一 BFS

5. 常见题型对照

LeetCode 题目 技巧
200 岛屿数量 DFS/BFS 淹没法
695 岛屿最大面积 DFS 返回面积
463 岛屿周长 遇水/边界 +1
130 被围绕的区域 边界 DFS + 标记法
994 腐烂的橘子 多源 BFS
490 迷宫 DFS + 一直走到撞墙
1091 二进制矩阵最短路径 BFS 最短路径
1254 封闭岛屿数量 先淹边界岛,再统计内部

总结

迷宫问题的核心就是:方向数组 + 越界检查 + 访问标记

  DFS(递归/栈)               BFS(队列)
  ┌──────────────┐           ┌──────────────┐
  │ 连通块计数    │           │ 最短路径      │
  │ 岛屿数量      │           │ 最少步数      │
  │ 岛屿面积      │           │ 迷宫最短路    │
  │ 路径存在性    │           │ 多源扩散      │
  └──────────────┘           └──────────────┘

 口诀:
   遇 '1' 先计数,DFS 淹没全岛
   求最短路用 BFS,逐层扩散天然短
   方向数组记心间,越界标记不能忘

关联题库

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