最大子阵
可以测试的链接:
1.C语言网 题目 1445: [蓝桥杯][历届试题]最大子阵 https://www.dotcpp.com/oj/problem1445.html
2.类似的题 ZOJ Problem Set - 1074 :https://zoj.pintia.cn/problem-sets/91827364500/problems/91827364573
题目描述
给定一个n*m的矩阵A,求A中的一个非空子矩阵,使这个子矩阵中的元素和最大。
其中,A的子矩阵指在A中行和列均连续的一块。
样例说明
取最后一列,和为10。
数据规模和约定
对于100%的数据,1< =n, m< =500,A中每个元素的绝对值不超过5000。
输入
输入的第一行包含两个整数n, m,分别表示矩阵A的行数和列数。
接下来n行,每行m个整数,表示矩阵A。
输出
输出一行,包含一个整数,表示A中最大的子矩阵中的元素和。
样例输入
3 3
-1 -4 3
3 4 -1
-5 -2 8
样例输出
10
思路
题意很简单,就是求子矩阵中元素和最大为多少,第一想法就是二维的前缀和处理,A是原数组,Sum是对应的前缀和数组,表示从左上角到当前位置的矩阵内的元素和。
for(int i=1; i<=n; i++)
{
for(int j=1; j<=m; j++)
{
Sum[i][j] = Sum[i-1][j] + Sum[i][j-1] + A[i][j] - Sum[i-1][j-1];
}
}
有了前缀和就可以通过其来计算子矩阵的和,可是接下来怎么做呢,直接枚举起点和终点吗?
int res = -INF;
for(int x1=0; x1<n; x1++)
{
for(int y1=0; y1<m; y1++)
{
for(int x2=x1+1; x2<=n; x2++)
{
for(int y2=y1+1; y2<=m; y2++)
{
int t = Sum[x2][y2] - Sum[x1][y2] - Sum[x2][y1] + Sum[x1][y1];//矩形和 画张图就可以明白了
res = res>t ? res : t;
}
}
}
}
显然,这样的算法写完是
O
(
n
4
)
O(n^4)
O(n4)的时间复杂度,看看数据应该是过不了的。
那能不能减少一重循环,降到
O
(
n
3
)
O(n^3)
O(n3),发现是可行的。
我们回顾一下求一维数组的区间和最大,还是先算的前缀和数组sum。
贪心:遍历 sum [ R ] ,每一步都取最小的 sum[ L-1 ] , 求差值更新res的值。
int Min = 0 , res = -INF;
for(int i=1; i<=n; i++ ){
Min = min( Min, sum[ L-1 ]);
res = max( res, sum[ R ] - Min );
}
那么在这个想法下我们可不可以把二维计算的过程降到一维的情况下,在一维的情况下计算各区间的和?转化一下,这样就可以减少一重循环。
我们可以先求每一列的一维区间和,这样子我们就可以直接枚举子矩阵的左上端点和右下端点的行就可以了,剩余就相当于遍历所有列,按照上面求一维区间和最大的方法计算即可。当然你也可以求每一行的一维区间和再上面思路对应做(相当于旋转了一下矩阵而已嘛)。(可能讲的不太明白,可以看看代码理解一下)
代码实现
#include<cstdio>
#include<algorithm>
using namespace std;
const int Max_N = 500;
const int Max_M = 500;
const int INF = 100000;
//输入
int n, m;
int A[Max_N + 1][Max_M + 1];
int f[Max_N + 1][Max_M + 1];//求一列的前缀和
int sum[Max_N + 1]; //求一维最大区间和的方式
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
scanf("%d", &A[i][j]);
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++) {
f[i][j] = f[i - 1][j] + A[i][j];//求各列的前缀和(每一列为一维)
}
}
int res = -INF;
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j <= n; j++)//遍历行号
{
//这里其实左上端点对应的i+1行,然后遍历到最后一行
for (int k = 1; k <= m; k++)//先求一维前缀和
{
sum[k] = f[j][k] - f[i][k] + sum[k - 1];
}
//求得以i+1行的点为左上端点的情况下子矩阵最大为多少
int Min = 0;
for (int k = 1; k <= m; k++)//求一维最大区间和
{
Min = min(Min, sum[k - 1]);
res = max(res, sum[k] - Min);
}
//循环更新答案
}
}
printf("%d\n", res);
return 0;
}
进行枚举的循环可以写的更好理解一点
循环的变量直接对应两个端点的行
#include<cstdio>
#include<algorithm>
using namespace std;
const int Max_N = 500;
const int Max_M = 500;
const int INF = 100000;
//输入
int n, m;
int A[Max_N + 1][Max_M + 1];
int f[Max_N + 1][Max_M + 1];//求一列的前缀和
int sum[Max_N + 1]; //求一维最大区间和的方式
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
scanf("%d", &A[i][j]);
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++) {
f[i][j] = f[i - 1][j] + A[i][j];//一列前缀和
}
}
int res = -INF;
//枚举左上端点和右下端点的行号,i对应左上端点,j对应右下端点
for (int i = 1; i <= n; i++)
{
for (int j = i; j <= n; j++)//遍历行号
{
for (int k = 1; k <= m; k++)//先求一维前缀和
{
sum[k] = f[j][k] - f[i - 1][k] + sum[k - 1];
}
int Min = 0;
for (int k = 1; k <= m; k++)//求一维最大区间和
{
Min = min(Min, sum[k - 1]);
res = max(res, sum[k] - Min);
}
}
}
printf("%d\n", res);
return 0;
}
当然也可以dp做法,思路类似,也是计算递推式子。