题目描述
给定一个 N×MN \times MN×M 的整数矩阵( 1≤N,M≤6001 \leq N,M \leq 6001≤N,M≤600 ,元素范围为 −106-10^6−106 到 10610^6106 ),要求找出一个子矩阵 ,使得该子矩阵按行优先顺序线性化 后得到一个严格递增序列 。你需要输出满足条件的子矩阵中元素个数最多 的那个的元素个数。
线性化 是指将子矩阵的每一行从左到右依次拼接起来,形成一维序列。
子矩阵 是指原矩阵中任意一个边平行于原矩阵边的矩形区域。
输入格式
输入包含多个测试用例。每个测试用例的第一行是两个整数 NNN 和 MMM 。接下来 NNN 行,每行 MMM 个整数。输入以一行 0 0 结束。
输出格式
对于每个测试用例,输出一行,包含满足条件的最大子矩阵的元素个数。
题目分析与解题思路
这是一个二维矩阵上的“最长递增子序列”问题,但条件更复杂:递增序列必须来自一个连续的子矩阵 ,且按行优先顺序排列后严格递增。
关键约束与观察
- 子矩阵的矩形性质 :子矩阵必须是完整的矩形,不能是任意形状的集合。
- 严格递增条件 :设子矩阵左上角为 (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]
这个序列必须严格递增。 - 递增的传递性 :这要求:
- 行内递增 :对于子矩阵中的每一行,从左到右必须严格递增。
- 行间衔接递增 :对于相邻的两行,下一行的第一个元素必须大于上一行的最后一个元素。
暴力枚举的不可行性
最直接的思路是枚举所有可能的子矩阵(左上角和右下角),然后检查其线性化是否递增。子矩阵个数约为 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,M≤600 完全不可行。
高效算法思路:枚举左右边界 + 行滑动窗口
我们需要一个 O(N2M2)O(N^2 M^2)O(N2M2) 或更优的算法。一个可行的策略是:
- 枚举子矩阵的左右边界 :设左边界列为 leftleftleft ,右边界列为 rightrightright 。这样我们就确定了子矩阵的宽度 width=right−left+1width = right - left + 1width=right−left+1 。
- 在行方向上寻找最大高度的合法子矩阵 :对于固定的列区间 [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[top−1][right] (即行间衔接递增)。
- 计算面积并更新答案 :对于每个 (left,right,top,bottom)(left, right, top, bottom)(left,right,top,bottom) 的组合,计算面积 width×(bottom−top+1)width \times (bottom - top + 1)width×(bottom−top+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] 来记录:当右边界 rightrightright 从 leftleftleft 开始向右扩展时,第 kkk 行在列区间 [left,right][left, right][left,right] 内连续递增的长度 。
- 初始时 right=leftright = leftright=left , sum[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][right′−1] :
- 如果满足,则 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[k−1][right] ,我们不能简单地累积,因为它依赖于两行之间的关系。
我们可以使用一个滑动窗口 (双指针)在行方向上进行维护:
- 用指针 lll 表示当前候选子矩阵的顶边行号, kkk 表示当前尝试作为底边的行号。
- 我们从上到下遍历每一行 kkk 作为底边,并尝试调整 lll 以保证窗口 [l,k][l, k][l,k] 内的所有行都满足条件。
- 维护规则:
- 行间衔接检查 :如果当前行 kkk 与上一行 k−1k-1k−1 不满足衔接递增条件(即 g[k][left]<=g[k−1][right]g[k][left] <= g[k-1][right]g[k][left]<=g[k−1][right] ),那么以 kkk 为底边的子矩阵不可能包含 k−1k-1k−1 以上的行,所以我们将窗口左边界 lll 移动到 kkk (从当前行重新开始)。
- 行内递增检查 :如果当前行 kkk 在列区间 [left,right][left, right][left,right] 内不是完全递增的(即 sum[k]!=widthsum[k] != widthsum[k]!=width ),那么这一行根本不能作为子矩阵的一部分,所以窗口左边界 lll 应该移动到 k+1k+1k+1 (下一行)。
- 收缩窗口 :由于 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×(k−l+1) ,并用它更新全局答案。
算法复杂度
- 枚举左边界 leftleftleft : O(M)O(M)O(M) 。
- 对于每个 leftleftleft ,枚举右边界 rightrightright : O(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,M≤600 , 600×6002≈2.16×108600 \times 600^2 \approx 2.16 \times 10^8600×6002≈2.16×108 ,在优化良好的 C++ \texttt{C++ }C++ 实现下通常可以在时限内通过( UVa\texttt{UVa}UVa 时限通常较宽松或本题经过特别优化)。
算法步骤总结
- 读入矩阵。
- 初始化答案为 111 (至少可以取一个元素)。
- 枚举左边界列 leftleftleft 从 000 到 M−1M-1M−1 。
- 初始化 sumsumsum 数组为 000 。
- 枚举右边界列 rightrightright 从 leftleftleft 到 M−1M-1M−1 。
- 计算当前宽度 width=right−left+1width = right - left + 1width=right−left+1 。
- 初始化滑动窗口左边界 l=0l = 0l=0 。
- 遍历每一行 kkk 从 000 到 N−1N-1N−1 作为底边:
a. 更新 sum[k]sum[k]sum[k] :如果 width==1width == 1width==1 , sum[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][right−1] ,则 sum[k]++sum[k]++sum[k]++ ,否则 sum[k]=1sum[k] = 1sum[k]=1 。
b. 检查行间衔接:如果 k>0k > 0k>0 且 g[k][left]<=g[k−1][right]g[k][left] <= g[k-1][right]g[k][left]<=g[k−1][right] ,则 l=kl = kl=k 。
c. 检查行内递增:如果 sum[k]!=widthsum[k] != widthsum[k]!=width ,则 l=k+1l = k + 1l=k+1 。
d. 收缩窗口:如果 l<=kl <= kl<=k 且 sum[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∗(k−l+1) ,更新答案。 - 输出答案。
代码实现
// 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;
}
983

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



