重新排列后的最大子矩阵 图解题解
这道题到底在问什么
- 输入
- matrix=[[0,0,1],[1,1,1],[1,0,1]]
- 输出
- 4
- 输入
- matrix=[[1,0,1,0,1]]
- 输出
- 3
最优解:为什么这么做
一句话答案:LeetCode 1727 重新排列后的最大子矩阵:列可整列重排,先把每列连续的 1 压成高度、每行从大到小排,站第 j 根柱子就有 j 乘高的矩形,逐根取最大,时间 O(m·n·log n)。
整列搬动之后,全 1 矩形最大能有多大
给一个 m 行 n 列、只装 0 和 1 的矩阵 matrix,你能把任意几列整列换位置,但一列里的上下格子必须原样跟着走。问重排之后全是 1 的矩形面积最大是多少。题面 matrix=[[0,0,1],[1,1,1],[1,0,1]] 把带 1 的列凑一起能框出 2 乘 2 全 1 块,面积 4;单行 matrix=[[1,0,1,0,1]] 把三个 1 挪到相邻,是一条长 3 的横条。
列能排出 n 的阶乘种顺序,挨个试排不动
n 列能排出 n 的阶乘那么多种顺序,枚举每种排法找矩形根本跑不完。但重排只挪列的左右位置,从没打乱过一列内部的上下关系——正因为列内锁死,一个格子向上连着几个 1,是重排前后都不变的量。抓住这个不变量,二维搬列就能拆成一行行分开算。
压高度:把每列连续的 1 累成一个数
第一步压高度,也就是把每列向上连续的 1 累成一个高度值:格子是 0 就记 0,是 1 就等于正上方那格的高度再加 1。压完,每列成了一根固定高度的柱子,重排列就是把这排柱子随意挪位置。
第二步在每行里数矩形。柱子能随便摆,最省的摆法就是把用到的柱子从高到低挨在一起。把这行高度从大到小排好,站在从左数第 j 根柱子上,它连同左边一共 j 根都不比它矮,恰好托起高等于它、宽等于 j 的矩形,面积就是 j 乘这根的高度,逐根取最大即可。
矩形的高,为什么取当前这根而不是最高那根
排完序,第 j 根柱子是左边这 j 根里最矮的一根。矩形要全是 1,高就被最矮那根卡死,矩形高只能取它自己、宽取 j。若图省事拿这片里的最高值当高,上半截会戳进矮柱子的 0 空当,矩形就不再全是 1。
拿题面 3 乘 3 矩阵逐格演一遍
先压高度。第 0 行在最顶上、没得接,原样 0、0、1。第 1 行都是 1,正上方高度 0、0、1 各加 1 得 1、1、2。第 2 行原始 1、0、1:头一个正上方 1 加 1 得 2,中间 0 记 0,末一个正上方 2 加 1 得 3,压成 2、0、3。
再一行行排序数矩形。第 0 行排成 1、0、0,第 1 根 1 乘 1 得 1,后两根空柱记 0。第 1 行排成 2、1、1,第 1 根 2 乘 1 得 2,第 2 根 1 乘 2 得 2,第 3 根 1 乘 3 得 3。第 2 行排成 3、2、0,第 1 根 3 乘 1 得 3,第 2 根 2 乘 2 得 4,第 3 根归零。三行各自最大是 1、3、4,取最大答案 4。
忘了清零、省掉排序,矩形就立不住
压高度扫一遍矩阵是 O(m 乘 n);数矩形每行给 n 个高度排序,单行 O(n 乘 log n),m 行合计 O(m 乘 n 乘 log n),排序是主导项。高度原地压在输入 matrix 上、没另开表,辅助只有常数变量,不计排序内部空间 O(1)。
两处一错就前功尽弃:压高度时忘了本身 0 的格子清零,高度虚高,量出的矩形混进 0;每行省掉排序、按原列序扫,宽度里可能夹根矮柱子,全 1 矩形立不住。边界也想清:全 0 矩阵面积 0;单行 1、0、1、0、1 排序后三根 1 相邻,得长 3 的横条。
▶ 动画逐步走查(共 29 步)——想跟着动画一帧帧对照就展开
- 3记住这两步:先把每格向上连续 1 的高度压出来,再把每一行的高度从大到小排队,站在第 j 根柱子上就有一个 高乘 j 的候选矩形,全程取最大。下面先从压高度开始。
- 4蓝格 = 0,绿格 = 1,逐格换算成向上高度这就是 3 行 3 列的原矩阵,蓝色是 0,绿色是 1。第一步,把每个格子换算成它「向上连续 1 的高度」,记成 h。规则很简单,格子本身是 0 就记 0;本身是 1,就看它正上方那个格子的 h 是几,再加 1。咱们从第 0 行开始,一行一行、从左到右地压。
- 5本身是 0,h = 0看格子 (0,0),它本身是 0,向上根本接不上 1,高度直接记 0。
- 6本身是 0,h = 0看格子 (0,1),它本身是 0,向上根本接不上 1,高度直接记 0。
- 7h = 1看格子 (0,2),它是 1,又在最顶上一行,上面没有别人了,向上连续的 1 只有它自己,高度记 1。
- 8h = 1看格子 (1,0),它是 1。看它正上方那个格子的高度是 0,那从这里往上连续的 1 就是上面那一摞再加自己,高度记 0 加 1,等于 1。
- 9h = 1看格子 (1,1),它是 1。看它正上方那个格子的高度是 0,那从这里往上连续的 1 就是上面那一摞再加自己,高度记 0 加 1,等于 1。
- 10h = 2看格子 (1,2),它是 1。看它正上方那个格子的高度是 1,那从这里往上连续的 1 就是上面那一摞再加自己,高度记 1 加 1,等于 2。
- 11h = 2看格子 (2,0),它是 1。看它正上方那个格子的高度是 1,那从这里往上连续的 1 就是上面那一摞再加自己,高度记 1 加 1,等于 2。
- 12本身是 0,h = 0看格子 (2,1),它本身是 0,向上根本接不上 1,高度直接记 0。
- 13h = 3看格子 (2,2),它是 1。看它正上方那个格子的高度是 2,那从这里往上连续的 1 就是上面那一摞再加自己,高度记 2 加 1,等于 3。
- 14h = 每格向上连续 1 的高度九个格子的高度都压好了。第 0 行是 0、0、1,第 1 行是 1、1、2,第 2 行是 2、0、3。这里的 3 就表示这个格子向上能连着三个 1。原来的二维矩阵,现在变成了三行「柱子高度」。接下来的重排和数矩形,全在每一行内部做。
- 15把每行高度看成一排柱子,列可换位就是柱子可任意排第二步数矩形。因为列可以随意换位置,把某一行的这排高度想成一排能自由挪动的柱子。要在里面找一块全 1 的大矩形,最聪明的排法就是从高到低排好、最高的靠最左。这样站在从左数第 j 根柱子上,它连同左边一共 j 根都不比它矮,正好托起一个 高等于它、宽等于 j 的矩形。咱们一行一行来,先看第 0 行。
- 16高度(原列序):0、0、1把第 0 行的三个高度 0、0、1 立成三根柱子,这是它们在原来列序下的样子。现在还乱着,先别急着数,下一帧把它们从高到低排队。
- 17排序后:1、0、0(最高靠左)把这排柱子从大到小排好队,变成 1、0、0,最高的挪到最左边。因为列本来就能随便换,这么排完全合法。排好之后有个好处:从左数任意第 j 根,它左边这一片全都不比它矮,能整整齐齐托起一个宽 j 的矩形。下面从最左那根开始,一根一根量矩形。
- 18候选面积 = 1 × 1 = 1站在从左数第 1 根柱子上,它高 1。它左边连它一共 1 根都不比它矮,于是能框出一个 高 1 乘 宽 1 的全 1 矩形,面积 1。这个 1 比之前的最大 0 还大,答案刷新成 1。
- 19空柱子,面积 0走到从左数第 2 根,它的高度是 0,是根空柱子,托不起任何全 1 矩形,面积 0,不刷新答案。继续看第 3 根。
- 20空柱子,面积 0走到从左数第 3 根,它的高度是 0,是根空柱子,托不起任何全 1 矩形,面积 0,不刷新答案。第 0 行到这里量完,本行最大是 1。
- 21高度(原列序):1、1、2把第 1 行的三个高度 1、1、2 立成三根柱子,这是它们在原来列序下的样子。现在还乱着,先别急着数,下一帧把它们从高到低排队。
- 22排序后:2、1、1(最高靠左)把这排柱子从大到小排好队,变成 2、1、1,最高的挪到最左边。因为列本来就能随便换,这么排完全合法。排好之后有个好处:从左数任意第 j 根,它左边这一片全都不比它矮,能整整齐齐托起一个宽 j 的矩形。下面从最左那根开始,一根一根量矩形。
- 23候选面积 = 2 × 1 = 2站在从左数第 1 根柱子上,它高 2。它左边连它一共 1 根都不比它矮,于是能框出一个 高 2 乘 宽 1 的全 1 矩形,面积 2。这个 2 比之前的最大 1 还大,答案刷新成 2。
- 24候选面积 = 1 × 2 = 2站在从左数第 2 根柱子上,它高 1。它左边连它一共 2 根都不比它矮,于是能框出一个 高 1 乘 宽 2 的全 1 矩形,面积 2。面积 2 没超过已有的最大 2,答案还是 2。
- 25候选面积 = 1 × 3 = 3站在从左数第 3 根柱子上,它高 1。它左边连它一共 3 根都不比它矮,于是能框出一个 高 1 乘 宽 3 的全 1 矩形,面积 3。这个 3 比之前的最大 2 还大,答案刷新成 3。
- 26高度(原列序):2、0、3把第 2 行的三个高度 2、0、3 立成三根柱子,这是它们在原来列序下的样子。现在还乱着,先别急着数,下一帧把它们从高到低排队。
- 27排序后:3、2、0(最高靠左)把这排柱子从大到小排好队,变成 3、2、0,最高的挪到最左边。因为列本来就能随便换,这么排完全合法。排好之后有个好处:从左数任意第 j 根,它左边这一片全都不比它矮,能整整齐齐托起一个宽 j 的矩形。下面从最左那根开始,一根一根量矩形。
- 28候选面积 = 3 × 1 = 3站在从左数第 1 根柱子上,它高 3。它左边连它一共 1 根都不比它矮,于是能框出一个 高 3 乘 宽 1 的全 1 矩形,面积 3。面积 3 没超过已有的最大 3,答案还是 3。
- 29候选面积 = 2 × 2 = 4站在从左数第 2 根柱子上,它高 2。它左边连它一共 2 根都不比它矮,于是能框出一个 高 2 乘 宽 2 的全 1 矩形,面积 4。这个 4 比之前的最大 3 还大,答案刷新成 4。
- 30空柱子,面积 0走到从左数第 3 根,它的高度是 0,是根空柱子,托不起任何全 1 矩形,面积 0,不刷新答案。第 2 行到这里量完,本行最大是 4。
- 31所有行、所有位置里最大的候选面积 = 4三行都量完了。第 0 行最大 1,第 1 行最大 3,第 2 行最大 4,取最大就是 4。这块 4,正是第 2 行排序成 3、2、0 后,站在第 2 根柱子上量出的 高 2 乘 宽 2。它跟题面示例给的 4 完全吻合。整套路子就是:先按列压高度,再让每行从大到小排、扫 高乘位置序号 取最大。
⚠️ 容易写错的地方
✗ 错:把「列可重排」理解成整块矩阵能任意打乱
✓ 对:只能整列搬动,一列里的上下关系不变
正因为列内不变,才能先按列压出向上高度,再靠排列凑相邻
✗ 错:压高度时忘了本身是 0 要清零
✓ 对:本身是 0 高度记 0,是 1 才接正上方加 1
一旦中间有 0,向上的连续 1 就被截断,高度必须从这里断开
✗ 错:每行不排序,直接按原列序数矩形
✓ 对:先把该行高度从大到小排,再扫
列能随便换,排成高到低才能让宽度里每根柱子都不比当前矮
✗ 错:矩形高度取成这一段的最大值
✓ 对:站在第 j 根柱子,矩形高就是这根柱子的高度
排序后第 j 根是这 j 根里最矮的,矩形要全 1,高度被它这根卡住
完整代码(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 largestSubmatrix(self, matrix: List[List[int]]) -> int:
for i in range(1, len(matrix)):
for j in range(len(matrix[0])):
if matrix[i][j]:
matrix[i][j] = matrix[i - 1][j] + 1
ans = 0
for row in matrix:
row.sort(reverse=True)
for j, v in enumerate(row, 1):
ans = max(ans, j * v)
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:
int largestSubmatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
for (int i = 1; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (matrix[i][j]) {
matrix[i][j] = matrix[i - 1][j] + 1;
}
}
}
int ans = 0;
for (auto& row : matrix) {
sort(row.rbegin(), row.rend());
for (int j = 0; j < n; ++j) {
ans = max(ans, (j + 1) * row[j]);
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int largestSubmatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
for (int i = 1; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (matrix[i][j] == 1) {
matrix[i][j] = matrix[i - 1][j] + 1;
}
}
}
int ans = 0;
for (int[] row : matrix) {
Arrays.sort(row);
for (int j = n - 1, k = 1; j >= 0 && row[j] > 0; --j, ++k) {
int s = row[j] * k;
ans = Math.max(ans, s);
}
}
return ans;
}
}复杂度
时间
O(m · n · log n)
m 是行数 n 是列数。压高度表把矩阵扫一遍是 O(m·n)。数矩形时,每一行要对 n 个高度排序,单行 O(n·log n),一共 m 行,合计 O(m·n·log n),排序是主导项
空间
不计排序 O(1);计排序 C++/Java O(log n)、Python O(n)
高度是原地压在输入 matrix 上,没另开高度表,辅助只用了答案等常数变量。排序内部开销:C++ 与 Java 递归栈约 O(log n),Python 的排序最坏 O(n)。按峰值,不计排序内部时是 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 重新排列后的最大子矩阵 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么把每一行的高度从大到小排一下,就能算出这行能贡献的最大矩形?+
因为题目允许整列任意重排,而压完高度后每一列就是一根固定高度的柱子,重排列等价于把这排柱子任意摆放。要在一排柱子里框一块全 1 的矩形,矩形的高等于所选那几根里最矮的一根,宽等于根数。既然能随便摆,最优摆法一定是把要用的柱子从高到低挨在一起。排好之后站在从左数第 j 根上,它连同左边一共 j 根都不比它矮,正好托起一个高等于它、宽等于 j 的矩形,逐根取最大就是这一行的最优解。
每行都排序,时间是 O(m 乘 n 乘 log n),还能更快吗?+
能。压出的高度都落在 0 到 m 之间,是有界的小整数,可以把比较排序换成计数排序——每行开一个长度 m 加 1 的桶,数一下各高度各出现几次,再从高到低摊开扫。单行排序就从 O(n 乘 log n) 降到 O(n),整体做到 O(m 乘 n)。面试里能点出「高度有界、可用计数排序」是个加分项。
为什么敢原地把输入矩阵改成高度表,不额外开一张空间?+
压高度是自上而下算的,每个格子只依赖正上方那个已经算好的高度,而下面的格子还没轮到、用不上当前格的原始 0 或 1,所以就地把 matrix 改写成高度不会破坏后续计算。这样除了排序内部那点开销,不用再开一张同样大小的表,空间是常数级。万一面试要求不许改动入参,另拷一份再压即可,量级不变。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 重新排列后的最大子矩阵 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。