前言:我真的不知道,在不知道最大子矩阵解法的情况下,我是怎么撑到现在的,我可以回去学习文化课了
问题描述:
求一个M*N的矩阵的最大子矩阵和。
比如在如下这个矩阵中:
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
拥有最大和的子矩阵为:
9 2
-4 1
-1 8
其和为15
分析:
这是一个经典问题了,我们最先想到的(最耿直的)做法就是枚举矩阵的左上角和右下角
复杂度应该是n^6的,即使加上二维前缀和优化,复杂度也是高达n^4
那我们来考虑一下,子矩阵都有哪些情况,
因为原矩阵有M行,那么ta的子矩阵就有可能有1~M行,每一种的首行有可能是1~M-k+1中的任何一种(k为子矩阵的宽)
假如说k=2,样例中的矩阵就可以分成以下三种情况:
0 -2 -7 0 9 2 -6 2 -4 1 -4 1
9 2 -6 2 -4 1 -4 1 -1 8 0 -2
在每一种情况里,我们还要找出一个最大的子矩阵,当然,这只是局部最大,不一定是问题的解
但是,如果我们知道每一种情况的最大矩阵,要找出问题的最终解就比较简单了
首先,本着化繁为简的原则,我们先考虑最简单的方法,k=1(实际上就是一个序列)
假设我们的面前摆着一个序列
9 2 -6 2
我们需要求出ta的最大子段和,我们可以用dp解决
(这里不得不提一句,求解不定区间的最大子段和,使用的是线段树)
dp方程:f[i]=max(f[i-1]+a[i],a[i]) ,f[i]表示1到i的最大子段和(这个子段必须包括i)
(考虑当前元素,ta可以加入i-1,也可以单独作为一个序列)
那么上面那一个行得到的f数组就是:9,11,5,7(max=11)
这样我们就得到了k=1时,这种情况下的答案
但是我们不仅要解决k=1,我们面临的问题是k=2,3,4,...,M
那怎么办呢
注意,我们重新审视我们的前提之后,会发现我们枚举了k,也就是说,我们已经知道了子矩阵的宽度
那如果我们在这个条件下,选择了x1到x2(列数)的矩阵,那么这一列上的元素我们都要计算进去
可能这样解释的云里雾里,看个例子:
5 1 2 5 -9
-1 2 4 1 5
4 1 6 4 -4
假设我们现在要求的是
-1 2 4 1 5
4 1 6 4 -4
中的最大子矩阵,我们的核心思想不外乎 枚举我们枚举了左右端点:x1=2,x2=4
这样我们的矩阵和就是(2+1)+(4+6)+(1+4)
我为什么要加括号? 为了说明无论枚举的左右端点是什么,同一列上的元素一定是同时行动的
那这就提示我们,既然一起加减,为什么我们不能把同一列上的元素都捆绑到一起,一同考虑呢?
很好,这个思路指引着我们找到了一个美妙的优化方式:
如果k=2,我们就把两行的元素上下对应相加,最后得到一个序列,在序列上求最大子段和即可
我们现在的算法变成了这样:
枚举上下界限(n^2),得到这些行中每一列的权值和,组成一个新序列,在这个序列中运行dp得到最大子段和
但是计算每一列的权值和还是需要浪费时间,我们要是能够O(1)得出新序列,整个算法就可以降至O(n^3)了
这个问题很容易解决,我们只要利用前缀和的思想,维护一个列上前缀和即可
tip
dp方程不要写错了,不要慌
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int INF=1e9;
int n;
int a[103][103],f[103];
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
a[i][j]=a[i-1][j]+x; //a[i][j]前i行,第j列的前缀和
}
int ans=-INF;
for (int i=1;i<=n;i++) //枚举上下边界
for (int j=i;j<=n;j++)
{
memset(f,0,sizeof(f));
for (int k=1;k<=n;k++)
{
f[k]=max(a[j][k]-a[i-1][k],f[k-1]+a[j][k]-a[i-1][k]);
ans=max(ans,f[k]);
}
}
printf("%d",ans);
return 0;
}