题目 2675:蓝桥杯2022年第十三届省赛真题-最大子矩阵(满分详解)

题目描述
真题链接: 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:快速查出某个行区间的最值
  1. 线段树:线段树可以维护区间的最值 O(logn)

  1. 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记录区间特殊值二分得到符合条件最值也很常见,单调队列单调栈等也是好帮手。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值