最大子矩阵SSL1209 (动态规划)

题目大意:

给出一个 N ( 2 ≤ N ≤ 100 ) N(2\leq N \leq100 ) N(2N100),并给出一个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=sumi1+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 i1,j 和以 i , j − 1 i,j-1 i,j1 为右下端点的矩阵重合而成的,同时,因为 i − 1 , j i-1,j i1,j 矩阵和 i , j − 1 i,j-1 i,j1 矩阵由重合部分,所以我们还得去掉一个重叠部分,就是加上以 i − 1 , j − 1 i-1,j-1 i1,j1 的矩阵,此时的和就是以 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=sumi1,j+sumi,j1sumi1,j1

有了 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,ysumx,jsumi,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的运用不仅仅在于刷模板,更在于在其他题目中灵活的运用。

如果你喜欢我的内容,那么也请支持一下他吧

猜你喜欢

转载自blog.csdn.net/SAI_2021/article/details/119856595