1. 程式人生 > 其它 >[LeetCode] 1293. Shortest Path in a Grid with Obstacles Elimination 網格中的最短路徑

[LeetCode] 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 題目講解彙總(持續更新中...)