题目描述

✅ 505. 迷宫 II

题意分析

球选定方向后必须一直滚到墙前或边界才能停下,要求最终停在目标格,并使总移动格数最少。一次滚动可能走过不同数量的格子,因此需要按实际移动距离比较路线。

解法:线性 Dijkstra + 滚动模拟

核心思路

[!blue]

把每个停点看作图中的节点,一次完整滚动看作一条边,边权就是移动的格数。dist[x][y] 保存目前找到的到达该停点的最短距离,used[x][y] 表示这个距离已经确定,不会再被改小。

Dijkstra 每轮在线性扫描中找出尚未确定、且 dist 最小的停点 u。由于所有边权非负,这个距离可以直接确定:如果存在更短路线,它从已确定区域进入的第一个未确定点,早已得到不大于该路线长度的距离,也就应比 u 更早被选中,产生矛盾。

确定 u 后,向四个方向分别滚动到不能前进,累计移动步数 step。若到停点 v 的新距离 dist[u] + step 更小,就更新 dist[v]。滚动时只对最终停点更新距离,中途经过目标格不算到达。

起点距离设为 $0$,其他位置暂不可达。每轮至少确定一个新位置;找不到有限距离的候选时,剩余位置都无法到达,可以结束。最后目标距离仍为不可达标记就返回 -1,否则返回它的距离。

解题步骤

  1. 初始化距离表和定型表,将起点距离设为 $0$。
  2. 扫描全表,选择尚未定型的最小有限距离位置;没有候选就结束。
  3. 标记该位置已定型。每个方向都从这里出发,每合法移动一格就令 step 加一,直到下一格是墙或越界。
  4. 比较完整滚动形成的新距离,只在更短时更新停点。
  5. 返回终点距离,不可达时返回 -1。起点等于终点时初始距离已经是 $0$;贴墙产生的零步滚动不会改进自身距离。

代码实现

class Solution {
    // 节点是可停靠的格子,边权是滚动步数。
    public int shortestDistance(int[][] maze, int[] start, int[] destination) {
        int m = maze.length;
        int n = maze[0].length;
        int inf = Integer.MAX_VALUE / 4;
        int[][] dist = new int[m][n];
        boolean[][] used = new boolean[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dist[i][j] = inf;
            }
        }

        dist[start[0]][start[1]] = 0;

        int[] dx = new int[] {
            1,
            -1,
            0,
            0
        };
        int[] dy = new int[] {
            0,
            0,
            1,
            -1
        };

        for (int round = 0; round < m * n; round++) {
            int bestX = -1;
            int bestY = -1;
            // 每轮重新选择尚未定型的最小有限距离
            int bestDist = inf;

            for (int x = 0; x < m; x++) {
                for (int y = 0; y < n; y++) {
                    if (!used[x][y] && dist[x][y] < bestDist) {
                        bestDist = dist[x][y];
                        bestX = x;
                        bestY = y;
                    }
                }
            }

            // 找不到有限候选时结束,不从不可达状态继续松弛
            if (bestX == -1) {
                break;
            }

            used[bestX][bestY] = true;

            for (int d = 0; d < 4; d++) {
                int nx = bestX;
                int ny = bestY;
                // 边权是整次滚动经过的格数,不是一次操作
                int step = 0;

                while (true) {
                    int tx = nx + dx[d];
                    int ty = ny + dy[d];

                    if (tx < 0 || ty < 0 || tx >= m || ty >= n || maze[tx][ty] == 1) {
                        break;
                    }

                    nx = tx;
                    ny = ty;
                    step++;
                }

                int nd = dist[bestX][bestY] + step;

                // 只有真正变短才更新,零步自环不会产生改进
                if (nd < dist[nx][ny]) {
                    dist[nx][ny] = nd;
                }
            }
        }

        int answer = dist[destination[0]][destination[1]];

        if (answer >= inf) {
            return -1;
        }

        return answer;
    }
}
func shortestDistance(maze [][]int, start []int, destination []int) int {
    // 节点是可停靠的格子,边权是滚动步数。
    m := len(maze)
    n := len(maze[0])
    inf := m*n*(m+n) + 1
    dist := make([][]int, m)
    used := make([][]bool, m)
    for i := 0; i < m; i++ {
        dist[i] = make([]int, n)
        used[i] = make([]bool, n)
        for j := 0; j < n; j++ {
            dist[i][j] = inf
        }
    }
    dist[start[0]][start[1]] = 0

    dx := []int{
        1,
        -1,
        0,
        0,
    }
    dy := []int{
        0,
        0,
        1,
        -1,
    }
    for round := 0; round < m*n; round++ {
        bestX, bestY := -1, -1
        // 每轮重新选择尚未定型的最小有限距离
        best := inf
        for x := 0; x < m; x++ {
            for y := 0; y < n; y++ {
                if !used[x][y] && dist[x][y] < best {
                    best = dist[x][y]
                    bestX = x
                    bestY = y
                }
            }
        }
        // 找不到有限候选时结束,不从不可达状态继续松弛
        if bestX == -1 {
            break
        }
        used[bestX][bestY] = true

        for d := 0; d < 4; d++ {
            nx := bestX
            ny := bestY
            // 边权是整次滚动经过的格数,不是一次操作
            step := 0
            for {
                tx := nx + dx[d]
                ty := ny + dy[d]
                if tx < 0 || ty < 0 || tx >= m || ty >= n || maze[tx][ty] == 1 {
                    break
                }
                nx = tx
                ny = ty
                step++
            }
            next := dist[bestX][bestY] + step
            // 只有真正变短才更新,零步自环不会产生改进
            if next < dist[nx][ny] {
                dist[nx][ny] = next
            }
        }
    }

    answer := dist[destination[0]][destination[1]]
    if answer >= inf {
        return -1
    }
    return answer
}

复杂度分析

  • 时间复杂度:$O((mn)^2)$。最多进行 $mn$ 轮,每轮扫描 $mn$ 个位置选点;滚动模拟另需 $O(mn\max(m,n))$,不超过前者。
  • 空间复杂度:$O(mn)$,距离与定型表。

关键点总结

[!green]

  • 最少滚动次数与最少经过格数不是同一个目标。
  • 没有有限距离候选时停止,不对不可达状态做加法。

易错点总结

[!yellow]

  • 用滚动次数当边权,会忽略每次滚动长度的差异;应累加实际移动格数。
  • 沿普通相邻格建图,会允许中途停下。
  • 发现节点就固定距离,可能错过之后更短的路径。

相似题目

题目 难度 关联与区别
490. 迷宫 中等 只判断可达时DFS或BFS即可,本题每次滚动长度不同,需要加权最短路。
499. 迷宫 III 困难 原题进一步要求遇洞提前停,并在距离相同的路径中选字典序最小者。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/leetcode-505
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!