UVa 11499 Longest Increasing Sub-sequence

题目描述

给定一个 N×MN \times MN×M 的整数矩阵( 1≤N,M≤6001 \leq N,M \leq 6001N,M600 ,元素范围为 −106-10^610610610^6106 ),要求找出一个子矩阵 ,使得该子矩阵按行优先顺序线性化 后得到一个严格递增序列 。你需要输出满足条件的子矩阵中元素个数最多 的那个的元素个数。

线性化 是指将子矩阵的每一行从左到右依次拼接起来,形成一维序列。

子矩阵 是指原矩阵中任意一个边平行于原矩阵边的矩形区域。

输入格式

输入包含多个测试用例。每个测试用例的第一行是两个整数 NNNMMM 。接下来 NNN 行,每行 MMM 个整数。输入以一行 0 0 结束。

输出格式

对于每个测试用例,输出一行,包含满足条件的最大子矩阵的元素个数。

题目分析与解题思路

这是一个二维矩阵上的“最长递增子序列”问题,但条件更复杂:递增序列必须来自一个连续的子矩阵 ,且按行优先顺序排列后严格递增。

关键约束与观察

  1. 子矩阵的矩形性质 :子矩阵必须是完整的矩形,不能是任意形状的集合。
  2. 严格递增条件 :设子矩阵左上角为 (r1,c1)(r_1, c_1)(r1,c1) ,右下角为 (r2,c2)(r_2, c_2)(r2,c2) ,则线性化后的序列为:
    matrix[r1][c1],matrix[r1][c1+1],…,matrix[r1][c2],matrix[r_1][c_1], matrix[r_1][c_1+1], \dots, matrix[r_1][c_2],matrix[r1][c1],matrix[r1][c1+1],,matrix[r1][c2],
    matrix[r1+1][c1],…,matrix[r2][c2]matrix[r_1+1][c_1], \dots, matrix[r_2][c_2]matrix[r1+1][c1],,matrix[r2][c2]
    这个序列必须严格递增。
  3. 递增的传递性 :这要求:
    • 行内递增 :对于子矩阵中的每一行,从左到右必须严格递增。
    • 行间衔接递增 :对于相邻的两行,下一行的第一个元素必须大于上一行的最后一个元素。

暴力枚举的不可行性

最直接的思路是枚举所有可能的子矩阵(左上角和右下角),然后检查其线性化是否递增。子矩阵个数约为 O(N2M2)O(N^2 M^2)O(N2M2) ,检查一个子矩阵需要 O(NM)O(NM)O(NM) 时间,总复杂度 O(N3M3)O(N^3 M^3)O(N3M3) ,对于 N,M≤600N, M \leq 600N,M600 完全不可行。

高效算法思路:枚举左右边界 + 行滑动窗口

我们需要一个 O(N2M2)O(N^2 M^2)O(N2M2) 或更优的算法。一个可行的策略是:

  1. 枚举子矩阵的左右边界 :设左边界列为 leftleftleft ,右边界列为 rightrightright 。这样我们就确定了子矩阵的宽度 width=right−left+1width = right - left + 1width=rightleft+1
  2. 在行方向上寻找最大高度的合法子矩阵 :对于固定的列区间 [left,right][left, right][left,right] ,我们需要找到一个行区间 [top,bottom][top, bottom][top,bottom] ,使得:
    • 对于 [top,bottom][top, bottom][top,bottom] 中的每一行,该行在列区间 [left,right][left, right][left,right] 内是严格递增的。
    • 对于 [top,bottom][top, bottom][top,bottom] 中相邻的两行,满足 matrix[top][left]>matrix[top−1][right]matrix[top][left] > matrix[top-1][right]matrix[top][left]>matrix[top1][right] (即行间衔接递增)。
  3. 计算面积并更新答案 :对于每个 (left,right,top,bottom)(left, right, top, bottom)(left,right,top,bottom) 的组合,计算面积 width×(bottom−top+1)width \times (bottom - top + 1)width×(bottomtop+1) ,并记录最大值。

关键问题 :如何高效地找到对于给定 [left,right][left, right][left,right] ,满足条件的最大行区间 [top,bottom][top, bottom][top,bottom]

行内递增的维护

