最大子矩阵SSL1209 (动态规划)
题目大意:
给出一个
N
(
2
≤
N
≤
100
)
N(2\leq N \leq100 )
N(2≤N≤100),并给出一个N*N的矩阵,矩阵中的数为[-127,127]之间。求出矩阵中一块子矩阵的最大和。
比如:
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
和最大的子矩阵应该是这个:
9 2
-4 1
-1 8
它的和是15。
解题思路:
看到这一题,我们可以考虑用枚举来求最大子矩阵。但是,如果每次尝试都进行一次求和,时间复杂度过不去,因此我们需要那个在神坛上的工具,二维前缀和。
前缀和我会,就是把前面的数加起来嘛,但是二维的,边长是会变化的呀,怎么求呢?这里简单阐述一下。
首先我们先来说一下一维前缀和的基础求法:设
s
u
m
i
sum_{i}
sumi表示从1到
i
i
i 的前缀和。
那么:
s u m i = s u m i − 1 + a i sum_{i}=sum_{i-1}+a_i sumi=sumi−1+ai
显然,二维前缀和因为有了行和列这组不同的因数,那么
s
u
m
sum
sum 的参数就该改为
s
u
m
i
,
j
sum_{i,j}
sumi,j,表示以第
1
1
1 行第
1
1
1 列为左上角端点,第
i
i
i 行第
j
j
j 列为右下角端点所形成的矩阵的和。
状态参数定义好了,接下来就是
s
u
m
sum
sum 数组求法的递推式了(千万不要死算,否则时间效率极低)
这时候就需要一定的平面想象力了,我们知道当前
s
u
m
i
,
j
sum_{i,j}
sumi,j 的状态必须要有 前面所算的
s
u
m
sum
sum 推出,那么到底该怎么算呢?
其实,以
i
,
j
i,j
i,j 为右下端点的矩阵,是由以
i
−
1
,
j
i-1,j
i−1,j 和以
i
,
j
−
1
i,j-1
i,j−1 为右下端点的矩阵重合而成的,同时,因为
i
−
1
,
j
i-1,j
i−1,j 矩阵和
i
,
j
−
1
i,j-1
i,j−1 矩阵由重合部分,所以我们还得去掉一个重叠部分,就是加上以
i
−
1
,
j
−
1
i-1,j-1
i−1,j−1 的矩阵,此时的和就是以
i
,
j
i,j
i,j 为右下端点的矩阵。
故有二维前缀和递推式:
s u m i , j = s u m i − 1 , j + s u m i , j − 1 − s u m i − 1 , j − 1 sum_{i,j}=sum_{i-1,j}+sum_{i,j-1}-sum_{i-1,j-1} sumi,j=sumi−1,j+sumi,j−1−sumi−1,j−1
有了 s u m i , j sum_{i,j} sumi,j 的前缀和数组,求一个以 i , j i,j i,j 为左上端点,以 x , y x,y x,y 为右下端点的矩阵就不难了。
s u m x , y − s u m x , j − s u m i , y + s u m i , j sum_{x,y}-sum_{x,j}-sum_{i,y}+sum_{i,j} sumx,y−sumx,j−sumi,y+sumi,j
求得了以 i , j i,j i,j 为右下端点的矩阵前缀和,我们只需要用循环枚举左上端点和右下端点找最大和就行了,时间复杂度为 O ( n 4 ) O (n^4) O(n4)。
二维前缀和CODE:
void qianzhui()
{
for(int i=n;i>=1;i--)
{
for(int j=n;j>=1;j--)
{
map[i][j]=map[i+1][j]+map[i][j+1]-map[i+1][j+1]+map[i][j];
//枚举右下端点求二维前缀和
}
}
}
这个复杂度交上去只能踩线过,因此我们考虑效率更高的动态规划。
依然考虑使用前缀和。
首先我们我们不考虑上述所说的二维前缀和的求法,只考虑线性的一维前缀和——就比如 s u m i sum_{i} sumi 表示从 1 1 1 到 i i i的前缀和,他在这一题中表示的意义就是 以 1 , 1 1,1 1,1 为左上端点, 1 , i 1,i 1,i 为右下端点的矩阵。
现在我们已经拥有了属于
(
1
,
1
)
(
1
,
i
)
(1,1) (1,i)
(1,1)(1,i) 范围的所有矩阵,现在我们要做的就是,如何让线性的前缀和达到遍通整个矩阵的效果。
我们一步一步来,尝试将前缀和能覆盖的范围扩张到两层,也就是说让一个一维前缀和同时表示右下端点为
1
,
i
1,i
1,i 以及右下端点
2
,
i
2,i
2,i的所有矩阵。
要完成这样不可思议的转换,我们需要跳出前缀和的常规。
首先,确定子矩阵的上行和下行,即双重循环枚举出所有可能。
只确定了子矩阵的上下行是不行的,最大和子矩阵可能还在目前确定的子矩阵中( 即更小的列数 ),因为行都能枚举到,只要再处理列就可以了。
怎么处理列呢?这里就利用一个特性,对已经确定好上行和下行的子矩阵进行压缩。所谓压缩,就是对确定好的子矩阵每一列前缀和放到一个一维数组中。为什么这样可以呢?我是这样想的,行数是枚举的,包括了所有可能,对于每列,一定是要求和的,只是取多少列的问题,所以干脆转化成一维的问题,也就是最大连续子序列和的问题。因为,当对前缀和进行压缩后,最大连续子序列就是原本的一个矩阵
我们算算,枚举上行和下行的时间复杂度为 O ( n 2 ) O (n^2) O(n2),普通的最长连续子序列DP的时间复杂度为 O ( n ) O (n) O(n) ,那么总的时间复杂度就是 O ( n 3 ) O (n^3) O(n3),明显比裸的枚举要高效不少
经过DP优化,叫上OJ的时候心态再也不忐忑,可以愉快AC。
AC CODE:
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int n,b,m,map[110][110]={0},a[110][110]={0};
int dp[110]={0},ans=0;
void input()
{
cin>>n;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
cin>>b;
map[i][j]=b; //存储矩阵地图
}
}
}
void qianzhui(int x)
{
for(int i=1;i<=n;i++)
a[x][i]+=a[x-1][i]+map[x][i]; //压缩前缀和
}
void DP(int x) //最大子序列
{
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
if(dp[i-1]+a[x][i]<0)
dp[i]=0;
else
dp[i]=dp[i-1]+a[x][i];
ans=max(ans,dp[i]);
}
}
void normal()
{
for(int i=1;i<=n;i++)
{
memset(a,0,sizeof(a));
for(int j=i;j<=n;j++) //枚举矩阵上下行,取最大和答案存入ans
{
qianzhui(j);
DP(j);
}
}
cout<<ans;
}
int main()
{
input();
normal();
return 0;
}
总结:
DP的运用不仅仅在于刷模板,更在于在其他题目中灵活的运用。
如果你喜欢我的内容,那么也请支持一下他吧
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 易语言 —— 开山篇