简介:
最大子矩阵问题是指在一个矩阵中找到一个子矩阵,使得该子矩阵的元素之和最大。
解决该问题的常用方法是使用动态规划。先计算出每一行的前缀和,然后对于每一列的起始和终止位置,计算出该区域内每一行的和,得到一个一维数组。再对该一维数组使用动态规划求解最大子数组和的问题,得到最大子矩阵的元素之和。
该问题也可以使用暴力搜索的方法,枚举所有可能的子矩阵,计算它们的元素之和,并找到最大值。但是由于时间复杂度过高,所以在实际应用中很少使用。
下面我们将以例题的形式时间复杂度由高到底给大家讲解几种方法。
例题 P1719 最大加权矩形
题目描述
为了更好的备战 NOIP2013,电脑组的几个女孩子 LYQ,ZSC,ZHQ 认为,我们不光需要机房,我们还需要运动,于是就决定找校长申请一块电脑组的课余运动场地,听说她们都是电脑组的高手,校长没有马上答应他们,而是先给她们出了一道数学题,并且告诉她们:你们能获得的运动场地的面积就是你们能找到的这个最大的数字。
校长先给他们一个 n×n 矩阵。要求矩阵中最大加权矩形,即矩阵的每一个元素都有一权值,权值定义在整数集上。从中找一矩形,矩形大小无限制,是其中包含的所有元素的和最大 。矩阵的每个元素属于 [−127,127] ,例如
代码语言:javascript复制 0 –2 –7 0
9 2 –6 2
-4 1 –4 1
-1 8 0 –2
在左下角:
代码语言:javascript复制9 2
-4 1
-1 8
和为 15。
几个女孩子有点犯难了,于是就找到了电脑组精打细算的 HZH,TZY 小朋友帮忙计算,但是遗憾的是他们的答案都不一样,涉及土地的事情我们可不能含糊,你能帮忙计算出校长所给的矩形中加权和最大的矩形吗?
输入格式
第一行:n,接下来是 n 行 n 列的矩阵。
输出格式
最大矩形(子矩阵)的和。
输入
代码语言:javascript复制4
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
输出
代码语言:javascript复制15
说明/提示
1≤n≤120
一、暴力求解
所谓的暴力求解就是分别枚举(x1,y1)、(x2,y2),四个for循环分别枚举每一个点的坐标,两个for循环去遍历这个矩阵的每一个点的权值,所以时间复杂度再O(n^6)。
代码实现:
代码语言:javascript复制#include<iostream>
using namespace std;
const int N=125;
int n,sum,maxx;
int a[N][N];
int main(){
cin>>n;
for(int i=1;i<=n;i ){
for(int j=1;j<=n;j ){
cin>>a[i][j];
}
}
for(int x1=1;x1<=n;x1 ){//枚举左上点
for(int y1=1;y1<=n;y1 ){
for(int x2=x1;x2<=n;x2 ){//枚举右下点
for(int y2=y1;y2<=n;y2 ){
sum=0;
for(int i=x1;i<=x2;i ){//遍历x1-x2,y1-y2点的子矩阵
for(int j=y1;j<=y2;j ){
sum =a[i][j];
}
}
maxx=max(maxx,sum);
}
}
}
}
cout<<maxx<<endl;
return 0;
}
暴力求解时间复杂度相当的高了,要想解决此题,需要更加优化的方法。下面按照时间复杂度继续优化。
二、一维前缀和优化
一维前缀和优化是建立在暴击求解的基础上来利用前缀和实现对求子矩阵,优化掉一层for循环,时间复杂度O(n^5),在解决此题也不能通过,时间复杂度也是比较高的。前缀和二维也就是通过一维滚动数组来实现。此一维前缀和优化,一般是不会用的,网上对它介绍的也比较少,大家了解一下即可。
代码实现:
代码语言:javascript复制#include<iostream>
using namespace std;
const int N=125;
int n,sum,maxx;
int a[N][N],presum[N][N];
int main(){
cin>>n;
for(int i=1;i<=n;i ){
for(int j=1;j<=n;j ){
cin>>a[i][j];
presum[i][j]=presum[i][j-1] a[i][j];
}
}
for(int x1=1;x1<=n;x1 ){//枚举左上点
for(int y1=1;y1<=n;y1 ){
for(int x2=x1;x2<=n;x2 ){//枚举右下点
for(int y2=y1;y2<=n;y2 ){
sum=0;
for(int i=x1;i<=x2;i ){//遍历x1-x2,y1-y2点的子矩阵
sum =presum[i][y2]-presum[i][y1-1];
}
maxx=max(maxx,sum);
}
}
}
}
cout<<maxx<<endl;
return 0;
}
三、二维前缀和优化
二维前缀和优化就是在求解此题时,提前把二维前缀和求出来,在计算矩阵时,优化了两个for循环求解子矩阵值的问题,可以利用前缀和快速求出,二维前缀和优化时间复杂度为O(n^4)。
那么如何求解子矩阵的和呢?看下面这张图,要求(x1,y1)到(x2,y2)这个矩阵的值,那么前缀和presum[x][y]是由起点(1,1)到(x,y)的值,如何转换成起点为(x1,y1)呢,很简单,如图求红色的矩阵的值,=整个大矩阵-黄色矩阵-绿色矩阵-蓝色矩阵。那么就可以理解为presum[x2][y2]=A B C D,A矩阵的起点是(1,1),所以黄色矩阵A也可以表示为presum[x1][y1],那么我们将黄色矩阵A合并给B跟C,这样起点就是(1,1)了,可以用前缀和表示了。那么红色矩阵D=presum[x2][y2]-(A B)-(A C) A,我们将它转化为前缀和的形式,注意不能取到顶点,那么D=presum[x2][y2]-presum[x1-1][y2]-presum[x2][y1-1] presum[x1-1][y1-1]
代码实现:
代码语言:javascript复制#include<iostream>
using namespace std;
const int N=125;
int a[N][N],presum[N][N];
int n,maxx;
int main(){
cin>>n;
for(int i=1;i<=n;i ){
for(int j=1;j<=n;j ){
scanf("%d",&a[i][j]);
presum[i][j]=presum[i][j-1] presum[i-1][j]-presum[i-1][j-1] a[i][j];//递推关系实现
}
}
for(int x1=1;x1<=n;x1 ){
for(int y1=1;y1<=n;y1 ){
for(int x2=x1;x2<=n;x2 ){
for(int y2=y1;y2<=n;y2 ){
int sum=presum[x2][y2]-presum[x2][y1-1]-presum[x1-1][y2] presum[x1-1][y1-1];//求子矩阵和
maxx=max(maxx,sum);
}
}
}
}
cout<<maxx<<endl;;
return 0;
}
四、dp版
前缀和优化只是过度,动态规划才是归宿,这道题我们的最终解法就是动态规划,通过对行(列)的状态压缩,以及递推关系式实现对列(行)的优化,这样可以再优化掉一层for循环,时间复杂度为O(n^3)。
首先我们利用状态压缩,这里代码以列的状态压缩。第一行到最后一行是递增的,把每一列看成一维数组,多个列就成了二维的数组了。在求解时,先枚举起实行跟终止行,再去枚举每一列,这样就确定了多个子矩阵,把它用dp数组表示,每一个小子矩阵还可以与相邻的子矩阵构成子矩阵,每一次与自己比较大小。
代码实现:
代码语言:javascript复制#include<iostream>
using namespace std;
const int N=125;
int a[N][N],presum[N][N],dp[N];
int n,maxx;
//dp解法:状态压缩,把二维压缩成一维
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i ){
for(int j=1;j<=n;j ){
scanf("%d",&a[i][j]);
presum[i][j]=presum[i-1][j] a[i][j];//列累加,列的前缀和
}
}
for(int x1=1;x1<=n;x1 ){//x1为起始行
for(int x2=x1;x2<=n;x2 ){//x2为终止行
//第x1行到第x2行的最大子矩阵和
for(int j=1;j<=n;j ){//j为列
dp[j]=presum[x2][j]-presum[x1-1][j];
}
for(int i=1;i<=n;i ){
dp[i]=max(dp[i],dp[i-1] dp[i]);//要么自己为子矩阵跟自己跟上一个联合起来为子矩阵
maxx=max(dp[i],maxx);
}
}
}
printf("%d",maxx);
return 0;
}