当前位置: 首页 > news >正文

leetcode_53 最大子数组和

1. 题意

给定一个数组,让你求最大子数组和;
所谓的子数组,就是数组中连续的一段。

2. 题解

非常经典的一道题目,值得反复把玩啊!!!

2.1 暴力枚举

首先我们想想怎么枚举子数组。

我们可以固定子数组的左端点,再依次处理子数组的右端点。

class Solution {
public:int maxSubArray(vector<int>& nums) {int n = nums.size();int sum = 0;int ans = nums[0];for (int i = 0; i < n; i++) {sum = 0;for (int j = i; j < n; ++j) {sum += nums[j];ans = max( sum, ans);}}return ans;        }
};

时间复杂度O(n2)O(n^2)O(n2)

2.2 动态规划

我们可以定义dp[n]dp[n]dp[n]为前nnn个数组的最大子数组和。

那么状态转移方程为

dp[n]={dp[n−1]+nums[n−1],dp[n−1]>0nums[n−1],dp[n−1]≤0\begin{equation*} dp[n] =\begin{cases} dp[n-1] +nums[n-1] , \quad dp[n-1] >0\\ nums[n-1], \quad dp[n-1] \le 0 \end{cases} \end{equation*} dp[n]={dp[n1]+nums[n1],dp[n1]>0nums[n1],dp[n1]0
最终的最大子数组和为
max⁡{dp[i]},1≤i≤arr.size\max \{ dp[i]\} , 1 \le i \le arr.size max{dp[i]},1iarr.size

class Solution {
public:int maxSubArray(vector<int>& nums) {int n = nums.size();vector<int> dp(n + 1, 0);dp[1] = nums[0];int ans = nums[0];for (int i = 1;i < n; ++i) {dp[i + 1] = dp[i] > 0 ? dp[i] + nums[i] : nums[i];ans = max(ans, dp[i + 1]);}return ans;        }
};

时间复杂度O(n)O(n)O(n), 空间复杂度O(n)O(n)O(n)

我们可以进行空间压缩, 用一个变量代替数组。

class Solution {
public:int maxSubArray(vector<int>& nums) {int n = nums.size();int premax = nums[0];int ans    = nums[0];for (int i = 1;i < n; ++i) {premax = premax > 0 ? premax + nums[i] : nums[i];ans = max(premax, ans);}return ans;        }
};

这样空间复杂度就优化到了O(1)O(1)O(1)

2.3 贪心

代码和动态规划的代码几乎一样,但是背后的思想却是不一样的。

就像评论区说的渣男渣女一样,之前的数组和如果小于0了;

那么在下一次元素求和中,我会将你果断的抛弃,我将重新进行选择。

因为你总不能阻止我奔向更好的人吧!

class Solution {
public:int maxSubArray(vector<int>& nums) {int ans = nums[0];int presum = nums[0];int n = nums.size();for (int i = 1; i < n; ++i) {if (presum < 0)presum = 0;presum += nums[i];ans = max(presum, ans);}return ans;}
};
2.4 分治

分治的思路跟归并排序、线段树的思想差不多。

首先分治的核心是把大的问题分成两个规模差不多一样大的子问题,

所以我们先要看怎么将大数组的最大子数组和给转换为小数组的

最大子数组和。

我们假设,数组范围为[l,r][l,r][l,r],分为两个子区间[l,mid],[mid+1,r][l,mid],[mid+1,r][l,mid],[mid+1,r]

再令msum(l,r)msum(l,r)msum(l,r)表示: [l,r][l,r][l,r]数组的最大子数组和。

msum(l,r)=max⁡{∑i=l′r′nums[i]∣l≤l′,r′≤r}msum(l,r) = \max \left\{ \sum_{i=l'}^{r'}nums[i]\ \Bigg|\ l \le l',r' \le r \right\} msum(l,r)=maxi=lrnums[i]  ll,rr

我们假设最大子数组最终的区间为[l′,r′][l',r'][l,r].

我们很容易的想到msum(l,mid)msum(mid+1,r)msum(l,mid)\ msum(mid+1,r)msum(l,mid) msum(mid+1,r)

可能是msum(l,r)msum(l,r)msum(l,r)的候选值。用更通俗的话来讲,子数组的最大子数组

和可能是数组的最大子数组和。

分别对应了[l′,r′]⊆[l,mid][l',r'] \subseteq [l,mid][l,r][l,mid][l′,r′]⊆[mid+1,r][l',r'] \subseteq [mid+1,r][l,r][mid+1,r]

除此之外,我们还有一种情况,那就是

最大子数组的区间跨越了两个区间,也就是l′≤mid∩r′≥mid+1l'\le mid \cap r' \ge mid+1lmidrmid+1

此时我们需要分到两个子区间进行处理,也就是[l′,mid][mid+1,r′][l',mid]\ [mid+1,r'][l,mid] [mid+1,r]

我们需要找到左下标l′∈[l,mid]l'\in[l,mid]l[l,mid]使得,∑i=l′midnums[i]\sum_{i=l'}^{mid} nums[i]i=lmidnums[i]最大;

同理我们需要找到右下标r′∈[mid+1,r]r'\in[mid+1,r]r[mid+1,r]使得,∑i=mid+1r′nums[i]\sum_{i=mid+1}^{r'} nums[i]i=mid+1rnums[i]最大。

