[LeetCode] 1210. Minimum Moves to Reach Target with Rotations 穿過迷宮的最少移動次數



In an n*n grid, there is a snake that spans 2 cells and starts moving from the top left corner at (0, 0) and (0, 1). The grid has empty cells represented by zeros and blocked cells represented by ones. The snake wants to reach the lower right corner at (n-1, n-2) and (n-1, n-1).

In one move the snake can:

  • Move one cell to the right if there are no blocked cells there. This move keeps the horizontal/vertical position of the snake as it is.
  • Move down one cell if there are no blocked cells there. This move keeps the horizontal/vertical position of the snake as it is.
  • Rotate clockwise if it's in a horizontal position and the two cells under it are both empty. In that case the snake moves from (r, c) and (r, c+1) to (r, c) and (r+1, c).
  • Rotate counterclockwise if it's in a vertical position and the two cells to its right are both empty. In that case the snake moves from (r, c) and (r+1, c) to (r, c) and (r, c+1).

Return the minimum number of moves to reach the target.

If there is no way to reach the target, return -1.

Example 1:

Input: grid = [[0,0,0,0,0,1],
               [1,1,0,0,1,0],
               [0,0,0,0,1,1],
               [0,0,1,0,1,0],
               [0,1,1,0,0,0],
               [0,1,1,0,0,0]]
Output: 11
Explanation: One possible solution is [right, right, rotate clockwise, right, down, down, down, down, rotate counterclockwise, right, down].

Example 2:

Input: grid = [[0,0,1,1,1,1],
               [0,0,0,0,1,1],
               [1,1,0,0,0,1],
               [1,1,1,0,0,1],
               [1,1,1,0,0,1],
               [1,1,1,0,0,0]]
Output: 9

Constraints:

  • 2 <= n <= 100
  • 0 <= grid[i][j] <= 1
  • It is guaranteed that the snake starts at empty cells.

這道題給了個 n by n 的二維數組 grid,只有0和1兩個數字,說是有個占兩個位置 (0, 0) 和 (0, 1) 的蛇,問是否可以移動到 (n-1, n-2) 和 (n-1, n-1) 位置,能的話返回最少步數,不能的話返回 -1。注意蛇只能走數字0的地方,而且蛇只有豎直和水平兩種姿勢,只能有三種行動模式:第一種是向右移動,當蛇是水平姿勢時,向右移動一格(前提是右邊的格子為0),當蛇是豎直姿勢時,蛇頭蛇尾同時向右移動一格(前提是右邊的兩個格子為0)。第二種是向下移動,當蛇是水平姿勢時,蛇頭蛇尾同時向下移動一格(前提是下邊的兩個格子為0),當蛇是豎直姿勢時,向下移動一格(前提是下邊的格子為0)。第三種是旋轉,分為順時針旋轉和逆時針旋轉,當蛇是水平姿勢時,順時針旋轉 90 度變為豎直姿勢,蛇尾位置不變,當蛇是豎直姿勢時,逆時針旋轉 90 度變為水平姿勢,蛇尾位置不變。語言干說有些蒼白,好在題目中給了圖示,還十分貼心地畫出了一條萌萌的小紅蛇,畫風滿分💯。其實這道題本質上還是一道迷宮遍歷的題目,只不過不再是簡單的上下左右四個方向移動,而是變成更為復雜的移動方式,不然怎么對得起其 Hard 的身價。但核心本質還是沒變,既然是求最少步數,就是要用廣度優先遍歷 Breadth-first Search 來做。

