1914. Cyclically Rotating a Grid
題目 / Problem
中文: 給定一個 m x n 的整數矩陣 grid,其中 m 和 n 都是偶數,以及一個整數 k。矩陣由若干「層」(layer) 組成,從外到內每層形成一個矩形環。每次旋轉時,每一層中的元素會被「逆時針方向相鄰元素」取代;將整個矩陣執行 k 次這樣的旋轉,回傳結果矩陣。
English: You are given an m x n integer matrix grid (both m and n are even) and an integer k. The matrix consists of concentric rectangular layers. In one rotation, every element of a layer moves into the place of its counter-clockwise neighbour. Perform k such rotations on every layer and return the resulting matrix.
Constraints
- 2 <= m, n <= 50,且 m、n 皆為偶數 / both even
- 1 <= grid[i][j] <= 5000
- 1 <= k <= 10^9 (k 可能很大 / k can be huge)
Example
Input: grid = [[40,10],[30,20]], k = 1
Output: [[10,20],[40,30]]
唯一的一層 [40,10,20,30] (順時針) 逆時針旋轉一格後變成 [10,20,30,40],寫回矩陣即得答案。
The single layer [40,10,20,30] (clockwise) rotates one step counter-clockwise to [10,20,30,40], which gives the answer when written back.
名詞解釋 / Glossary
- Layer / 層:矩陣中由外到內第
d個矩形邊框,範圍是row ∈ [d, m-1-d],col ∈ [d, n-1-d]的最外圈。一個m×n矩陣共有min(m,n)/2層。 / The d-th rectangular ring counted from the outside; there aremin(m,n)/2such rings. - Cyclic rotation / 循環旋轉:把序列當成一個首尾相接的環,每個元素往同一方向移動一格;最後一個會繞回到第一個。 / Treat a sequence as a ring; every element shifts one step in the same direction and the last wraps around to the first.
- Counter-clockwise / 逆時針:沿環移動時,左上角往左下角(再到右下、右上)的方向。本題中元素「占據逆時針方向相鄰元素的位置」。 / Direction opposite to a clock's hands: top-left → bottom-left → bottom-right → top-right.
- Modulo / 取餘 (
%):a % b是a除以b的餘數,常用於把任意整數壓進[0, b-1]範圍。本題用k % L把超大的k壓成「實際有效的旋轉次數」(因為轉L次等於沒轉)。 / The remainder ofa / b; we usek % Lbecause rotating a length-Lring byLpositions is the identity. - In-place / 原地修改:不額外複製整個矩陣,直接在輸入上改寫;只用一條長度為
L的暫存陣列存當前這一層。 / Mutate the input directly; we only allocate one temporary array per layer (length = layer perimeter). - Perimeter of a layer / 層的周長:對第
d層,長度為2 * ((m-2d) + (n-2d) - 2);最內層至少有 4 個元素(因 m, n ≥ 2 且為偶數)。 /2 * ((m-2d) + (n-2d) - 2); even the innermost ring has at least 4 elements.
思路
最直觀的做法是「轉一次就重排一次」,但 k 可達 10^9,逐次模擬會超時。關鍵觀察是:每一層都是獨立的一個環,環的長度 L 固定;旋轉 L 次會回到原狀,所以實際只需要旋轉 k % L 次。於是問題拆成兩步:先把每一層「攤平」成一個一維陣列 arr,再把陣列做一次循環位移後寫回原來的位置。攤平的順序我們選順時針(從左上角開始,沿上邊→右邊→下邊→左邊)。對應地,逆時針旋轉一格在這個順時針陣列上就相當於「左移一格」:新的 pos[i] 應該等於舊的 pos[(i+1) mod L]。所以旋轉 k 次後,寫回時用公式 new[i] = old[(i + k%L) mod L]。為了避免 k 比 L 大、或讀取/寫入時搞混順序,我們先把整層複製進 arr、把 k 對 L 取餘,然後用同樣的順時針順序遍歷一次寫回去——這樣讀寫位置一一對應,不會錯位。重複處理每一層即可,每個元素只被讀一次寫一次,效率最佳。
The naive approach—rotate one step at a time and repeat k times—is too slow because k can reach 10^9. The key insight is that each layer is an independent cyclic ring of fixed length L, and rotating a length-L ring by L is a no-op, so the effective number of rotations is k % L. With that in hand, processing one layer becomes a flatten-rotate-restore sandwich: linearise the layer into a 1-D buffer arr (we walk it clockwise: top row left→right, right column top→bottom, bottom row right→left, left column bottom→top), shift, then write back. A counter-clockwise step on the ring corresponds to a left shift in our clockwise array, so after k rotations the new value at position i is arr[(i + k%L) % L]. We traverse the layer in the same clockwise order on the way back, which guarantees every read/write maps to the correct cell. Loop this over all min(m,n)/2 layers and we are done. Each grid cell is touched a constant number of times, so the runtime is O(m·n).
逐步走查 / Walkthrough
範例 / Example: grid = [[40,10],[30,20]], k = 1.
只有一層,d = 0,top=0, bottom=1, left=0, right=1,周長 L = 2*(1) + 2*(1) = 4,kk = 1 % 4 = 1。
There is only one layer with top=0, bottom=1, left=0, right=1, perimeter L = 4, effective shift kk = 1.
Step 1 — Flatten clockwise / 順時針攤平
| idx | 來源座標 / source cell | 值 / value |
|---|---|---|
| 0 | grid[0][0] |
40 |
| 1 | grid[0][1] |
10 |
| 2 | grid[1][1] |
20 |
| 3 | grid[1][0] |
30 |
所以 arr = [40, 10, 20, 30]。
Step 2 — Compute the shifted index / 計算位移後的索引
寫回 pos[i] 時取 arr[(i + kk) % 4] = arr[(i + 1) % 4]。
| 寫回 idx i | 目標座標 / target | 取自 / read from | 寫入值 / value |
|---|---|---|---|
| 0 | grid[0][0] |
arr[1] |
10 |
| 1 | grid[0][1] |
arr[2] |
20 |
| 2 | grid[1][1] |
arr[3] |
30 |
| 3 | grid[1][0] |
arr[0] |
40 |
Step 3 — Result / 結果
grid = [[10, 20], [40, 30]] ✓ 與題目輸出一致 / matches the expected output.
Solution — C
// 演算法 / Algorithm:
// 1) 對每一層,從左上角依順時針順序把所有元素攤平成一維陣列 arr。
// For each layer, flatten the ring clockwise (top→right→bottom→left) into arr.
// 2) 因為轉 L 次回到原狀,實際旋轉次數是 kk = k % L。
// Effective rotation is kk = k % L since rotating L times is identity.
// 3) 用同樣的順時針順序寫回:new[i] = arr[(i + kk) % L]。
// Write back in the same clockwise order using new[i] = arr[(i + kk) % L].
#include <stdlib.h> // malloc / free,以便動態配置暫存陣列 / for malloc/free
int** rotateGrid(int** grid, int gridSize, int* gridColSize, int k,
int* returnSize, int** returnColumnSizes) {
int m = gridSize; // 矩陣列數 / number of rows
int n = gridColSize[0]; // 矩陣行數 / number of columns (every row same length)
// 層數 = min(m, n) / 2;m, n 皆為偶數,所以一定整除。
// Number of layers = min(m, n) / 2 (exact since m, n are even).
int layers = (m < n ? m : n) / 2;
for (int d = 0; d < layers; d++) {
// 第 d 層的四個邊界 / four boundaries of layer d
int top = d, bottom = m - 1 - d;
int left = d, right = n - 1 - d;
// 周長 = 兩條垂直邊 + 兩條水平邊;再扣掉重複算的 4 個角。
// Perimeter: 2*(height) + 2*(width) - 4, written here in expanded form.
int L = 2 * (bottom - top) + 2 * (right - left);
// 暫存這一層的元素;只配置長度 L,用完即釋放。
// Temporary buffer for this layer; freed at the end of the loop body.
int* arr = (int*)malloc(sizeof(int) * L);
// ---- 攤平:順時針把這一層複製到 arr ----
// ---- Flatten the ring clockwise into arr ----
int idx = 0;
// 上邊:由左到右 / top edge: left → right
for (int j = left; j <= right; j++) arr[idx++] = grid[top][j];
// 右邊:由上到下(跳過已加入的右上角)/ right edge: top+1 → bottom
for (int i = top + 1; i <= bottom; i++) arr[idx++] = grid[i][right];
// 下邊:由右到左(跳過已加入的右下角)/ bottom edge: right-1 → left
for (int j = right - 1; j >= left; j--) arr[idx++] = grid[bottom][j];
// 左邊:由下到上(跳過已加入的左下角與左上角)/ left edge: bottom-1 → top+1
for (int i = bottom - 1; i > top; i--) arr[idx++] = grid[i][left];
// 把超大的 k 壓成有效旋轉次數;否則迴圈會浪費時間。
// Reduce k modulo L; without this, large k would blow up the runtime.
int kk = k % L;
// ---- 寫回:同樣順時針走一遍,讀 arr[(i + kk) % L] ----
// ---- Restore: walk the same clockwise path, but read shifted indices ----
idx = 0;
for (int j = left; j <= right; j++)
grid[top][j] = arr[(idx++ + kk) % L]; // 上邊 / top edge
for (int i = top + 1; i <= bottom; i++)
grid[i][right] = arr[(idx++ + kk) % L]; // 右邊 / right edge
for (int j = right - 1; j >= left; j--)
grid[bottom][j] = arr[(idx++ + kk) % L]; // 下邊 / bottom edge
for (int i = bottom - 1; i > top; i--)
grid[i][left] = arr[(idx++ + kk) % L]; // 左邊 / left edge
free(arr); // 釋放這層的暫存記憶體 / release the per-layer buffer
}
// LeetCode 的 C 介面要求回傳列數與每列長度。
// The LeetCode C harness asks for the number of rows and the length of each row.
*returnSize = m;
*returnColumnSizes = (int*)malloc(sizeof(int) * m);
for (int i = 0; i < m; i++) (*returnColumnSizes)[i] = n;
return grid; // 我們是原地修改,所以可以直接回傳同一個指標 / in-place, return same ptr
}
Solution — C++
// 演算法與 C 版完全相同:對每層攤平 → 用 k % L 做循環位移 → 順時針寫回。
// Same algorithm as the C version: flatten each layer, shift by k % L, write back clockwise.
// 這裡用 std::vector 取代手動 malloc/free,讓程式更短也更安全。
// Here we use std::vector instead of manual malloc/free for safer, terser code.
#include <vector> // std::vector,自動管理記憶體的動態陣列 / dynamic array w/ auto memory mgmt
using std::vector;
class Solution {
public:
vector<vector<int>> rotateGrid(vector<vector<int>>& grid, int k) {
int m = grid.size(); // 列數 / row count
int n = grid[0].size(); // 行數 / column count
int layers = std::min(m, n) / 2; // 層數 / number of layers
for (int d = 0; d < layers; ++d) {
// 當前層的四個邊界 / four boundaries of layer d
int top = d, bottom = m - 1 - d;
int left = d, right = n - 1 - d;
int L = 2 * (bottom - top) + 2 * (right - left); // 周長 / perimeter
// vector<int> 是 STL 動態陣列;reserve 預先配置容量,避免多次擴容。
// vector<int> is the STL dynamic array; reserve preallocates capacity.
vector<int> arr;
arr.reserve(L);
// ---- 攤平:順時針把這一層 push 進 arr ----
// ---- Flatten the ring clockwise via push_back ----
for (int j = left; j <= right; ++j) arr.push_back(grid[top][j]);
for (int i = top + 1; i <= bottom; ++i) arr.push_back(grid[i][right]);
for (int j = right - 1; j >= left; --j) arr.push_back(grid[bottom][j]);
for (int i = bottom - 1; i > top; --i) arr.push_back(grid[i][left]);
int kk = k % L; // 有效旋轉次數 / effective shift
// ---- 順時針寫回,讀位移後的索引 ----
// ---- Walk the same clockwise path and read shifted positions ----
int idx = 0;
for (int j = left; j <= right; ++j)
grid[top][j] = arr[(idx++ + kk) % L];
for (int i = top + 1; i <= bottom; ++i)
grid[i][right] = arr[(idx++ + kk) % L];
for (int j = right - 1; j >= left; --j)
grid[bottom][j] = arr[(idx++ + kk) % L];
for (int i = bottom - 1; i > top; --i)
grid[i][left] = arr[(idx++ + kk) % L];
// 迴圈結束,arr 自動被解構釋放;不需要手動 free。
// arr is destroyed automatically at scope exit — no manual free required.
}
return grid; // 已就地修改,直接回傳 / mutated in place, return as-is
}
};
複雜度 / Complexity
- Time:
O(m · n)— 每個格子在攤平和寫回時各被存取一次,層的總周長加起來等於矩陣總格數;k % L把大k一次壓掉,不會再多花時間。 / Each cell is visited a constant number of times; the sum of all layer perimeters equalsm·n, andk % Lcollapses the hugekupfront. - Space:
O(max(m, n))— 暫存陣列arr一次只儲存一層,最外層的周長為2(m+n)-4,即O(m+n)。回傳的矩陣是原本的grid,不額外計算。 / The per-layer buffer holds at most the outermost perimeter,2(m+n)-4. The output reusesgrid, so it is not counted.
Pitfalls & Edge Cases
- 忘記取
k % L/ Forgettingk % L:k可達10^9,逐次模擬必 TLE;不同層L不同,要分別取餘。 / Without it, brute simulation TLEs; rememberLdiffers per layer, so computekkinside the loop. - 方向搞反 / Direction confusion:題目是「逆時針」旋轉,但我們用「順時針」攤平 + 「索引加
kk」來實現;若改成索引減kk,會變成順時針旋轉,結果錯誤。 / The puzzle rotates counter-clockwise, but we flatten clockwise; the equivalent operation is addingkkto the index. Subtracting would rotate the wrong way. - 角落重複 / Double-counting corners:四條邊接在一起時要避免把角落讀兩次。攤平迴圈把每一邊的起點向內縮一格(
top+1、right-1、bottom-1、條件i > top)就避開了。 / The four edge loops use offset start indices so each corner is touched exactly once. - 最內層的退化情形 / Degenerate innermost layer:當
m = n = 2時只有一層、L = 4,程式仍正常工作,因為四條邊長度都至少為 1。 / Even withm = n = 2the formulas giveL = 4and all loops execute correctly. - 回傳介面 (僅 C) / Return interface (C only):LeetCode 的 C 簽名要求填
*returnSize與配置*returnColumnSizes。漏掉任一個會在判題時 segfault。 / Forgetting to set*returnSizeor to allocate*returnColumnSizeswill crash the judge. - 不要複製整個矩陣 / Don't deep-copy the grid:既然每層之間互不影響,逐層原地改寫就足夠;否則會多花一份
O(m·n)空間。 / Layers are independent; mutate in place to save memory.