1293. Shortest Path in a Grid with Obstacles Elimination

You are given an m x n integer matrix grid where each cell is either 0 (empty) or 1 (obstacle). You can move up, down, left, or right from and to an empty cell in one step.

Return  the minimum number of steps to walk from the upper left corner (0, 0)  to the lower right corner  (m - 1, n - 1)  given that you can eliminate at most  k  obstacles. If it is not possible to find such walk return -1.

Example 1:

Input: grid = [[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k = 1
Output: 6
Explanation:
The shortest path without eliminating any obstacle is 10.
The shortest path with one obstacle elimination at position (3,2) is 6. Such path is (0,0) -> (0,1) -> (0,2) -> (1,2) -> (2,2) -> (3,2) -> (4,2).

Example 2:

Input: grid = [[0,1,1],[1,1,1],[1,0,0]], k = 1
Output: -1
Explanation: We need to eliminate at least two obstacles to find such a walk.

Constraints:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 40
  • 1 <= k <= m * n
  • grid[i][j] is either 0 or 1.
  • grid[0][0] == grid[m - 1][n - 1] == 0

这道题说是给了一个 m by n 的二维数组迷宫,只有两个数字0和1,其中0表示空地,即可以通行,1表示障碍物。在一般的迷宫题目中,障碍物是不能通行的,但是这里起始时给了k个清除障碍物的机会,障碍物被清除后就可以通行了,现在让找从左上角到右下角的最短的路径长度。可能有的童鞋看到是从左上角到右下角,是不是每次只要向右或者向下移动就能得到最短的路径,但其实不是的,由于障碍物的存在,所以本质上还是个迷宫,虽然有移除障碍物的机会,但有可能障碍物的个数会远大于移除的次数,所以回头路可能是无法避免的。迷宫遍历求最短路径,刷题老司机们应该都会立马条件反射般的想到应该是用广度优先遍历 Breadth-first Search。对于一般的 BFS 来说,状态就只包括位置信息,但这里的每一个状态不仅仅包括位置,还应该包括当前剩余的除障碍次数,这两个信息放在一起组成了状态,同时为了提高查找速度,博主将每个状态编码成字符串,放到 HashSet 以便查重,但不幸的是,这个写法最终还是超时了 Time Limit Exceeded,看来这道题的 OJ 对于时间的要求还是蛮苛刻的,不过也算对得起其 Hard 的身价。

既然传统的 BFS 写法超时了,就要想想怎么样才能进行优化,那么首先就得分析清楚到底的哪个部分比较耗时。目前由于每个状态包含了两个信息,位置和剩余的除障碍次数,就是说同一个位置可以访问多次,只要剩余的去除障碍次数不同就是不同的状态,但是这样的话,有可能会存在大量的重复计算,剩余的去除障碍次数少的路径一定步数更多,因为之前就到过这个位置了,所以剩余的去除障碍次数少的那条路径就可以直接砍掉,不用再往下计算浪费时间了,同时 BFS 保证了最短的距离,所以不用担心得不到正确的结果。这样的话就要知道每一个位置的最大剩余去除障碍次数,可以建立一个跟原数组相同大小的 visited 数组,初始化为 -1,但是 (0, 0) 位置初始化为k,因为起始时可以去除障碍k次。接下来就是 BFS 的实现了,新建一个队列 queue,然后把起始状态位置加次数放到一个数组里,排入队列中。

接下来进行 while 循环,注意是层序遍历的写法,里面用一个 for 循环,一次遍历完当前层的所有元素。在 for 循环中,取出队首元素,判断当前位置是否为终点,是的话返回结果 res,否则就遍历周围四个位置。计算出新位置的坐标,若越界了则跳过,然后计算新位置的剩余去除障碍次数,由于新位置可能是障碍,可以直接减去对应位置的数值,因为障碍是1,减去1正好表示用了一次去除障碍,而通路是0,减去0则没有变化。得到 newK 后,需要判断一下,若其小于0了,说明去除障碍次数不够用,需要跳过;或者 newK 小于等于 visited 数组的值,这种情况就是前面分析中说的需要剪枝的情况,同样跳过。然后用 newK 来更新 visited 数组中的值,并把新的状态加入到队列中。每层遍历结束后,记得结果 res 要自增1,参见代码如下:

解法一:

class Solution {
public:
    int shortestPath(vector<vector<int>>& grid, int k) {
        int res = 0, m = grid.size(), n = grid[0].size();
        vector<vector<int>> dirs{{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
        vector<vector<int>> visited(m, vector<int>(n, -1)); // The number of obstacles that we can still remove after walking through that cell
        visited[0][0] = k;
        queue<vector<int>> q;
        q.push({0, 0, k});
        while (!q.empty()) {
            for (int i = q.size(); i > 0; --i) {
                auto t = q.front(); q.pop();
                if (t[0] == m - 1 && t[1] == n - 1) return res;
                for (auto dir : dirs) {
                    int x = t[0] + dir[0], y = t[1] + dir[1];
                    if (x < 0 || x >= m || y < 0 || y >= n) continue;
                    int newK = t[2] - grid[x][y];
                    if (newK < 0 || newK <= visited[x][y]) continue;
                    visited[x][y] = newK;
                    q.push({x, y, newK});
                }
            }
            ++res;
        }
        return -1;
    }
};

或者我们也可以对 visitedd 数组的定义稍稍修改一下,反过来定义一下,表示到达该位置需要的移除的最少障碍数,则起始位置需要初始化0,其他位置则可以初始化为整型最大值。其余地方跟上面解法都很类似,不同点在于计算 newK,此时的 newK 应该是之前位置的剩余去除障碍次数加上新位置上的原数组中的值,然后判断条件也是和上面翻过来,当 newK 大于k,或者 visited 中对应的值小于等于 newK 时,需要跳过。否则用 newK 来更新 visited 数组中的值,并把新的状态加入到队列中。每层遍历结束后,记得结果 res 要自增1,参见代码如下:

解法二:

class Solution {
public:
    int shortestPath(vector<vector<int>>& grid, int k) {
        int res = 0, m = grid.size(), n = grid[0].size();
        vector<vector<int>> dirs{{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
        vector<vector<int>> visited(m, vector<int>(n, INT_MAX)); // Record the minimum obstacles removed to get to that position
        visited[0][0] = 0;
        queue<vector<int>> q;
        q.push({0, 0, 0});
        while (!q.empty()) {
            for (int i = q.size(); i > 0; --i) {
                auto t = q.front(); q.pop();
                if (t[0] == m - 1 && t[1] == n - 1) return res;
                for (auto dir : dirs) {
                    int x = t[0] + dir[0], y = t[1] + dir[1];
                    if (x < 0 || x >= m || y < 0 || y >= n) continue;
                    int newK = t[2] + grid[x][y];
                    if (newK > k || visited[x][y] <= newK) continue;
                    visited[x][y] = newK;
                    q.push({x, y, newK});
                }
            }
            ++res;
        }
        return -1;
    }
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/1293

类似题目:

Shortest Path to Get Food

参考资料:

https://leetcode.com/problems/shortest-path-in-a-grid-with-obstacles-elimination/

https://leetcode.com/problems/shortest-path-in-a-grid-with-obstacles-elimination/discuss/712992/C%2B%2B-or-BFS

https://leetcode.com/problems/shortest-path-in-a-grid-with-obstacles-elimination/discuss/1188835/Java-Clean-O(MNK)-Time-BFS-Solution-oror-comparing-with-Dijkstra's

LeetCode All in One 题目讲解汇总(持续更新中…)

(欢迎加入博主的知识星球,博主将及时答疑解惑,并分享刷题经验与总结,快快加入吧~)

知识星球 喜欢请点赞,疼爱请打赏❤️.

微信打赏

|

Venmo 打赏


—|—


转载请注明来源于 Grandyang 的博客 (grandyang.com),欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 grandyang@qq.com

💰


微信打赏


Venmo 打赏

(欢迎加入博主的知识星球,博主将及时答疑解惑,并分享刷题经验与总结,试运营期间前五十位可享受半价优惠~)

×

Help us with donation