最大子矩阵(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++(附图文超详细讲解)(保姆级)
|
22天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4天前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
30 6
|
24天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
22天前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
22天前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
2月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
81 19
|
2月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
70 13
|
2月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
63 5
|
2月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
48 5