最大子矩阵和问题就是在一个n*m的已知的全为数字的矩阵中,找到一个子矩阵,使这个矩阵各元素的和是所有子矩阵中最大的。
在我看来,这种类型的题分为两种类型:
1、固定了矩阵的长宽,求最大和;
2、没有固定矩阵大小,求子矩阵最大和。
虽然算是两种不同的情况,但是两者都可以用差不多的方法解决:二维数组前缀和。
后面就各用一道题来记录一下这两种类型:
一、AW的西瓜田
学校oj的题:http://172.18.156.134/problem.php?id=1472
不过没找到外网的题目链接,还是写一下题目吧
题目描述:
AW最近比较佛系,开始玩起了种田游戏养生。AW在种田的时候遇到了一个问题,在一块 n×m 个方格的地图上,他想围一块边长为 k 的正方形土地当做西瓜田,但是每个方格的土地肥沃度 w 并不一样,AW想围出一块肥沃度之和最大的土地,请你帮帮他。
输入:
一个整数t,表示t组测试数据。
对于每组测试数据,第一行三个整数n, m(1 <= n, m <= 500), k。含义如上所示并且k保证合法。
接下来有n行m列个正整数 w(0 <= w <= 1000),表示每块土地的肥沃度。
输出:
每组样例两个整数x, y。表示以地图左上角方格为(1,1)开始的坐标系中,西瓜田左上角的坐标。(横轴表示x,纵轴表示y)
如果有多个满足题意的答案,输出坐标最小(先取x最小,x相同取y最小)的那个答案。
样例输入:
2
2 2 1
1 2
3 4
3 3 2
10 10 10
20 10 30
0 10 0
样例输出:
2 2
2 1
这题也是能直接看出来求最大矩阵和的,只不过这题给了矩阵是边长k的正方形,那就相当于是已知了子矩阵的大小。那就是上面的第一种情况。
这题一开始看,想的是暴力,毕竟数据不大,枚举起点再直接暴力遍历整个正方形加每一个数字,然后再更新最大值。这种做法的复杂度是O(tnmk2),那肯定是过不了的。
那这会,我们可以尝试用前缀和进行优化。我们可以对行元素进行求前缀和处理。处理完我们就只需要枚举起点,然后再遍历一次k,把每一行的元素和进行相加即可。通过前缀和的处理,我们可以把复杂度降到O(tnmk)。通过这次优化,我们便可以ac这题了。
放放代码:
#include<cstdio>
using namespace std;
int t,n,m,k;
int map[505][505],qz[505][505],zs,ans,xm,ym;
int main()
{
scanf("%d",&t);
while(t--)
{
ans=0;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
scanf("%d",&map[i][j]);
map[i][j]+=mp[i][j-1];
}
for(int i=1;i<=m-k+1;i++) //枚举起点
for(int j=1;j<=n-k+1;j++)
{
zs=0;
for(int z=0;z<k;z++) //遍历每一条边
zs+=qz[i+z][j+k-1]-qz[i+z][j-1];
if(zs>ans)
{
ans=zs;
xm=j; ym=i; //记录一下答案
}
}
printf("%d %d\n",xm,ym);
}
return 0;
}
二、“九韶杯”G题 最强对手矩阵
题目链接:https://ac.nowcoder.com/acm/contest/13493/G
在这题中,题目表意就十分清楚了:求实力总和最大的矩阵区域的实力和是多少,那这题就能直接看出来是求最大子矩阵和,但是这题的矩阵并没有大小范围规定,说明这是上面提到的第二种情况,也就是没给矩阵边界的情况,不过做法也挺相似。
这题如果暴力做,我们一般会想到用两层for循环来表示这个矩阵的起点,然后再用两层for循环来模拟矩阵的大小,那这样,时间复杂度就是O(n2m2),这种复杂度肯定是过不了的。
那这时,我们可以想到用前缀和来处理来降低时间复杂度。比如,我们可以把列元素进行前缀和处理,那这样我们就能用两层for循环模拟竖边的起点和终点,然后再用一层for循环来模拟横边,这样时间复杂度就降到O(n2m )了。
但是,我们会发现这样才70分,说明还能进一步优化。
另外我们会发现,这两种参数的系数是不一样的,那我们还能进一步优化一下,那就是当n>m的时候,可以转一下矩阵,那这样n、m倒转过来,n是较小的,m是较大的,同为O(n2m),但是能更快一点。
好了,上代码吧
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
int n,m;
ll ans;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin>>n>>m;
vector<vector<int>> a(n+1,vector<int>(m+1));
//题目并没有说明n、m的具体范围,当然是开动态数组最好
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
if(m<n)
{
vector<vector<int>> b(m+1,vector<int>(n+1));
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
b[i][j]=a[j][i]; //反转整个矩阵
b[i][j]+=b[i-1][j]; //计算列前缀和
}
swap(n,m);
for(int i=1;i<=n;i++) //枚举竖边的起点
for(int j=i;j<=n;j++) //枚举竖边的终点
{
ll tot=0;
//当枚举了竖边,那我们就相当于把竖边的和当作一个已知数了
//那现在就可以把他看成一个求横边的最大子序列和问题啦
for(int k=1;k<=m;k++)
{
if(tot<0) tot=0;
tot+=b[j][k]-b[i-1][k];
ans=max(ans,tot);
}
}
}
else
{
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][j]+=a[i-1][j]; //这种就少一个反转操作,直接计算前缀和就行
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
{
ll tot=0;
for(int k=1;k<=m;k++)
{
if(tot<0) tot=0;
tot+=a[j][k]-a[i-1][k];
ans=max(ans,tot);
}
}
}
cout<<ans;
return 0;
}
对于这两种类型,用的都是二维前缀和的方法,之前做的时候都没第一时间想到前缀和,也希望后面见到这种题能记起来吧 qwq。