题目描述
真题链接: https://www.dotcpp.com/oj/problem2675.html
题目还是比较简单的,即找给出矩阵中符合题意的最大子矩阵(子矩阵元素:max-min<=limit)
前置知识
着急得到思路可以直接看完美解法,对下面知识点不够熟悉可以跟着做一做
最大连续子段和:O(n)复杂度下找出数组中连续子数组之和最大值
推荐题目: https://leetcode.cn/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/
最大矩阵和(二维的最大连续子段和)
推荐题目: https://leetcode.cn/problems/max-submatrix-lcci/
体会其将二维问题压缩成一维问题的思想
线段树(二选一):O(logn)下查出区间最值
dp求区间最大值(二选一,更推荐):O(1)下查出区间最值
单调队列:定长窗口滑动过程中维护窗口内最值 O(n)
推荐题目: https://leetcode.cn/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/solutions/
二分:通过二分查找满足题意的极值 O(logn)
解题思路
暴力
枚举矩阵左上角和右下角位置(x1,y1),(x2,y2),0<x1,x2<m 0<y1,y2<n
判断每个子矩阵是否符合题意(check),记录最大值
O(check):需找到子矩阵中两个的最值 (x2-x1)*(y2-y1)
时间复杂度:n^2*m^2*O(check)
显然会超时
二维问题-->一维问题
通过前置知识中的一二点,我们能否也将多行压缩压缩成一行来看呢?
判断子矩阵是否合法任务是找到子矩阵的最值,显然是可以用一个节点来代表一列中行区间的最值
只需要枚举一列中的行区间[ns:ne],查出此区间的最值,作为一个节点
这样问题就转变为找出一行节点的最值
说说为什么要压缩行而不是列,因为题目给的数据有的m>>n,所有不选择压缩列枚举列区间
优化check:快速查出某个行区间的最值
线段树:线段树可以维护区间的最值 O(logn)
dp:通过下面转移方程能得到第k列,第i到j行中的最值 O(1)
// matrix:矩阵数组
// 逻辑:如果第j个元素大于前面区间的最大值,当前区间最大值则为当前元素,否则为前面区间最大值
maxT[k][i][j] = Math.max(maxT[k][i][j - 1], matrix[j][k]);
minT[k][i][j] = Math.min(minT[k][i][j - 1], matrix[j][k]);
这题用dp更快
到这已经可以形成还不错的解法了,和前置知识2很像,如下
不完美解法
枚举行区间,通过线段树查出区间最值,以每一列作为起点去找能形成的最大矩形
时间复杂度:n^2*m*(<m)*logm
<m:因为以每一列起点去找能形成的最大矩形时,必然中途有不符合的矩形结束循环
logm:线段树查询时间,换成dp更快为O(1),本人这样写只是为了复习线段树
不能通过全部测试点,也不推荐,不过是前置知识一二的一个很好延升,也承上启下的为完美解法铺路
把线段树换成dp说不一定能过,有机会试试
package lqb;
import java.io.BufferedInputStream;
import java.util.Scanner;
public class LQB题目2675_蓝桥杯2022年第十三届省赛真题_最大子矩阵 {
private static int n, m;
private static int[][] matrix;
private static int limit;
private static int[][] maxT;
private static int[][] minT;
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Scanner scanner = new Scanner(new BufferedInputStream(System.in));
n = scanner.nextInt();
m = scanner.nextInt();
matrix = new int[n][m];
maxT = new int[m][n << 2];
minT = new int[m][n << 2];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
matrix[i][j] = scanner.nextInt();
}
}
limit = scanner.nextInt();
scanner.close();
// build维护区间最大最小值的两个线段树
for (int c = 0; c < m; c++) {
buildMax(c, 0, n - 1, 1);
buildMin(c, 0, n - 1, 1);
}
int MaxS = Integer.MIN_VALUE; // 符合题意的最大面积
for (int ns = 0; ns < n; ++ns) { // 上边界
for (int ne = ns; ne < n; ++ne) { // 下边界
int L = ne - ns + 1; // 上下区间长度
int[] curMa = new int[m]; // 记录当前上下界下每一列的最值,避免下面的过程重复query
int[] curMi = new int[m];
boolean[] isGet = new boolean[m]; // 记录当前上下界下某一列是否被遍历过
for (int c = 0; c < m; ++c) { // 枚举列
int curMax = Integer.MIN_VALUE, curMin = Integer.MAX_VALUE;
boolean isEnd = true;
for (int ce = c; ce < m; ++ce) { // 找到从c开始能形成的最大矩阵
// 得到当前列ce,行ns:nd区间内的最值
int colMax, colMin;
if (isGet[ce] == true) { // 遍历过此列
colMax = curMa[ce];
colMin = curMi[ce];
} else {
colMax = curMa[ce] = queryMax(ce, ns, ne);
colMin = curMi[ce] = queryMin(ce, ns, ne);
}
isGet[ce] = true;
// 更新当前形成的矩阵中的最值
if (colMax > curMax)
curMax = colMax;
if (colMin < curMin)
curMin = colMin;
// 更新当前形成的矩阵中的稳定度
int curLimit = curMax - curMin;
if (curLimit > limit) { // 加上当前列矩阵超出limit
// 取得当前矩阵的面积更新最大值(当前列不算入)
MaxS = Math.max(MaxS, L * ((ce - 1) - c + 1));
isEnd = false;
// 结束以c为起点的循环,换下一个起点找下一个矩阵
break;
}
}
// 到最后一列仍符合题意时
if (isEnd == true)
MaxS = Math.max(MaxS, L * (m - c));
}
}
}
System.out.print(MaxS);
}
// 构建线段树
private static int buildMax(int c, int l, int r, int k) {
if (l == r) {
return maxT[c][k] = matrix[l][c];
}
int mid = l + ((r - l) >> 1);
return maxT[c][k] = Math.max(buildMax(c, l, mid, k << 1), buildMax(c, mid + 1, r, (k << 1) | 1));
}
private static int buildMin(int c, int l, int r, int k) {
if (l == r) {
return minT[c][k] = matrix[l][c];
}
int mid = l + ((r - l) >> 1);
return minT[c][k] = Math.min(buildMin(c, l, mid, k << 1), buildMin(c, mid + 1, r, (k << 1) | 1));
}
private static int queryMax(int c, int fl, int fr, int l, int r, int k) {
if (fl <= l && r <= fr) { // 查找的区间包含当前区间
return maxT[c][k];
}
int ans = Integer.MIN_VALUE;
int mid = l + ((r - l) >> 1);
if (fl <= mid) { // 当前区间左子区间也需要查
ans = Math.max(ans, queryMax(c, fl, fr, l, mid, k << 1));
}
if (fr > mid) { // 当前区间右子区间也需要查
ans = Math.max(ans, queryMax(c, fl, fr, mid + 1, r, (k << 1) | 1));
}
return ans;
}
private static int queryMax(int c, int fl, int fr) {
return queryMax(c, fl, fr, 0, n - 1, 1);
}
private static int queryMin(int c, int fl, int fr) {
return queryMin(c, fl, fr, 0, n - 1, 1);
}
private static int queryMin(int c, int fl, int fr, int l, int r, int k) {
if (fl <= l && r <= fr) { // 查找的区间包含当前区间
return minT[c][k];
}
int ans = Integer.MAX_VALUE;
int mid = l + ((r - l) >> 1);
if (fl <= mid) {
ans = Math.min(ans, queryMin(c, fl, fr, l, mid, k << 1));
}
if (fr > mid) { // 全在右子区间
ans = Math.min(ans, queryMin(c, fl, fr, mid + 1, r, k << 1 | 1));
}
return ans;
}
}
完美解法
如何再优化上面的解法呢?
行区间枚举是不可避免的,区间查找最值也优化到了O(1)
能优化的就是最后的枚举列作为子矩阵起点找最大矩形了
回到题目,目的是找到最大矩阵最大面积
对于每一次行枚举,高度H是固定的,S=H*L
那么任务就是找到最长的宽度L
如何快速找到最长的宽度呢?
从最长的宽度开始去尝试?枚举列,以列为起点宽度为L......L(m)*m*m,不太行
每个宽度都需要枚举吗?
如果Lx不能在当前行区间找到符合题意的子矩阵,L>Lx是不是也不能找到呢?
是的!L可以二分求得最值!!!可以在O(logm)内找到行区间下合法最大子矩阵
log(L)*m*m 效率似乎还是不够用,m*m的复杂度太要命了
那么怎样快速判断是否存在宽度为L的合法子矩阵呢?
固定的宽度L,子矩阵向右移动尝试是否合法,能不能将移动前后两个子矩阵重叠部分信息利用起来呢?
类似于滑动窗口,任务是维护滑动窗口内的最值
单调队列很好地满足这个无理的要求 O(m)
至此,我们终于打败了这个题目
枚举行区间+二分+单调队列+dp取得区间最值 n^2*log(m)*n*1
package lqb;
import java.io.BufferedInputStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Scanner;
public class LQB题目2675_蓝桥杯2022年第十三届省赛真题_最大子矩阵3 {
private static int n, m;
private static int[][] matrix;
private static int limit;
private static int[][][] maxT;
private static int[][][] minT;
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Scanner scanner = new Scanner(new BufferedInputStream(System.in));
n = scanner.nextInt();
m = scanner.nextInt();
matrix = new int[n][m];
maxT = new int[m][n][n]; // maxT[k][i][j]:第k列中第i行到第j行区间内的最大值
minT = new int[m][n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
matrix[i][j] = scanner.nextInt();
}
}
limit = scanner.nextInt();
scanner.close();
// 预处理最值
for (int k = 0; k < m; ++k) {
for (int i = 0; i < n; ++i) {
for (int j = i; j < n; ++j) {
if (i == j) {
maxT[k][i][j] = matrix[i][k];
minT[k][i][j] = matrix[i][k];
} else {
maxT[k][i][j] = Math.max(maxT[k][i][j - 1], matrix[j][k]);
minT[k][i][j] = Math.min(minT[k][i][j - 1], matrix[j][k]);
}
}
}
}
// 枚举上下边界点 时间复杂度:O(n^2)
int MaxS = Integer.MIN_VALUE; // 符合题意的最大面积
for (int ns = 0; ns < n; ++ns) { // 上边界
for (int ne = ns; ne < n; ++ne) { // 下边界
int H = ne - ns + 1; // 上下区间长度
// 二分矩阵的宽度 时间复杂度:O(log(m)*T(check()))
int l = 0, r = m + 1;
while (l + 1 < r) {
int mid = l + ((r - l) >> 1);
if (check(ns, ne, mid) == true) { // 当前宽度可以找到符合的矩阵
// 那么可以让宽度再增大一些再尝试
l = mid;
} else {
r = mid;
}
}
int L=l;
MaxS=Math.max(MaxS, H*L);
}
}
System.out.print(MaxS);
}
// 单调队列实现滑动窗口 O(m)
private static boolean check(int ns, int ne, int L) {
Deque<Integer> maxDeque = new ArrayDeque<>(); // 单调递减(队首最大)维持窗口最大值
Deque<Integer> minDeque = new ArrayDeque<>(); // 单调递增(队首最小)维持窗口最小值
for (int c = 0; c < m; ++c) {
// 得到当前位点的最大值最小值
int colMax = maxT[c][ns][ne];
int colMin = minT[c][ns][ne];
// 新入队元素破坏队列单调性时(即新元素大于队尾元素)
while (maxDeque.size() > 0 && colMax > maxT[maxDeque.getLast()][ns][ne]) {
maxDeque.removeLast();
}
while (minDeque.size() > 0 && colMin < minT[minDeque.getLast()][ns][ne]) {
minDeque.removeLast();
}
// 入队
maxDeque.addLast(c);
minDeque.addLast(c);
// 出队:当队首元素下标超出窗口范围时
if (c - maxDeque.getFirst() + 1 > L) {
maxDeque.removeFirst();
}
if (c - minDeque.getFirst() + 1 > L) {
minDeque.removeFirst();
}
// 当窗口长度为L时,开始判断矩阵是否合格
if (c >= L - 1) {
if (maxT[maxDeque.getFirst()][ns][ne] - minT[minDeque.getFirst()][ns][ne] <= limit) {
return true;
}
}
}
return false;
}
}
心得体会
这个题目不算复杂,原本打算快速解决这个问题,可这个题的题解很多不是最优的且有些写得过于简略,对于本菜鸟来说实在难以理解,于是从头至尾琢磨了一下这个题目,虽然花了很长时间,但也很值得。
其中二维问题到一维问题的转变思路很重要,很多矩阵问题都需要压缩,都需要用dp记录区间特殊值,二分得到符合条件最值也很常见,单调队列单调栈等也是好帮手。