旋转盒子 图解题解
这道题到底在问什么
- 输入
- box=[["#",".","#"]]
- 输出
- [["."],["#"],["#"]]
- 输入
- box=[["#",".","*","."],["#","#","*","."]]
- 输出
- [["#","."],["#","#"],["*","*"],[".","."]]
最优解:为什么这么做
一句话答案:LeetCode 1861 旋转盒子:顺时针转 90 度后向右恰好变成向下,于是先让每行石头在原图向右靠拢再整体旋转,一遍矩阵模拟即可,时间 O(m·n)、空间 O(m·n)。
旋转加重力,最后要返回什么形状
给一个 m 行 n 列的字符箱子 box,井号是石头、星号是固定障碍物、点是空位。把整个箱子顺时针转 90 度,重力立刻生效,石头垂直下落,直到压在障碍物、别的石头或箱子底部才停,障碍物钉死不动。题面例子是 2 行 4 列,转完要返回 4 行 2 列——行列尺寸从 m 行 n 列换成 n 行 m 列,这点先记牢。
真把图转过来再一格格往下砸,累在哪
先真把矩阵转出来、再让每颗石头一格格往下砸,是最贴题意的走法:扫一遍网格,石头正下方是空位就挪它下去一格,一趟到不了底就再扫,直到整张图不再变。它能算对,可一颗石头最坏落 n 格、每趟又要扫全图,掉到底可能要 O(n) 趟,合起来奔着 O(m·n·max(m,n)) 去,反复扫到稳定的退出条件也容易写错。
凭什么旋转前就敢让石头先向右靠拢
绕开反复下落,靠的是把旋转和重力拆开看。顺时针转 90 度会把方向也一起转过去:原图里的向右,转完恰好对应新图里的向下。既然如此,与其转完再让石头往下掉,不如旋转前就在原图每行里让石头向右靠拢到位,再把靠好的图整体转过去,两种顺序落点完全一样。重力就这样被提前化成每行向右滑这件一维小事,剩下的旋转只是纯粹搬格子。
每行怎么靠拢,障碍物把它切成几段
障碍物是死挡板,把一行切成几段互不相通的区间,石头只能在自己那段里滑、穿不过障碍物到隔壁段。于是从这行最右端往左扫,把遇到的空位记成落脚点:扫到障碍物,攒的落脚点全部清空,因为它右边的空位帮不了左边的石头;扫到石头,右侧同段还留着落脚点就滑过去占最靠右那个、腾出的空位再补回,同段没有则不动。参考代码用一个队列 deque 存空位下标、从队头取最靠右的;换个写指针从右往左原地摆放也一样。
题面那个 2 行 4 列箱子,两步各走出什么
拿题面的 box=[["#",".","*","."],["#","#","*","."]] 走两步。先靠拢:行 0 = ["#",".","*","."],障碍物在第 2 列,把这行切成第 0-1 列与第 3 列两段;左段第 0 列的石头向右滑一格到第 1 列,行 0 成 [".","#","*","."]。行 1 = ["#","#","*","."] 的两颗石头本就贴着障碍物、无处可滑,整行不动。
再旋转:顺时针转 90 度,底行行 1 竖成结果最左列,顶行行 0 竖成最右列。左列自上而下是石头、石头、障碍物、空位;右列是空位、石头、障碍物、空位。两列并起来就是 [["#","."],["#","#"],["*","*"],[".","."]],和题面答案一致。
O(m·n) 之外,单行和空行别漏想
每个格子在旋转拷贝时搬一次、靠拢时入队出队常数次,时间 O(m·n),至少得读遍所有格子,已是下界;答案矩阵占 O(m·n),另加存空位下标的队列 O(max(m,n))。只有一行也不特殊,靠拢在这行内做完再竖过来;整行没石头就只旋转、不移动;石头贴着右墙或障碍物、同段无落脚点就留在原地。真正坑人的是结果尺寸:转完是 n 行 m 列而非 m 行 n 列,新数组得按行列互换后的形状开,沿用原尺寸会越界。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记牢这两步:先让每行石头向右靠拢、遇障碍物断开,再把整张图顺时针旋转。下面每一帧都在套这两步,先从第一行的靠拢开始。
- 4红 = 障碍物(不动),蓝底 = 待处理石头这就是 2 行 4 列的箱子,井号是石头,星号是障碍物,点是空位。咱们先一行一行让石头向右靠拢。右边面板记这一行右侧有哪些空位能落脚,现在从行 0 开始,从最右边往左扫。
- 5记下一个可落脚的空位行 0 扫到第 3 列,是空位。把它记进右边面板,当作后面石头可以滑过来的落脚点。现在这一行右侧能落脚的空位有 1 个。
- 6障碍物挡住去路,落脚点清空行 0 扫到第 2 列,是障碍物星号。石头穿不过障碍物,所以障碍物右边攒下的落脚点在这里全部作废,面板清空。障碍物左边的石头只能落在障碍物这一侧,和右边互不相通。
- 7记下一个可落脚的空位行 0 扫到第 1 列,是空位。把它记进右边面板,当作后面石头可以滑过来的落脚点。现在这一行右侧能落脚的空位有 1 个。
- 8右边第 1 列有空位,石头滑过去行 0 扫到第 0 列,是一颗石头。它右边最近的空位在第 1 列。重力让它向右滑,它要从第 0 列滚到第 1 列去。
- 9第 0 列空出来,成为新落脚点石头落在第 1 列,原来的第 0 列空了出来。这个新腾出的空位接着记进面板,留给它左边可能还有的石头用。
- 10本行:. # * .行 0 靠拢完成,石头都贴到了障碍物左边。原来它散在第 0 列,现在滑到了第 1 列,紧挨着障碍物。接着处理行 1。
- 11记下一个可落脚的空位行 1 扫到第 3 列,是空位。把它记进右边面板,当作后面石头可以滑过来的落脚点。现在这一行右侧能落脚的空位有 1 个。
- 12障碍物挡住去路,落脚点清空行 1 扫到第 2 列,是障碍物星号。石头穿不过障碍物,所以障碍物右边攒下的落脚点在这里全部作废,面板清空。障碍物左边的石头只能落在障碍物这一侧,和右边互不相通。
- 13右侧没有空位,石头原地不动行 1 扫到第 1 列,是一颗石头,它右边马上就是障碍物,同一段里没有可落脚的空位,所以它已经靠到位,原地不动。
- 14右侧没有空位,石头原地不动行 1 扫到第 0 列,是一颗石头,它右边同一段内已被第 1 列石头占住,再往右被障碍物截断,没有可用的落脚点,它已经靠到位,原地不动。
- 15本行:# # * .行 1 靠拢完成。这一行的两颗石头本来就贴着障碍物,障碍物左侧这一段没有空位;第 3 列虽然是空位,却被障碍物隔开,给不了左侧的石头用,所以两颗石头原地没动。两行都靠拢好了。
- 16重力已在旋转前先「模拟」到位两行的石头都向右靠拢好了。为什么可以在旋转前就这么做?因为顺时针转 90 度之后,原来的向右恰好变成向下,旋转后石头往下掉,等价于旋转前让它们先向右滑到位。现在只剩纯粹的旋转这一步了。
- 17底行将变左列,顶行将变右列开始旋转。顺时针转 90 度有个好记的规律:原来的底行,会变成结果的左列;原来的顶行,会变成结果的右列。这里先把底行,也就是行 1 的这四个格子高亮出来,它们待会儿会竖着排到结果的最左边一列。
- 18放入 石头(井号)底行从左往右第 0 个是石头(井号),顺时针转过去,它落到结果左列从上往下第 0 行。继续放下一个。
- 19放入 石头(井号)底行从左往右第 1 个是石头(井号),顺时针转过去,它落到结果左列从上往下第 1 行。继续放下一个。
- 20放入 障碍物(星号)底行从左往右第 2 个是障碍物(星号),顺时针转过去,它落到结果左列从上往下第 2 行。继续放下一个。
- 21放入 空位(点)底行从左往右第 3 个是空位(点),顺时针转过去,它落到结果左列从上往下第 3 行。结果的左列这就填满了。
- 22左列 = 底行竖过来结果的左列填好了,从上到下是石头、石头、障碍、空,正是原来底行从左到右的顺序竖过来。接着把顶行,也就是行 0,放到结果的右列。
- 23放入 空位(点)顶行从左往右第 0 个是空位(点),顺时针转过去,它落到结果右列从上往下第 0 行。继续放下一个。
- 24放入 石头(井号)顶行从左往右第 1 个是石头(井号),顺时针转过去,它落到结果右列从上往下第 1 行。继续放下一个。
- 25放入 障碍物(星号)顶行从左往右第 2 个是障碍物(星号),顺时针转过去,它落到结果右列从上往下第 2 行。继续放下一个。
- 26放入 空位(点)顶行从左往右第 3 个是空位(点),顺时针转过去,它落到结果右列从上往下第 3 行。结果的右列也填满了。
- 27答案 = 每行靠拢 + 顺时针旋转整张图转完了,结果是 4 行 2 列。回看全程,咱们没有真的去转图,而是先让每行石头向右靠拢模拟掉落,再按顺时针把底行变左列、顶行变右列排好。这个结果和题目样例 2 给的答案一模一样。
⚠️ 容易写错的地方
✗ 错:让石头穿过障碍物继续下落
✓ 对:障碍物星号是死挡板,石头只能停在它这一侧
扫到障碍物必须把攒下的落脚点全部清空,障碍物两侧的空位不能通用
✗ 错:搞反旋转后重力的方向
✓ 对:顺时针转 90 度后,原来的向右变成向下
正因如此才能在旋转前先让每行向右靠拢来模拟下落,方向记反结果就全错
✗ 错:把结果矩阵的行列尺寸写反
✓ 对:原来 m 行 n 列,旋转后是 n 行 m 列
行列互换了,先开对 n 行 m 列的新数组,再往里填,别沿用原尺寸
✗ 错:原地在同一个矩阵上转
✓ 对:旋转后行列尺寸变了,要另开新数组
除非矩阵是正方形,否则没法原地旋转,老老实实新建 n 行 m 列的 ans
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def rotateTheBox(self, boxGrid: List[List[str]]) -> List[List[str]]:
m, n = len(boxGrid), len(boxGrid[0])
ans = [[None] * m for _ in range(n)]
for i in range(m):
for j in range(n):
ans[j][m - i - 1] = boxGrid[i][j]
for j in range(m):
q = deque()
for i in range(n - 1, -1, -1):
if ans[i][j] == "*":
q.clear()
elif ans[i][j] == ".":
q.append(i)
elif q:
ans[q.popleft()][j] = "#"
ans[i][j] = "."
q.append(i)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<char>> rotateTheBox(vector<vector<char>>& boxGrid) {
int m = boxGrid.size(), n = boxGrid[0].size();
vector<vector<char>> ans(n, vector<char>(m));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
ans[j][m - i - 1] = boxGrid[i][j];
}
}
for (int j = 0; j < m; ++j) {
queue<int> q;
for (int i = n - 1; ~i; --i) {
if (ans[i][j] == '*') {
queue<int> t;
swap(t, q);
} else if (ans[i][j] == '.') {
q.push(i);
} else if (!q.empty()) {
ans[q.front()][j] = '#';
q.pop();
ans[i][j] = '.';
q.push(i);
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public char[][] rotateTheBox(char[][] boxGrid) {
int m = boxGrid.length, n = boxGrid[0].length;
char[][] ans = new char[n][m];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
ans[j][m - i - 1] = boxGrid[i][j];
}
}
for (int j = 0; j < m; ++j) {
Deque<Integer> q = new ArrayDeque<>();
for (int i = n - 1; i >= 0; --i) {
if (ans[i][j] == '*') {
q.clear();
} else if (ans[i][j] == '.') {
q.offer(i);
} else if (!q.isEmpty()) {
ans[q.pollFirst()][j] = '#';
ans[i][j] = '.';
q.offer(i);
}
}
}
return ans;
}
}复杂度
时间
O(m·n)
m 是原矩阵行数,n 是列数。旋转拷贝把每个格子搬一次是 O(m·n);之后逐列从下往上扫,每个格子只入队或出队常数次,也是 O(m·n)。合起来仍是 O(m·n),必须至少读一遍所有格子,这已是下界
空间
O(m·n)
要新建并返回一个 n 行 m 列的答案矩阵,这是题目要求的输出,占 O(m·n)。除答案之外只多用一个队列存空位下标,额外空间是 O(max(m,n));若不计必须返回的答案矩阵,额外空间就是 O(max(m,n))
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 旋转盒子 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么先靠拢再旋转,和先旋转再让石头下落,结果会一样?+
因为旋转把方向也一起带转了。顺时针转 90 度后,原图里的向右这个方向,恰好对应到新图里的向下。参考代码先把图转过去,再让石头在新图里沿着向下掉;先靠拢的做法是不转图、先让石头在原图里沿着向右滑到位,再把已经靠好的图整体转过去。同一批石头、同样的相对位置、同样被障碍物挡着,只是滑和转的先后换了个个,每颗石头最终落在哪个格子完全相同,所以两种顺序等价。
每行靠拢那一步,能不能不开队列、用两个指针原地做?+
能。对每一行从右往左扫,维护一个写指针,起初指向这一行最右的可落位置。往左遇到障碍物,就把写指针跳到障碍物左边一格重新起算;遇到空位不用管;遇到石头,就把它写到写指针处、写指针再左移一格,石头本来就在写指针处就相当于没动。一趟扫下来,石头都紧贴右侧或障碍物排好,只用了常数个额外变量,不必开队列。用队列只是把空位显式记下来更好讲清。
这题的时间复杂度还有没有可能压得更低?+
没有。答案矩阵有 m 乘 n 个格子,你至少得把每个输入格子读一遍、每个输出格子写一遍,O(m·n) 就是这道题的下界。旋转拷贝和逐列扫落都恰好是这个量级,已经贴着下界,再优化只能抠常数,量级降不下来。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 旋转盒子 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。