1861. Rotating the Box
題目 / Problem
中文: 給定一個 m x n 的字元矩陣 boxGrid,代表一個盒子的側視圖。每個格子是下列三種之一:
- 石頭 '#'
- 固定障礙物 '*'
- 空格 '.'
將盒子順時針旋轉 90 度後,由於重力作用,石頭會往下掉,直到落在障礙物、其他石頭、或盒子底部之上。障礙物的位置不會被重力影響,旋轉產生的慣性也不影響石頭的水平位置。回傳旋轉後的 n x m 矩陣。
English: You are given an m x n character grid boxGrid representing a side-view of a box. Each cell is a stone '#', an obstacle '*', or empty '.'. The box is rotated 90 degrees clockwise; stones fall under gravity until they rest on an obstacle, another stone, or the floor. Obstacles do not move, and the rotation's inertia does not change stones' horizontal positions. Return the resulting n x m matrix.
Constraints / 限制:
- 1 <= m, n <= 500
- 每格只會是 '#'、'*' 或 '.'。
Worked example (Example 1):
Input: [["#",".","#"]]
Output: [["."],
["#"],
["#"]]
原本一行三格 # . #。順時針轉 90° 後變成一直行三列;上面那顆 # 因重力掉到底部,所以從上到下是 .、#、#。
名詞解釋 / Glossary
- Gravity simulation / 重力模擬:用程式模擬石頭往一個方向移動直到碰到障礙的過程。本題不需要一次只移動一格,可以直接算出最終落點。
- Two pointers / 雙指針:在同一個陣列上同時維護兩個索引,本題用一個「讀取指針
j」與一個「寫入指針write」。write永遠記錄「下一顆石頭應該停在哪裡」。 - In-place modification / 原地修改:直接在輸入矩陣上修改資料,不另開一份大陣列,節省空間。
- Clockwise rotation formula / 順時針旋轉公式:將
m x n的矩陣box順時針轉 90° 變成n x m的矩陣rot,對應關係是rot[i][j] = box[m-1-j][i]。可以記成「新矩陣的第 i 列、第 j 行」來自「舊矩陣的第 (m-1-j) 列、第 i 行」。 - Right-to-left scan / 由右往左掃描:因為旋轉後盒子的「下方」對應原本盒子每一列的「右側」,所以我們在原矩陣上把石頭推到右邊(等同旋轉後落到下方)。從右往左掃可以一次定位每顆石頭的最終位置。
- Sentinel / 邊界值:
write = n - 1是初始值,代表「最右邊那格還是空的、可以放石頭」。每次遇到*,就把write重置為j - 1,意思是「障礙物左邊那格才是新的可放位置」。
思路
最直觀的暴力法是:先把矩陣旋轉成 n x m,再對每一行從上到下模擬重力——對每顆石頭一格一格地往下推,直到撞到東西為止。但這樣每顆石頭最壞要走 O(n) 步,總共 O(m·n²),當 m, n = 500 時運算量可能達到 1.25 億,不算理想。關鍵觀察是:旋轉之後的「下方」就是旋轉前每一列的「右方」。所以我們可以先在原矩陣上把每一列的石頭往右推(這就等同旋轉後的下落),再做一次乾淨的旋轉。要在一列裡有效率地推石頭,最佳做法是由右往左掃描並維護一個「下一個可放石頭的位置 write」:碰到空格忽略;碰到障礙 * 把 write 設為 j - 1(障礙左邊那格才能放新石頭);碰到石頭 # 就把它從原位 j 搬到 write,然後 write--。每一列只掃一次,總時間 O(m·n),再加上旋轉本身也是 O(m·n),整體最優。最後用公式 rot[i][j] = box[m-1-j][i] 寫進結果矩陣即可。
The brute-force idea is to first rotate the grid into n x m, then push each stone down one cell at a time until it lands. That can cost O(m·n²) per stone, which is wasteful. The key insight is that "down" in the rotated box corresponds to "right" in the original box, so we can apply gravity on the original matrix by pushing stones to the right of each row, then perform a single clean rotation. To do the push efficiently per row, scan right-to-left with a write pointer write that always tracks "the next free slot a stone can land in". When we see '.', do nothing; when we see '*', the obstacle blocks everything to its right, so reset write = j - 1; when we see '#', move the stone from index j to index write, then decrement write. Each row is scanned once for O(m·n) total, and rotation via rot[i][j] = box[m-1-j][i] is another O(m·n), giving the optimal overall complexity.
逐步走查 / Walkthrough
以 Example 1 為例 / Using Example 1: boxGrid = [["#",".","#"]],m = 1, n = 3.
Phase 1 — 重力推到右邊 / Gravity to the right, 對 row 0 從右往左掃:
| 步驟 step | j |
box[0][j] |
動作 action | write 更新後 / after |
row 狀態 / state |
|---|---|---|---|---|---|
| 初始 init | — | — | write = n-1 = 2 |
2 | # . # |
| 1 | 2 | # |
把石頭放到 write = 2(同位置),write-- |
1 | # . # |
| 2 | 1 | . |
空格略過 / skip empty | 1 | # . # |
| 3 | 0 | # |
把 box[0][0] 移到 box[0][1]:原位設 .,目標設 #,write-- |
0 | . # # |
掃完後 row 0 變成 . # #。
Phase 2 — 順時針旋轉 / Clockwise rotation 成 n x m = 3 x 1 結果:用 result[i][j] = box[m-1-j][i] = box[0][i]。
i |
j |
box[0][i] |
result[i][j] |
|---|---|---|---|
| 0 | 0 | . |
. |
| 1 | 0 | # |
# |
| 2 | 0 | # |
# |
最終輸出 / Final output: [["."],["#"],["#"]] ✓
Solution — C
/*
* 演算法 / Algorithm:
* 1. 對每一列由右往左掃,用 write 指針把石頭推到右側 (= 旋轉後的下方)。
* Sweep each row right-to-left and push stones rightward (= "down" after rotation).
* 2. 用公式 rot[i][j] = box[m-1-j][i] 把推完石頭的矩陣順時針旋轉 90°。
* Rotate the gravity-applied matrix 90° clockwise via rot[i][j] = box[m-1-j][i].
* Time: O(m*n), Space: O(m*n) for the output (input is modified in place).
*/
// LeetCode 此題每個格子是長度 1 的字串,所以矩陣型別是 char***
// On LeetCode this problem uses char*** because each cell is a 1-char string
char*** rotateTheBox(
char*** box, // 原矩陣 / input grid, box[i][j] 是一個字串 / a string
int boxSize, // m,列數 / number of rows
int* boxColSize, // 每列的長度(都等於 n)/ row lengths (all = n)
int* returnSize, // 輸出指標:要寫入結果列數 / pointer to output row count
int** returnColumnSizes // 輸出指標:要寫入每列長度的陣列 / pointer to per-row length array
) {
int m = boxSize; // m 是原本的列數 / m = rows of input
int n = boxColSize[0]; // n 是每列的長度 / n = cols of input
// ---------- Phase 1: 把每列的石頭推到右邊 / push stones right per row ----------
for (int i = 0; i < m; i++) { // 對每一列獨立處理 / each row is independent
int write = n - 1; // 下一顆石頭該放的位置 / slot for next stone
for (int j = n - 1; j >= 0; j--) { // 由右往左掃 / scan right-to-left
char c = box[i][j][0]; // 取出該格字元 / read the cell's char
if (c == '*') { // 障礙擋住右側 / obstacle blocks the right
write = j - 1; // 新可放位置在障礙左邊 / next slot is left of it
} else if (c == '#') { // 是石頭就要往右掉 / a stone falls right
box[i][j][0] = '.'; // 原位變空 / vacate old spot
box[i][write][0] = '#'; // 寫入指針位置 / drop into write slot
write--; // 下一顆石頭再往左堆 / next stone stacks left
}
// c == '.' 不用做事 / empty cell needs no action
}
}
// ---------- Phase 2: 順時針旋轉 90° / rotate 90° clockwise ----------
*returnSize = n; // 旋轉後共 n 列 / output has n rows
// 配置長度為 n 的「每列長度」陣列 / allocate per-row size array of length n
*returnColumnSizes = (int*)malloc(sizeof(int) * n);
// 配置外層指標陣列:n 個 char** / allocate outer array of n row pointers
char*** result = (char***)malloc(sizeof(char**) * n);
for (int i = 0; i < n; i++) { // 填入每一列 / fill each output row
(*returnColumnSizes)[i] = m; // 每列有 m 個元素 / each row has m cells
result[i] = (char**)malloc(sizeof(char*) * m); // 配置這一列的格子陣列 / row of m strings
for (int j = 0; j < m; j++) {
// 每格分配 2 bytes:1 個字元 + 結尾 '