我们可以预处理或动态维护一个信息:对于第 kkk 行,从 leftleftleft 列开始,向右最远能延伸到哪一列,使得该区间内是严格递增的。更具体地,我们可以维护一个数组 len[k]len[k]len[k] ,表示以第 kkk 行为底边,在当前的 [left,right][left, right][left,right] 列区间内,该行是否完全递增,以及其“连续的递增行高度”。

但这里有一个更巧妙的累积方法,如 AC 代码所示:

  • 用一个数组 sum[k]sum[k]sum[k] 来记录:当右边界 rightrightrightleftleftleft 开始向右扩展时,第 kkk 行在列区间 [left,right][left, right][left,right]连续递增的长度
  • 初始时 right=leftright = leftright=leftsum[k]=1sum[k] = 1sum[k]=1 (单列总是递增)。
  • rightrightright 向右移动一列到 right′right'right 时,检查第 kkk 行是否满足 g[k][right′]>g[k][right′−1]g[k][right'] > g[k][right'-1]g[k][right]>g[k][right1]
    • 如果满足,则 sum[k]++sum[k]++sum[k]++ (递增序列延长了)。
    • 如果不满足,则 sum[k]=1sum[k] = 1sum[k]=1 (递增序列从当前列重新开始,因为我们必须包含当前右边界所在的列)。
  • 这样,对于当前的 [left,right][left, right][left,right] ,如果 sum[k]==widthsum[k] == widthsum[k]==width ,说明第 kkk 行在整段列区间内都是严格递增的,它可以作为子矩阵的一部分。
行间衔接递增的维护与滑动窗口

对于行间衔接递增条件 g[k][left]>g[k−1][right]g[k][left] > g[k-1][right]g[k][left]>g[k1][right] ,我们不能简单地累积,因为它依赖于两行之间的关系。

我们可以使用一个滑动窗口 (双指针)在行方向上进行维护:

  • 用指针 lll 表示当前候选子矩阵的顶边行号, kkk 表示当前尝试作为底边的行号。
  • 我们从上到下遍历每一行 kkk 作为底边,并尝试调整 lll 以保证窗口 [l,k][l, k][l,k] 内的所有行都满足条件。
  • 维护规则:
    1. 行间衔接检查 :如果当前行 kkk 与上一行 k−1k-1k1 不满足衔接递增条件(即 g[k][left]<=g[k−1][right]g[k][left] <= g[k-1][right]g[k][left]<=g[k1][right] ),那么以 kkk 为底边的子矩阵不可能包含 k−1k-1k1 以上的行,所以我们将窗口左边界 lll 移动到 kkk (从当前行重新开始)。
    2. 行内递增检查 :如果当前行 kkk 在列区间 [left,right][left, right][left,right] 内不是完全递增的(即 sum[k]!=widthsum[k] != widthsum[k]!=width ),那么这一行根本不能作为子矩阵的一部分,所以窗口左边界 lll 应该移动到 k+1k+1k+1 (下一行)。
    3. 收缩窗口 :由于 sum[k]sum[k]sum[k] 可能小于 widthwidthwidth ,我们可能需要不断右移 lll ,直到窗口 [l,k][l, k][l,k] 内的所有行都满足 sum[行]>=widthsum[行] >= widthsum[]>=width (实际上,由于我们按条件2设置了 lll ,这里通常已经满足,但为了处理之前行不满足而 lll 未及时移动的情况,可以再检查并收缩)。
  • 在维护了合法的窗口 [l,k][l, k][l,k] 后,我们就可以计算以第 kkk 行为底边的子矩阵面积: area=width×(k−l+1)area = width \times (k - l + 1)area=width×(kl+1) ,并用它更新全局答案。

算法复杂度

  • 枚举左边界 leftleftleftO(M)O(M)O(M)
  • 对于每个 leftleftleft ,枚举右边界 rightrightrightO(M)O(M)O(M)
  • 对于每个 (left,right)(left, right)(left,right) ,遍历所有行并维护滑动窗口: O(N)O(N)O(N)
  • 总时间复杂度: O(NM2)O(N M^2)O(NM2)
  • 空间复杂度: O(N)O(N)O(N) (主要用于 sumsumsum 数组)。

对于 N,M≤600N, M \leq 600N,M600600×6002≈2.16×108600 \times 600^2 \approx 2.16 \times 10^8600×60022.16×108 ,在优化良好的 C++ \texttt{C++ }C++ 实现下通常可以在时限内通过( UVa\texttt{UVa}UVa 时限通常较宽松或本题经过特别优化)。

算法步骤总结

  1. 读入矩阵。
  2. 初始化答案为 111 (至少可以取一个元素)。
  3. 枚举左边界列 leftleftleft000M−1M-1M1
  4. 初始化 sumsumsum 数组为 000
  5. 枚举右边界列 rightrightrightleftleftleftM−1M-1M1
  6. 计算当前宽度 width=right−left+1width = right - left + 1width=rightleft+1
  7. 初始化滑动窗口左边界 l=0l = 0l=0
  8. 遍历每一行 kkk000N−1N-1N1 作为底边:
    a. 更新 sum[k]sum[k]sum[k] :如果 width==1width == 1width==1sum[k]=1sum[k] = 1sum[k]=1 ;否则如果 g[k][right]>g[k][right−1]g[k][right] > g[k][right-1]g[k][right]>g[k][right1] ,则 sum[k]++sum[k]++sum[k]++ ,否则 sum[k]=1sum[k] = 1sum[k]=1
    b. 检查行间衔接:如果 k>0k > 0k>0g[k][left]<=g[k−1][right]g[k][left] <= g[k-1][right]g[k][left]<=g[k1][right] ,则 l=kl = kl=k
    c. 检查行内递增:如果 sum[k]!=widthsum[k] != widthsum[k]!=width ,则 l=k+1l = k + 1l=k+1
    d. 收缩窗口:如果 l<=kl <= kl<=ksum[k]<widthsum[k] < widthsum[k]<width ,则不断 l++l++l++ 直到条件满足或 l>kl > kl>k
    e. 如果 l<=kl <= kl<=k ,计算面积 area=width∗(k−l+1)area = width * (k - l + 1)area=width(kl+1) ,更新答案。
  9. 输出答案。

代码实现

// Longest Increasing Sub-sequence
// UVa ID: 11499
// Verdict: Accepted
// Submission Date: 2025-12-03
// UVa Run Time: 0.660s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

int g[605][605]; // 存储矩阵
int sum[605];    // sum[k]:对于当前列区间,第k行连续递增的长度
int n, m;

int main() {
    while (scanf("%d %d", &n, &m) == 2) {
        if (n + m == 0) break;
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                scanf("%d", &g[i][j]);

        int ret = 1; // 答案至少为1(单个元素)
        // 枚举子矩阵的左边界
        for (int left = 0; left < m; left++) {
            memset(sum, 0, sizeof(sum)); // 每次左边界变化时重置sum数组
            // 枚举子矩阵的右边界
            for (int right = left; right < m; right++) {
                int width = right - left + 1; // 当前子矩阵的列数(宽度)
                int l = 0; // 滑动窗口的左边界(行号),表示当前候选子矩阵的顶边
                // 遍历每一行作为子矩阵的底边
                for (int k = 0; k < n; k++) {
                    // 更新当前行在列区间[left, right]内的连续递增长度
                    if (width == 1) {
                        // 单列总是递增的
                        sum[k] = 1;
                    } else {
                        // 如果当前列比前一列大,则递增序列可以延长
                        if (g[k][right] > g[k][right - 1])
                            sum[k]++; // 增加连续递增长度
                        else
                            sum[k] = 1; // 否则从头开始(仅当前列)
                    }

                    // 检查行间衔接是否满足递增:当前行左端元素 > 上一行右端元素
                    if (k > 0 && g[k][left] <= g[k - 1][right]) {
                        // 不满足,则窗口必须从当前行重新开始
                        l = k;
                    }

                    // 检查当前行在列区间内是否完全递增
                    if (sum[k] != width) {
                        // 不完全递增,则当前行不能作为子矩阵的一部分,
                        // 窗口左边界移到下一行
                        l = k + 1;
                    }

                    // 收缩窗口:如果当前行的连续递增长度不足宽度,
                    // 则需要不断抬高窗口顶边,直到满足条件或窗口为空
                    while (l <= k && sum[k] < width) {
                        l++;
                    }

                    // 如果窗口非空,计算面积并更新答案
                    if (l <= k) {
                        int height = k - l + 1; // 子矩阵的行数(高度)
                        ret = max(ret, height * width);
                    }
                }
            }
        }
        printf("%d\n", ret);
    }
    return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值