当前位置: 首页 > 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.xdnf.cn/news/1152667.html

相关文章:

  • 学习 Python 爬虫需要哪些基础知识?
  • KVM中使用桥接模式.运维就业技术教程
  • Linux操作系统之线程(三)
  • 定时器与间歇函数
  • STC增强型单片机寄存器 PWM EEPROM TMOD TCON
  • 在摄像机视图中想像在普通 3D 视口里那样随意移动
  • 【音视频协议篇】RTSP系列
  • XSS相关理解
  • Kotlin main函数
  • Chris Fraser | 中国早期思想中墨家与荀子的知识论
  • 生成式引擎优化(GEO)权威指南:提升网站在AI搜索中的可见性
  • HTTP与HTTPS技术细节及TLS密钥交换与证书校验全流程
  • CSS面试题及详细答案140道之(81-100)
  • 零基础学习性能测试第二章-linux服务器监控:网络iftop
  • Keil编译文件格式转换全解析
  • 滤波电路Multisim电路仿真实验汇总——硬件工程师笔记
  • XSS的反射型、DOM型、存储型漏洞
  • 语音识别技术:从声音到文字的 AI 魔法
  • 强化学习入门-免模型预测
  • Django母婴商城项目实践(十一)- 用户信息模块之用户登录注册
  • [每日随题11] 贪心 - 数学 - 区间DP
  • 让Logo/文字“自己画自己”!✨
  • Linux某个进程CPU占用率高原因定位手段
  • 从零手写红黑树(C++实现详解)
  • 142. 环形链表 II
  • FPGA自学——整体设计思路
  • Python Pandas读取Excel表格中数据并根据时间字段筛选数据
  • 使用 validation 框架生成一个校验参数是否在枚举内的校验器
  • 结合python面向对象编程,阐述面向对象三大特征
  • 【RK3576】【Android14】调试方法