问题描述
给定一个 N 行 M 列的二维矩阵,定义子矩阵的「稳定度」为该子矩阵内最大值与最小值的差值(即 f(m) = max(m) - min(m))。要求在矩阵中找到一个稳定度不超过给定限制 limit 的子矩阵,且该子矩阵的面积(元素个数)尽可能大。
核心约束:子矩阵必须由连续的行和连续的列组成,不能跳过任意一行或一列。
输入输出格式
- 输入:第一行输入两个整数 N、M 表示矩阵大小;接下来 N 行每行输入 M 个整数描述矩阵;最后一行输入整数 limit 表示稳定度限制。
- 输出:输出满足条件的子矩阵的最大面积。
样例输入
3 4
2 0 7 9
0 6 9 7
8 4 6 4
8
样例输出
6
解释:满足条件的最大子矩阵为 2 行 3 列(如第 1-2 行、第 1-3 列),元素为 [[6,9,7],[4,6,4]],最大值 9,最小值 4,稳定度 5 ≤ 8,面积 2×3=6。
一、暴力解法:思路与局限性
1. 暴力思路
枚举所有可能的子矩阵,计算每个子矩阵的稳定度,筛选出稳定度 ≤ limit 的子矩阵,记录最大面积。具体步骤:
- 枚举子矩阵的所有可能「左上角」和「右下角」坐标,确定子矩阵的范围;
- 遍历子矩阵内所有元素,计算最大值和最小值,判断稳定度是否满足条件;
- 跟踪所有满足条件的子矩阵,更新最大面积。
2. 局限性
- 时间复杂度极高:枚举子矩阵的左上角(N×M 种)和右下角(N×M 种),共 O (N²M²) 种组合;每个子矩阵计算最值需 O (面积) 时间,最坏情况下总复杂度为 O (N³M³);
- 实用性差:当 N、M 均为 100 时,操作数已达 1e12,完全无法在合理时间内运行。
因此,暴力解法仅适用于极小规模矩阵,必须通过优化降低时间复杂度。
二、高效解法:固定边界 + 降维 + 滑动窗口
核心思想
通过「降维」将二维矩阵问题转化为一维区间问题,再用「滑动窗口 + 单调队列」优化最值查询,最终将时间复杂度降至 O (N²M),可高效处理 N、M 为 200 左右的中等规模矩阵。
分步详解
步骤 1:固定上下边界,压缩列维度
将二维矩阵转化为一维数组的关键:
- 固定子矩阵的「上边界」
top和「下边界」bottom(0 ≤ top ≤ bottom < N); - 对每一列 j,计算从
top到bottom行的「列最值」:maxCol[j]:第 j 列在[top, bottom]范围内的最大值;minCol[j]:第 j 列在[top, bottom]范围内的最小值;
- 此时,原二维子矩阵的稳定度等价于「
maxCol和minCol组成的一维数组」中某个区间的「最大 max - 最小 min」。
例:若 top=1、bottom=2(样例矩阵的第 2-3 行),则:
maxCol = [max(0,8), max(6,4), max(9,6), max(7,4)] = [8,6,9,7];minCol = [min(0,8), min(6,4), min(9,6), min(7,4)] = [0,4,6,4]。
步骤 2:滑动窗口找最长有效区间
对压缩后的 maxCol 和 minCol,需找到最长的区间 [left, right],满足:max(maxCol[left..right]) - min(minCol[left..right]) ≤ limit
该区间的长度即为子矩阵的宽度,乘以高度(bottom - top + 1)就是当前上下边界下的最大子矩阵面积。
滑动窗口 + 单调队列优化:
- 用两个单调队列维护区间最值,确保窗口移动时能 O (1) 获取最值:
maxQueue:单调递减队列,队首元素为当前窗口maxCol的最大值索引;minQueue:单调递增队列,队首元素为当前窗口minCol的最小值索引;
- 窗口右边界
right从 0 到 M-1 移动,左边界left仅在窗口无效时右移,确保每个元素入队出队各一次,时间复杂度 O (M)。
步骤 3:枚举所有上下边界,更新全局最大面积
遍历所有可能的 top 和 bottom 组合(共 O (N²) 种),重复步骤 1 和 2,记录全局最大面积。
关键优化点
- 列最值增量更新:当
bottom向下扩展一行时,无需重新计算整列最值,仅需用当前列最值与新行元素比较(maxCol[j] = max(maxCol[j], matrix[bottom][j])),时间复杂度 O (M); - 滑动窗口剪枝:一旦窗口的
max - min > limit,立即移动左边界,避免无效计算; - 单调队列维护最值:解决滑动窗口中「动态最值查询」的核心,将每次查询从 O (M) 降至 O (1)。
三、完整代码实现(Java)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt(); // 矩阵行数
int M = sc.nextInt(); // 矩阵列数
int[][] matrix = new int[N][M];
// 读取矩阵
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
matrix[i][j] = sc.nextInt();
}
}
int limit = sc.nextInt();
sc.close();
int maxArea = 0;
// 步骤1:枚举所有上边界 top
for (int top = 0; top < N; top++) {
// 初始化列最值数组(当前上边界top,下边界从top开始)
int[] maxCol = new int[M];
int[] minCol = new int[M];
for (int j = 0; j < M; j++) {
maxCol[j] = matrix[top][j];
minCol[j] = matrix[top][j];
}
// 步骤2:枚举所有下边界 bottom(从top向下扩展)
for (int bottom = top; bottom < N; bottom++) {
// 增量更新列最值(bottom > top时才需要更新)
if (bottom > top) {
for (int j = 0; j < M; j++) {
maxCol[j] = Math.max(maxCol[j], matrix[bottom][j]);
minCol[j] = Math.min(minCol[j], matrix[bottom][j]);
}
}
// 步骤3:滑动窗口找当前列最值数组的最长有效区间
Deque<Integer> maxQueue = new LinkedList<>(); // 存储maxCol的索引,单调递减
Deque<Integer> minQueue = new LinkedList<>(); // 存储minCol的索引,单调递增
int left = 0; // 滑动窗口左边界
int currentMaxWidth = 0; // 当前窗口最大宽度
for (int right = 0; right < M; right++) {
// 维护maxQueue:移除所有小于当前maxCol[right]的元素,确保队列递减
while (!maxQueue.isEmpty() && maxCol[right] >= maxCol[maxQueue.peekLast()]) {
maxQueue.pollLast();
}
maxQueue.offerLast(right);
// 维护minQueue:移除所有大于当前minCol[right]的元素,确保队列递增
while (!minQueue.isEmpty() && minCol[right] <= minCol[minQueue.peekLast()]) {
minQueue.pollLast();
}
minQueue.offerLast(right);
// 检查窗口有效性:若max - min > limit,移动左边界
while (!maxQueue.isEmpty() && !minQueue.isEmpty()) {
int currentMax = maxCol[maxQueue.peekFirst()];
int currentMin = minCol[minQueue.peekFirst()];
if (currentMax - currentMin > limit) {
// 移除超出窗口范围的队列元素
if (maxQueue.peekFirst() == left) {
maxQueue.pollFirst();
}
if (minQueue.peekFirst() == left) {
minQueue.pollFirst();
}
left++;
} else {
break; // 窗口有效,停止移动左边界
}
}
// 更新当前窗口最大宽度
currentMaxWidth = Math.max(currentMaxWidth, right - left + 1);
}
// 计算当前上下边界下的最大面积,更新全局最大值
int height = bottom - top + 1;
int area = height * currentMaxWidth;
if (area > maxArea) {
maxArea = area;
}
}
}
System.out.println(maxArea);
}
}
四、样例执行过程演示
以样例输入为例,关键步骤如下:
- 当
top=1、bottom=2时:maxCol = [8,6,9,7],minCol = [0,4,6,4];
- 滑动窗口处理:
right=0:队列初始化,窗口[0,0],max=8,min=0,差值 8≤8,宽度 1;right=1:窗口[0,1],max=8,min=0,差值 8≤8,宽度 2;right=2:窗口[0,2],max=9,min=0,差值 9>8,左边界移至 1;right=3:窗口[1,3],max=9,min=4,差值 5≤8,宽度 3;
- 此时高度 = 2(
bottom-top+1=2),面积 = 2×3=6,即全局最大面积。
五、复杂度分析
- 时间复杂度:O (N²M)。枚举上边界 O (N)、下边界 O (N),每次更新列最值 O (M),滑动窗口 O (M),总复杂度 O (N×N×M);
- 空间复杂度:O (M)。仅需存储两个列最值数组(大小 M)和两个单调队列(长度不超过 M),空间开销极小。
对比暴力解法的 O (N²M²),该解法效率提升显著:当 N=M=200 时,操作数仅 8e6,可在毫秒级完成计算。
六、总结
本文提出的「固定上下边界 + 列最值压缩 + 滑动窗口」解法,是处理二维子矩阵最值相关问题的经典思路,核心优势在于:
- 降维转化:将二维子矩阵问题转化为一维区间问题,简化求解难度;
- 高效优化:用单调队列解决滑动窗口的动态最值查询,将时间复杂度大幅降低;
- 通用性强:可推广到「子矩阵极差最小」「子矩阵和最大」等同类问题。
该解法兼顾易懂性和高效性,是应对中等规模矩阵问题的最优选择之一。如果需要处理更大规模矩阵(如 N,M=1e3),可进一步优化(如固定左右边界压缩行最值,适配 N 远大于 M 的场景)。
86

被折叠的 条评论
为什么被折叠?