先來想想,該如何表示蛇的某一個狀態,首先蛇是占兩個格子,分蛇頭和蛇尾,其次,蛇還有水平和豎直兩種姿勢。這里蛇的姿勢肯定要保存在狀態里,用0表示水平,1表示豎直,還有就是蛇的位置也要記錄,這里沒必要同時記錄兩個位置,而是只用蛇頭位置加上姿勢,三個變量組成的狀態即可。這里將初始狀態 {0, 1, 0} 放入隊列 queue 和 visited 集合中,其中 (0, 1) 是初始時蛇頭的位置,0表示水平姿勢。然后開始 BFS 的循環遍歷,由於需要統計最小步數,所以中間用個 for 循環來一次遍歷每一步可到達的所有位置。取出隊首狀態,若當前的蛇頭位置已經到達了 (n-1 ,n-1),且姿勢是水平,表示遍歷已經完成了,返回當前步數 res 即可。否則分為水平和豎直兩個姿勢分別進行處理,若是水平姿勢,則此時先判斷蛇是否能右移,只需要判斷右邊的位置是否為0,且沒有被訪問過,可以到達的話將下個狀態排入隊列中。再來看是否能下移和旋轉,這兩個操作的共同的點是需要蛇下方的兩個位置都是0,所以放一起判斷,若下移和旋轉后的狀態未出現過,則排入隊列中。對於豎直姿勢,也是類似的操作,先判斷蛇是否能下移,只需要判斷下邊的位置是否為0,且沒有被訪問過,可以到達的話將下個狀態排入隊列中。再來看是否能右移和旋轉,這兩個操作的共同的點是需要蛇右邊的兩個位置都是0,所以放一起判斷,若右移和旋轉后的狀態未出現過,則排入隊列中。最后別忘了步數 res 自增1,若 while 循環退出了,表示沒法到達目標點,返回 -1 即可,參見代碼如下:


解法一:

class Solution {
public:
    int minimumMoves(vector<vector<int>>& grid) {
        int res = 0, n = grid.size();
        set<vector<int>> visited{{0, 1, 0}};
        queue<vector<int>> q;
        q.push({0, 1, 0});
        while (!q.empty()) {
            for (int i = q.size(); i > 0; --i) {
                auto t = q.front(); q.pop();
                int x = t[0], y = t[1], dir = t[2];
                if (x == n - 1 && y == n - 1 && dir == 0) return res;
                if (dir == 0) { // horizontal
                    if (y + 1 < n && grid[x][y + 1] == 0 && !visited.count({x, y + 1, 0})) { // Move right
                        visited.insert({x, y + 1, 0});
                        q.push({x, y + 1, 0});
                    }
                    if (x + 1 < n && y > 0 && grid[x + 1][y - 1] == 0 && grid[x + 1][y] == 0) {
                        if (!visited.count({x + 1, y, 0})) { // Move down
                            visited.insert({x + 1, y, 0});
                            q.push({x + 1, y, 0});
                        }
                        if (!visited.count({x + 1, y - 1, 1})) { // Rote
                            visited.insert({x + 1, y - 1, 1});
                            q.push({x + 1, y - 1, 1});
                        }
                    }
                } else { // vertical
                    if (x + 1 < n && grid[x + 1][y] == 0 && !visited.count({x + 1, y, 1})) { // Move down
                        visited.insert({x + 1, y, 1});
                        q.push({x + 1, y, 1});
                    }
                    if (y + 1 < n && x > 0 && grid[x - 1][y + 1] == 0 && grid[x][y + 1] == 0) {
                        if (!visited.count({x, y + 1, 1})) { // Move right
                            visited.insert({x, y + 1, 1});
                            q.push({x, y + 1, 1});
                        }
                        if (!visited.count({x - 1, y + 1, 0})) { // Rotate
                            visited.insert({x - 1, y + 1, 0});  
                            q.push({x - 1, y + 1, 0});
                        }
                    }
                }
            }
            ++res;
        }
        return -1;
    }
};

上面的方法雖然能過 OJ,但也是險過,來想想到底哪個地方比較耗時。對於一般的 BFS,大多情況下都是用 HashSet 來記錄訪問過的狀態,由於這里的狀態由三個變量組成,所以組成數組后放到 TreeSet 中了。一種優化方法就是將三個變量 encode 成一個字符串,這樣就可以用 HashSet 了,查找就是常數級的復雜度了。還有一種方法就是直接利用 grid 數組來記錄蛇的姿勢,因為原來的 grid 數組只有0和1,只有一位,可以用第二位表示是否是豎直(通過'或'上2來改變狀態),第三位表示是否是水平(通過'或'上4來改變狀態)。隊列中還是保存位置和姿勢信息,但是這里稍微變一下,記錄蛇尾的位置,因為蛇尾在旋轉操作時不會改變,能稍微簡單一些。在 while 循環中,還是用個內部 for 循環進行層序遍歷,取出隊首狀態,若此時蛇尾已經到了 (n-1, n-2),直接返回步數 res 即可。那你可能會問,為啥此時不用判斷蛇的姿勢了呢?因為若此時蛇是豎直姿勢的話,蛇頭就越界了,這種非法狀態根本不會排入隊列中。結下來就是判斷當前狀態是否出現過了,由於蛇只能往右邊和下邊移動,所以很難走到之前的位置,唯一可能出現的重復狀態是姿勢,因為旋轉的時候蛇尾位置不變,所以這里只要判讀當前位置的姿勢是否出現過。由於之前說了使用第二位和第三位來分別記錄豎直和水平姿勢,所以這里判斷 dir,若是1(表示豎直),則'或'上數字2,若是0(表示水平),則'或'上數字4。取出對應位上的數字后判斷,若是1,則表示當前狀態已經處理過了,則跳過,否則就將對應位上的數字置為1。