这里我们作符号简化处理,

lmsum(l,r)lmsum(l,r)lmsum(l,r)表示数组中以lll开始的所有子数组中最大的子数组和;

lmsum(l,r)={∑i=lr′nums[i]∣l≤r′≤r}lmsum(l,r) = \left\{ \sum_{i=l}^{r'}nums[i] \ \Bigg | \ l \le r' \le r\right\} lmsum(l,r)=i=lrnums[i]  lrr

rmsum(l,r)rmsum(l,r)rmsum(l,r)表示数组中以rrr结尾的所有子数组 中最大子数组和。
rmsum(l,r)={∑i=l′rnums[i]∣l≤l′≤r}rmsum(l,r)=\left\{ \sum_{i=l'}^{r}nums[i] \ \Bigg |\ l \le l' \le r \right\} rmsum(l,r)={i=lrnums[i]  llr}

综合上面的分析,可以得到

msum(l,r)=max⁡{msum(l,mid),msum(mid+1,r),rmsum(l,mid)+lmsum(mid+1,r)}msum(l,r) =\\ \max \left\{ msum(l,mid), msum(mid+1,r),\\ rmsum(l,mid)+lmsum(mid+1,r)\right\} msum(l,r)=max{msum(l,mid),msum(mid+1,r),rmsum(l,mid)+lmsum(mid+1,r)}
前面两个子区间的最大子数组和直接递归即可。

后面的lmsum(mid+1,r)rmsum(l,mid)lmsum(mid+1,r)\ rmsum(l,mid)lmsum(mid+1,r) rmsum(l,mid)怎么求呢?

这里有两种方式:

第一种直接枚举;

对于lmsum(mid+1,r)lmsum(mid+1,r)lmsum(mid+1,r), 我们可以从左往右枚举r′,mid+1≤r′≤rr', mid+1 \le r' \le rr,mid+1rr, 求得区间和sum(mid+1,r′)sum(mid+1,r')sum(mid+1,r),取最大值即可;

对于rmsum(l,mid)rmsum(l,mid)rmsum(l,mid)同理,只是我们从右往左逆序枚举l′l'l,求得区间和sum(l′,mid)sum(l',mid)sum(l,mid), 取最大值即可。

下面是不那么重要的代码

class Solution {
public:int getMaxSubArray(const vector<int> &nums, int l, int r){if ( l == r)return nums[l];int mid = l + ((r -l) >> 1);int lsum = nums[mid];int tsum = nums[mid];for (int i = mid - 1; i >= l; --i) {tsum += nums[i];lsum = max( lsum, tsum );}int rsum = nums[mid + 1];tsum = nums[mid + 1];for (int i = mid + 2; i <= r; ++i) {tsum += nums[i];rsum = max( rsum, tsum);}int ans = lsum + rsum;ans = max(ans, getMaxSubArray(nums, l       , mid ));ans = max(ans, getMaxSubArray(nums, mid + 1 , r      ));return ans;}int maxSubArray(vector<int>& nums) {return getMaxSubArray( nums, 0, static_cast<int>(nums.size()) - 1);    }
};

时间复杂度O(nlog⁡n)O(n\log n)O(nlogn),空间复杂度O(log⁡n)O(\log n)O(logn)

第二种方式是递归的,对于lmsum(l,r)lmsum(l,r)lmsum(l,r)我们同样可以根据左右区间求得,

以左端点为开端的最大子数组要么是在左子区间,要么是左子区间加上右子区间的以左端点为开端的最大子数组。

这里我们就需要数组的区间和了,我们以sum(l,r)sum(l,r)sum(l,r)表示∑i=lrnums[i]\sum_{i=l}^{r}nums[i]i=lrnums[i]

叙述起来有点绕,用符号表示很简单也就是。

lmsum(l,r)=max⁡{lmsum(l,mid),sum(l,mid)+lmsum(mid+1,r)}lmsum(l,r) =\\\max\left\{ lmsum(l,mid), \\sum(l,mid)+lmsum(mid+1,r)\right\} lmsum(l,r)=max{lmsum(l,mid),sum(l,mid)+lmsum(mid+1,r)}

同理

rmsum(l,r)=max⁡{rmsum(mid+1,r),sum(mid+1,r)+rmsum(l,mid)}rmsum(l,r) =\\\max \left\{ rmsum(mid+1,r), \\ sum(mid+1,r) +rmsum(l,mid)\right\}rmsum(l,r)=max{rmsum(mid+1,r),sum(mid+1,r)+rmsum(l,mid)}

因此,对于一个区间,我们需要维护四个值

  • tsumtsumtsum:区间和
  • lmsumlmsumlmsum: 以左端点为起点的所有子数组的最大和
  • rmsumrmsumrmsum:以右端点为终点的所有子数组的最大和
  • sumsumsum: 区间的最大子数组和

用一个结构体进行存储

代码如下,差不多就是官解同款了!

class Solution {
public:struct status {status(int msum_,int lmsum_, int rmsum_,int tsum_):msum( msum_), lmsum( lmsum_), rmsum( rmsum_ ), tsum(tsum_) {}int msum;int lmsum;int rmsum;int tsum;};status getMaxSubArray(const vector<int> &nums, int l, int r){if ( l == r)return status{ nums[l], nums[l], nums[l], nums[l]};int mid = l + ( (r - l) >> 1);auto ls = getMaxSubArray( nums, l, mid);auto rs = getMaxSubArray( nums, mid + 1, r);status s = ls;s.tsum = ls.tsum + rs.tsum;s.lmsum = max(ls.lmsum, ls.tsum + rs.lmsum);s.rmsum = max(rs.rmsum, ls.rmsum + rs.tsum);s.msum  = max( ls.rmsum + rs.lmsum, max(ls.msum, rs.msum) );return s;}int maxSubArray(vector<int>& nums) {return getMaxSubArray( nums, 0, static_cast<int>(nums.size()) - 1).msum;    }
};

时间复杂度O(n)O(n)O(n), 空间复杂度O(log⁡n)O(\log n)O(logn)
不太清楚怎么分析出O(n)O(n)O(n)的,有问题找官解。

2.4 前缀和+贪心

这个解法在评论区中看到的0x3f的解法;

由于本质是找一个区间和[l′,r′][l',r'][l,r]使得它的和在[l,r][l,r][l,r]的所有子区间中最大。

我们可以累加得到前缀和presumpresumpresum,从而将问题从找区间最大和转化为找到一对i,ji,ji,j使得presum[j]−presum[i]presum[j] -presum[i]presum[j]presum[i]最大。

区间和最大变成数组中后前值差值最大。

而这个问题其实就是lc121 买卖股票的最佳时机。

也就是贪心的处理了,唯一不同的至少要买卖一次。

先给出两次遍历的代码,先计算前缀和,再贪心处理。

class Solution {
public:int maxSubArray(vector<int>& nums) {int pre = 0;for (auto &v: nums) {pre += v;v = pre;}int n = nums.size();int mn = 0;int ans = nums[0];for (int i = 0;i < n; ++i) {ans = max(ans, nums[i] - mn);mn  = min(mn, nums[i]);}return  ans;}
};

边计算前缀和,边贪心处理,注意对min_presummin\_{presum}min_presum的边界处理。

class Solution {
public:int maxSubArray(vector<int>& nums) {int n = nums.size();int ans = nums[0];int mn = 0;for (int i = 0;i < n; ++i) {if (i > 0)nums[i] += nums[i - 1];ans = max(nums[i] - mn, ans);mn  = min( mn, nums[i]);}return ans;}
};

参考

lc官解
0x3f

http://www.dtcms.com/a/288206.html

相关文章:

  • MySQL(145)如何升级MySQL版本?
  • 研华PCI-1285/1285E 系列------(一概述)
  • VIT速览
  • 用 Numpy 手动实现矩阵卷积运算
  • 汽车ECU控制器通信架构
  • 车载诊断架构 --- 故障码DTC严重等级定义
  • LVS部署模式NAT集群案例
  • WSL在 Windows 上使用 Linux 工具链和开发环境
  • 慕尼黑工业大学具身机器人实时环境探索!FindAnything:基于开放词汇对象中心映射的机器人任意环境认知与导航
  • FLASH:GPU 集群全连接通信的近最优极速调度
  • Keil编译文件格式转换全解析
  • 5 基于STM32单片机的绝缘检测系统设计(STM32代码编写+手机APP设计+PCB设计+Proteus仿真)
  • QT窗口(5)-对话框
  • 基于朴素贝叶斯的姓名性别预测系统
  • 如何构建未来的人-AI-环境智能教育生态系统
  • Java并发8--并发安全容器详解
  • 关于Vuex
  • uhd_find_devices有serial但是GNU Radio显示find no devices
  • Vue rem回顾
  • YOLOv8中添加SENet注意力机制
  • XSS-Labs 各关卡测试过程
  • 统计学习方法
  • 如何解决 ext4 文件系统的元数据损坏问题
  • 【深度强化学习】MIP-DQN 实现案例(完整Python代码)
  • [spring6: IntroductionAdvisor IntroductionInterceptor]-源码分析
  • C++编程学习(第11天)
  • Patch-wise Structural:一种引入局部统计特性的时序预测损失函数
  • eNSP综合实验(DNCP、NAT、TELET、HTTP、DNS)
  • 定时器中BDTR死区时间和刹车功能配置
  • debian的pulseaudio删掉也没事