历届试题 最大子阵
问题描述
给定一个 n × m n\times m n×m 的矩阵 A,求 A 中的一个非空子矩阵,使这个子矩阵中的元素和最大。
其中,A 的子矩阵指在A中行和列均连续的一块。输入格式
输入的第一行包含两个整数 n , m n, m n,m,分别表示矩阵 A 的行数和列数。
接下来 n n n 行,每行 m m m 个整数,表示矩阵 A。输出格式
输出一行,包含一个整数,表示 A 中最大的子矩阵中的元素和。
样例输入:
3 3
-1 -4 3
3 4 -1
-5 -2 8样例输出:
10
样例说明
取最后一列,和为 10。
数据规模和约定
对于50%的数据, 1 ≤ n , m ≤ 50 1\le n, m\le50 1≤n,m≤50;
对于100%的数据, 1 ≤ n , m ≤ 500 1\le n, m\le500 1≤n,m≤500,A 中每个元素的绝对值不超过 5000。
分析
按照常规思路,可以写一个根据子矩阵中左上角和右下角的位置坐标来求其中所有元素值的函数。接下来遍历整个矩阵,枚举所有的子矩阵,并将其中的最大值求出即可。但是,这样的解法基本上是没有任何意义的。因为单单计算一次某个子矩阵的元素总和就需要不少的时间,而对于一个 n × m n \times m n×m 的矩阵而言,其子矩阵个数更是高达 ( 1 + 2 + 3 + … + n ) × ( 1 + 2 + 3 + … + m ) (1 + 2 + 3 + … + n)\times (1 + 2 + 3 + … + m) (1+2+3+…+n)×(1+2+3+…+m) 之多。所以这样的算法必然严重超时!
那用二维前缀数组是否能胜任呢?应该要想到,使用了二维前缀数组后,实际上是将求子矩阵的元素总和由 O ( n ∗ m ) O(n*m) O(n∗m)变为了常数级。这样一来,求解该题的时间复杂度即为 ( 1 + 2 + 3 + … + n ) × ( + 2 + 3 + … + m ) (1 + 2 + 3 + … + n)\times( + 2 + 3 + … + m) (1+2+3+…+n)×(+2+3+…+m),亦即 O ( n 2 ∗ m 2 ) O(n^2 * m^2) O(n2∗m2),这依然超时无疑。
回想前面在 P1115 最大子段和 中,该题是求在一维数组中的最大子序列之和,其主要思路是维护一个临时的前缀和变量以记录从一开始时的某个子序列组成的前缀和,并在之后的过程中不断更新它。现在本题则是对二维矩阵进行最大子矩阵之和进行寻找。试想,可不可以用某种方法,将二维的矩阵降维,然后再用 P1115 最大子段和 的思路来进行求解。
这样一来,我们的求解思路如下:
- 首先按行枚举所有子矩阵会包含的行(该步骤将用两层循环完成,其目的是依次得到原矩阵的 1 ∼ 1 1\sim1 1∼1 行、 1 ∼ 2 1\sim2 1∼2 行、……、 1 ∼ n 1\sim n 1∼n 行、 2 ∼ 2 2\sim2 2∼2 行、 1 ∼ 3 1\sim3 1∼3 行、……、 2 ∼ n 2\sim n 2∼n 行、……、 n ∼ n n\sim n n∼n 行);
- 对于上面的每次枚举,相当于都确定了一个待求子矩阵的上下边界,接下来我们需要确定其左右边界以使得这个子矩阵所包含的元素之和最大。这样一来,所有的子矩阵元素之和都能参与到比较中,从而保证了该算法的正确性。但是在确定当前子矩阵的左右边界时,由于其可能含有多行,因此我们必须想办法来使得这些行信息变得更简便。于是换个思路,如果我们将确定的上下边界的子矩阵按列分组,并视每个列为一个单独的整体(假设这个整体的总和我们可以直接得到),那么此时,就相当于把问题变成了和 P1115 最大子段和 相同的问题——求一组序列中的最大子序列(最大是指和最大)。
基于此,就能将时间复杂度降至 O ( n 2 ∗ m ) O(n^2*m) O(n2∗m),这在 1 ≤ n , m ≤ 500 1\le n, m\le500 1≤n,m≤500 的前提下是足以胜任的。
现在的新问题是,如何设计一个能在常数级别内得到某指定列在固定行间之间的元素总和的数据结构?很简单,一个按列分组并按行进行前缀求和的二维数组即可。若设该数组为 rowPrefix[ ][ ],则有:
r o w P r e f i x [ i ] [ j ] = r o w P r e f i x [ i − 1 ] [ j ] + r o w P r e f i x [ i ] [ j ] rowPrefix[i][j] = rowPrefix[i-1][j] + rowPrefix[i][j] rowPrefix[i][j]=rowPrefix[i−1][j]+rowPrefix[i][j]
实际上,可以视 rowPrefix[ ][ ] 在其每一列上都是一个独立的前缀和数组。下面给出求解本题的完整代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=505,MIN=-5000;
int map[N][N]; // 存储原始矩阵
int rowPrefix[N][N]; // rowPrefix[i][j]存储按列分组后按行求和的前缀数组
int list[N]; // 用于保存通过 rowPrefix 得到的某子矩阵按列分组后的求和序列
int main()
{
int n,m,i,j,k,ans=MIN;
cin>>n>>m;
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
{
cin>>map[i][j];
rowPrefix[i][j] = rowPrefix[i-1][j] + map[i][j];
}
for(i=0;i<n;i++)
{
for(j=i+1;j<=n;j++)
{
for(k=1;k<=m;k++)
list[k] = rowPrefix[j][k] - rowPrefix[i][k];
for(k=1;k<=m;k++)
{
if(list[k-1] > 0)
list[k] += list[k-1];
if(list[k] > ans)
ans = list[k];
}
}
}
cout<<ans<<endl;
return 0;
}