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/