最大子矩阵和问题(“九韶杯” 最强对手矩阵、AW的西瓜田)

最大子矩阵和问题就是在一个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。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值