最大子矩阵(C/C++)

简介: 最大子矩阵(C/C++)

简介:

最大子矩阵问题是指在一个矩阵中找到一个子矩阵,使得该子矩阵的元素之和最大。

解决该问题的常用方法是使用动态规划。先计算出每一行的前缀和,然后对于每一列的起始和终止位置,计算出该区域内每一行的和,得到一个一维数组。再对该一维数组使用动态规划求解最大子数组和的问题,得到最大子矩阵的元素之和。

该问题也可以使用暴力搜索的方法,枚举所有可能的子矩阵,计算它们的元素之和,并找到最大值。但是由于时间复杂度过高,所以在实际应用中很少使用。

下面我们将以例题的形式时间复杂度由高到底给大家讲解几种方法。

例题 P1719 最大加权矩形

题目描述

为了更好的备战 NOIP2013,电脑组的几个女孩子 LYQ,ZSC,ZHQ 认为,我们不光需要机房,我们还需要运动,于是就决定找校长申请一块电脑组的课余运动场地,听说她们都是电脑组的高手,校长没有马上答应他们,而是先给她们出了一道数学题,并且告诉她们:你们能获得的运动场地的面积就是你们能找到的这个最大的数字。


校长先给他们一个 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。

几个女孩子有点犯难了,于是就找到了电脑组精打细算的 HZH,TZY 小朋友帮忙计算,但是遗憾的是他们的答案都不一样,涉及土地的事情我们可不能含糊,你能帮忙计算出校长所给的矩形中加权和最大的矩形吗?

输入格式

第一行:n,接下来是 n 行 n 列的矩阵。

输出格式

最大矩形(子矩阵)的和。

输入
4
0 -2 -7 0
 9 2 -6 2
-4 1 -4  1 
-1 8  0 -2
输出
15
说明/提示

1≤n≤120

一、暴力求解

所谓的暴力求解就是分别枚举(x1,y1)、(x2,y2),四个for循环分别枚举每一个点的坐标,两个for循环去遍历这个矩阵的每一个点的权值,所以时间复杂度再O(n^6)。

代码实现:
#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),在解决此题也不能通过,时间复杂度也是比较高的。前缀和二维也就是通过一维滚动数组来实现。此一维前缀和优化,一般是不会用的,网上对它介绍的也比较少,大家了解一下即可。

代码实现:
#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]

代码实现:
#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数组表示,每一个小子矩阵还可以与相邻的子矩阵构成子矩阵,每一次与自己比较大小。

代码实现:
#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;
}

相关文章
|
人工智能 JavaScript C++
蓝桥杯统计子矩阵前缀和C++(附图文超详细讲解)(保姆级)
蓝桥杯统计子矩阵前缀和C++(附图文超详细讲解)(保姆级)
|
6天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
29 4
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
23 4
|
30天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
30天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
30天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
19 1