接下來就要判斷能否移動或旋轉了,跟上面分姿勢討論不同的是,這里是直接判斷是否能進行移動或旋轉,而且分別放到一個子函數中,這樣更加清晰一些。對於 canGoDown 函數,若蛇是水平姿勢,判斷下面兩個位置的 grid 值是否越界,且最低位是否為0,因為第二三位可能不為0,所以不能直接判斷 grid 值是否為0,而是要'與'上1取出最低位。若蛇是豎直姿勢,判斷下邊一個位置是否越界,且最低位是否為0。對於 canGoRight 函數,若蛇是水平姿勢,判斷右邊一個位置是否越界,且最低位是否為0。若蛇是豎直姿勢,判斷右邊兩個位置的 grid 值是否越界,且最低位是否為0。對於 canRotate 函數,不管蛇是水平還是豎直姿勢,蛇尾的位置都不變,需要判斷蛇尾的右邊,下邊,和右下邊位置的 grid 值的最低位是否為0。若可以移動或者旋轉,則將目標狀態排入隊列 queue 中。最后別忘了步數 res 自增1,若 while 循環退出了,表示沒法到達目標點,返回 -1 即可,參見代碼如下:


解法二:

class Solution {
public:
    int minimumMoves(vector<vector<int>>& grid) {
        int res = 0, n = grid.size();
        queue<vector<int>> q;
        q.push({0, 0, 0}); // 0 is horizontal, 1 is vertial
        while (!q.empty()) {
            for (int i = q.size(); i > 0; --i) {
                auto t = q.front(); q.pop();
                int x = t[0], y = t[1], dir = t[2];
                if (x == n - 1 && y == n - 2) return res;
                if ((grid[x][y] & (dir ? 2 : 4)) != 0) continue;
                grid[x][y] |= (dir ? 2 : 4);
                if (canGoDown(grid, x, y, dir)) q.push({x + 1, y, dir});
                if (canGoRight(grid, x, y, dir)) q.push({x, y + 1, dir});
                if (canRotate(grid, x, y)) q.push({x, y, !dir});
            }
            ++res;
        }
        return -1;
    }
    bool canGoDown(vector<vector<int>>& grid, int x, int y, int dir) {
        int n = grid.size();
        if (dir == 0) return x + 1 < n && (grid[x + 1][y] & 1) == 0 && (grid[x + 1][y + 1] & 1) == 0;
        return x + 2 < n && (grid[x + 2][y] & 1) == 0;
    }
    bool canGoRight(vector<vector<int>>& grid, int x, int y, int dir) {
        int n = grid.size();
        if (dir == 0) return y + 2 < n && (grid[x][y + 2] & 1) == 0;
        return y + 1 < n && (grid[x][y + 1] & 1) == 0 && (grid[x + 1][y + 1] & 1) == 0;
    }
    bool canRotate(vector<vector<int>>& grid, int x, int y) {
        int n = grid.size();
        return x + 1 < n && y + 1 < n && (grid[x + 1][y] & 1) == 0 && (grid[x][y + 1] & 1) == 0 && (grid[x + 1][y + 1] & 1) == 0;
    }
};

Github 同步地址:

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


參考資料:

https://leetcode.com/problems/minimum-moves-to-reach-target-with-rotations/

https://leetcode.com/problems/minimum-moves-to-reach-target-with-rotations/discuss/392872/C%2B%2B-BFS

https://leetcode.com/problems/minimum-moves-to-reach-target-with-rotations/discuss/393511/JavaPython-3-25-and-17-liner-clean-BFS-codes-w-brief-explanation-and-analysis.


LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM