最大子矩阵(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++(附图文超详细讲解)(保姆级)
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
92 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
169 0
|
6月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
180 12
|
7月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
130 16
|
8月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
7月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
7月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
7月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
353 6