题目描述

✅ 490. 迷宫

题意分析

球选择一个方向后,会一直滚到下一格是墙或越界的位置才停止,停下后才能重新选择方向。需要判断能否最终停在目标格;滚动过程中经过目标格,不代表能够停在那里。

解法:停点 BFS

核心思路

[!blue]

每次停下的位置决定了接下来能选择哪些滚动方向,因此把停点作为图中的节点,把一次完整滚动作为一条边。起点也是初始停点。从某个停点出发,沿四个方向分别模拟到不能继续移动,就得到它的全部后继状态。

用 BFS 遍历这张图:先将起点标记并入队,再不断取出停点、生成它的后继。相同停点的后续选择始终相同,所以第一次发现时就标记,之后无需重复搜索;贴墙方向滚动零步得到自身,也会被已有标记过滤。

滚动时始终先检查下一格,只有合法且为空地才向前移动。循环结束时,当前位置仍在空地上,而下一步已经受阻,恰好就是这次滚动的停点。中途经过的格子不进入队列,也不用于判断成功。

任意可行路线都能拆成这些完整滚动,BFS 会逐个扩展所有可达停点,所以找到终点就返回 true,队列耗尽仍未找到则返回 false。本题只判断可达性,一次滚动经过多少格不会影响搜索顺序的正确性。

解题步骤

  1. 创建访问表和队列,标记起点并入队。
  2. 取出队头,若等于终点则返回 true;起点等于终点也会在这里直接成功。
  3. 对四个方向分别调用滚动过程,得到最终停点。
  4. 停点尚未访问时,立即标记并入队。
  5. 队列为空后返回 false。

代码实现

class Solution {
    // 从某个停点出发有四个方向,每个方向需要模拟滚动直到下一步越界或遇墙。
    public boolean hasPath(int[][] maze, int[] start, int[] destination) {
        int m = maze.length;
        int n = maze[0].length;
        boolean[][] visited = new boolean[m][n];

        Queue<int[]> queue = new ArrayDeque<>();

        visited[start[0]][start[1]] = true;
        queue.offer(new int[] {
            start[0],
            start[1]
        });

        int[][] dirs = {
            {1, 0},
            {-1, 0},
            {0, 1},
            {0, -1},
        };

        while (!queue.isEmpty()) {
            int[] cur = queue.poll();

            if (cur[0] == destination[0] && cur[1] == destination[1]) {
                return true;
            }

            for (int[] dir : dirs) {
                // 整次滚动结束的位置才是可入队状态
                int[] next = roll(maze, cur[0], cur[1], dir);

                if (visited[next[0]][next[1]]) {
                    continue;
                }

                // 发现停点即标记,重复方向与零步自环不再展开
                visited[next[0]][next[1]] = true;
                queue.offer(next);
            }
        }

        return false;
    }

    private int[] roll(int[][] maze, int row, int col, int[] dir) {
        int m = maze.length;
        int n = maze[0].length;

        while (canMove(maze, row + dir[0], col + dir[1], m, n)) {
            row += dir[0];
            col += dir[1];
        }

        return new int[] {
            row,
            col
        };
    }

    private boolean canMove(int[][] maze, int row, int col, int m, int n) {
        return row >= 0 && row < m && col >= 0 && col < n && maze[row][col] == 0;
    }
}
func hasPath(maze [][]int, start []int, destination []int) bool {
    // 从某个停点出发有四个方向,每个方向需要模拟滚动直到下一步越界或遇墙。
    m, n := len(maze), len(maze[0])
    visited := make([][]bool, m)
    for i := 0; i < m; i++ {
        visited[i] = make([]bool, n)
    }

    queue := make([][2]int, 0)
    queue = append(queue, [2]int{
        start[0],
        start[1],
    })
    visited[start[0]][start[1]] = true

    dirs := [][2]int{
        {1, 0},
        {-1, 0},
        {0, 1},
        {0, -1},
    }
    roll := func(row, col int, dir [2]int) [2]int {
        for {
            nextRow := row + dir[0]
            nextCol := col + dir[1]
            if nextRow < 0 || nextRow >= m || nextCol < 0 || nextCol >= n {
                break
            }
            if maze[nextRow][nextCol] == 1 {
                break
            }

            row = nextRow
            col = nextCol
        }

        return [2]int{
            row,
            col,
        }
    }

    for head := 0; head < len(queue); head++ {
        cur := queue[head]
        if cur[0] == destination[0] && cur[1] == destination[1] {
            return true
        }

        for _, dir := range dirs {
            // 整次滚动结束的位置才是可入队状态
            next := roll(cur[0], cur[1], dir)
            if visited[next[0]][next[1]] {
                continue
            }

            // 发现停点即标记,重复方向与零步自环不再展开
            visited[next[0]][next[1]] = true
            queue = append(queue, next)
        }
    }

    return false
}

复杂度分析

  • 时间复杂度:$O(mn\max(m,n))$。矩阵有 $m$ 行、$n$ 列,至多展开 $mn$ 个停点,每个停点向四个方向滚动,单次最多扫描一行或一列。
  • 空间复杂度:$O(mn)$,访问表与队列。

关键点总结

[!green]

  • 状态是停点,不是每个经过的空格。
  • 贴墙方向得到自身,已有访问标记会过滤这条无用边。

易错点总结

[!yellow]

  • 把相邻空格直接入队,会允许球中途刹车。
  • 把途经终点当作成功,会接受无法停留的位置。
  • 滚动之前不检查下一格,会越过边界或停进墙里。

相似题目

题目 难度 关联与区别
505. 迷宫 II 中等 滚动到墙才停止的规则相同,原题还需最小化实际走过格子数,边权不再统一。
499. 迷宫 III 困难 原题遇洞可提前停止且要选最短字典序路径,本题只判断能否恰好停在目标。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/78897291
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!