← 題庫 / Archive
2026-05-06 Daily Medium ArrayTwo PointersMatrix

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 rotationn 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 個字元 + 結尾 '' / 2 bytes: 1 char + null terminator
            result[i][j] = (char*)malloc(sizeof(char) * 2);
            // 套用旋轉公式 / apply rotation formula:
            // rot[i][j] = box[m-1-j][i]
            result[i][j][0] = box[m - 1 - j][i][0];
            result[i][j][1] = '';                        // C 字串需要 null 結尾 / null-terminate
        }
    }
    return result;                                         // 交回結果 / return the rotated grid
}

Solution — C++

/*
 * 演算法 / Algorithm:
 * 1. 對每列由右往左掃,把石頭推到右邊 (旋轉後即落到下方)。
 *    For each row, sweep right-to-left and push stones to the right (== "down" after rotating).
 * 2. 以 rot[i][j] = box[m-1-j][i] 順時針旋轉成 n x m 結果。
 *    Rotate via rot[i][j] = box[m-1-j][i] into an n x m result.
 * Time: O(m*n). Space: O(m*n) for the output.
 */

class Solution {
public:
    // LeetCode 給的型別是 vector<vector<char>>,每格直接是 char
    // LeetCode signature uses vector<vector<char>>; each cell is a plain char
    vector<vector<char>> rotateTheBox(vector<vector<char>>& box) {
        int m = box.size();           // 列數 / number of rows
        int n = box[0].size();        // 行數 / number of columns

        // ---------- Phase 1: 每列把石頭推到右邊 / gravity-right per row ----------
        for (int i = 0; i < m; ++i) {              // 逐列處理 / process each row
            int write = n - 1;                     // 下一個可放石頭的位置 / next free slot
            for (int j = n - 1; j >= 0; --j) {     // 由右往左掃 / right-to-left scan
                if (box[i][j] == '*') {            // 障礙會擋住右側石頭 / obstacle blocks rightward
                    write = j - 1;                 // 新落點在障礙左邊一格 / new slot is left of obstacle
                } else if (box[i][j] == '#') {     // 石頭往右掉到 write 位置 / stone falls to write
                    box[i][j] = '.';               // 原位置清空 / clear original cell
                    box[i][write] = '#';           // 在 write 位置放下石頭 / place stone at write
                    --write;                       // 下顆石頭往左疊 / next stone stacks one to the left
                }
                // '.' 就什麼都不做 / nothing to do for empty cells
            }
        }

        // ---------- Phase 2: 順時針旋轉 / clockwise rotate ----------
        // 用「外圍 vector 大小 n、內層每個 vector 大小 m」的方式建好空矩陣
        // Build an n x m result vector pre-sized to the rotated dimensions
        vector<vector<char>> result(n, vector<char>(m));
        for (int i = 0; i < n; ++i) {              // 走每一個輸出位置 / iterate each output cell
            for (int j = 0; j < m; ++j) {
                // 旋轉公式:新 (i,j) 來自原 (m-1-j, i)
                // Rotation formula: new (i,j) comes from old (m-1-j, i)
                result[i][j] = box[m - 1 - j][i];
            }
        }
        return result;                             // 交回旋轉後矩陣 / return rotated grid
    }
};

複雜度 / Complexity

  • Time: O(m·n) — Phase 1 對每個格子最多看一次(每列從右掃到左),Phase 2 也只走過 n·m 個輸出格子;兩階段都是線性。Both phases visit each cell once: gravity scans every row left/right, then rotation writes each output cell exactly once.
  • Space: O(m·n) — 輸出矩陣本身需要 n·m 個格子;除此之外只用到 writeij 等常數變數。The output grid dominates; auxiliary memory is O(1) because we modify the input in place.

Pitfalls & Edge Cases

  • 掃描方向反了 / Scanning the wrong direction:如果由左往右掃,遇到第一顆石頭時還不知道它最終會落到哪,因為要等看到右側障礙或邊界。由右往左掃就能讓 write 從邊界開始倒數,邏輯一次到位。Going left-to-right forces you to look ahead for obstacles; right-to-left lets write count down from the wall and the logic stays one-pass.
  • 障礙重置 write / Resetting write at an obstacle:碰到 * 時必須把 write 設為 j - 1,不是 j。因為 * 那格本身是障礙,新石頭只能堆到障礙的「左邊」。Forgetting j - 1 would let stones overwrite the obstacle.
  • 就地交換時的同位置覆寫 / Same-cell write when stone is already in place:當石頭剛好就在 write 上(j == write),先 box[i][j] = '.'box[i][write] = '#' 結果仍正確,因為第二步把 '.' 覆寫回 '#'。This is correct as written; do not add an if (j != write) short-circuit that forgets the decrement.
  • 旋轉公式記錯 / Mis-remembering the rotation formula:必須是 rot[i][j] = box[m-1-j][i](順時針)。如果寫成 box[j][n-1-i] 那是逆時針,會輸出鏡像錯誤。Use rot[i][j] = box[m-1-j][i]; the swapped version gives a counter-clockwise rotation.
  • C 的記憶體配置 / C memory layout:每格是一個長度為 1 的字串,所以每格要 malloc(2)'X' + '\0'),別忘了結尾的 '\0'。Each cell needs 2 bytes including the null terminator; forgetting it causes UB on read.
  • returnColumnSizes 必填 / Must set returnColumnSizes:LeetCode 用它來解析每列長度,沒設 LeetCode 會回報錯誤。Forgetting to fill *returnColumnSizes causes the judge to print garbage or crash.