子数组系列一(数组中连续的一段)
点赞 👍👍收藏 🌟🌟关注 💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.最大子数组和
题目链接: 53. 最大子数组和
题目分析:
子数组 是数组中的一个连续部分。子数组最少包含一个元素也就是说本身也是子数组!
算法原理:
1.状态表示
像这种研究的是子数组这样的模型,状态表示 依旧可以用 经验 + 题目要求
以 i 位置为结尾,巴拉巴拉。
以 i 位置为结尾,我们要的是最大子数组和,是不是先把以 i 位置为结尾的所有子数组拿到。单独 i 元素 是一个子数组,还有前面以 i 元素 为结尾的所有子数组,我要的是一个最大和。
dp[i] 表示:以 i 位置元素为结尾的所有子数组中的最大和。
2.状态转移方程
找出所有子数组最大和就可以了,所有子数组可以划分两大类。第一类就是单独自己构成子数组,第二类就是它自己与前面元素的结合构成子数组。所以可以根据长度来划分,长度为1 单独自己构成子数组,子数组最大和 nums[i],长度大于1,nums[i]一定是要的,然后在找到以 i - 1 位置元素为结尾的所有子数组中的最大和,两个相加就可以了,而dp[i - 1] 就是以 i - 1 位置元素为结尾的所有子数组中的最大和,因此 dp[i + 1] + nums[i],我要求的是 i 位置元素为结尾的所有子数组中的最大和。因此两种情况取最大就好了
3.初始化
多申请一个节点
- 里面的值要保证后面填表的正确
- 下标映射关系
先考虑如果不多加一个节点第一个位置应该填多少呢?是不是填自己本身啊,为了不让多加的节点影响后面的填表正确,因此可以给 0。注意下标映射关系,我们躲开了一个空间相当于整体往右移动一位,如果要回原数组下标要 -1
4.填表顺序
从左往右
5.返回值
注意这里可不是返回最后一个位置的值,因为最大子数组可能在这个数组中任何一个地方。所以返回的是dp中最大值
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 1.创建 dp 表
// 2.初始化
// 3.填表
// 4.返回值
int n = nums.size();
vector<int> dp(n + 1);
for(int i = 1; i <= n; ++i)
{
dp[i] = max(nums[i - 1], dp[i - 1] + nums[i - 1]);
}
int ret = INT_MIN;
for(int i = 1; i <= n; ++i)
ret = max(ret,dp[i]);
return ret;
}
};
2.环形子数组的最大和
题目链接: 918. 环形子数组的最大和
题目分析:
给一个环形数组,让找子数组最大和。子数组可能是中间连续部分,包括自己本身也是子数组。子数组可能是绕一圈的。
算法原理:
如果直接就在环形上面做有很多边界问题需要考虑。前面我们做过一道题 " 打家劫舍II " 也是一个环形的,我们是将 " 打家劫舍II " 转换成 " 打家劫舍I ",也就是将一个环形数组转换成一个普通数组,然后在普通数组上用" 打家劫舍I "来做。
这里也是将环形数组转换成一个普通数组来解决问题。
分类讨论一:子数组没有跨过数组的边界,在数组的中间。
这个就和上面求最大子数组和一模一样。
分类讨论二:子数组跨过数组边界,在数组两端
那前面一部分和后面一部分求最大和,直接求是非常恶心的,但是可以看到空白的地方正好在数组内部,整段数组的和是一个定值 sum,如果阴影部分的和是最大的,那空白地方和是最小的!所以只要在整个数组找一个连续的区间使和最小,那剩下两部分拼起来和就是最大的。
因此这里就转化成两个问题,求子数组和最大值,求子数组和最小值。都没环形无关了。求子数组和最小值然后用sum减去这个最小值就是两端最大值。最后返回这两种情况的最大值就好了。
1.状态表示
经验 + 题目要求
以 i 位置为结尾,巴拉巴拉。
以 i 位置为结尾,我们要的是最大子数组和,是不是先把以 i 位置为结尾的所有子数组拿到。单独 i 元素 是一个子数组,还有前面以 i 元素 为结尾的所有子数组,我要的是一个最大和。
f[i] 表示:以 i 位置元素为结尾的所有子数组中的最大和。
同理:
g[i] 表示:以 i 位置元素为结尾的所有子数组中的最小和。
2.状态转移方程
我们可以把所有子数组分成两类,第一类是自己本身就是子数组,第二类就是自己加上前面一个或者两个等等构成的子数组。
3.初始化
注意到填表填 0 位置的时候会越界。我们这里可以多申请一个节点
- 里面的值要保证后面填表的正确
- 下标映射关系
先考虑如果不多加一个节点第一个位置应该填多少呢?是不是填自己本身啊,为了不让多加的节点影响后面的填表正确,因此可以给 0。f求最大和可以给0,g求最小和也可以给0。
注意下标映射关系,我们躲开了一个空间相当于整体往右移动一位,如果要回原数组下标要 -1
4.填表顺序
从左往右
5.返回值
回到分类讨论,我们是把问题分成两个子问题,一个是在数组不跨边界找最大子数组和 记为 fmax,一个在数组跨界也就是数组两端找子数组最小和 记为 gmin,然后sum - gmin 就是两端子数组最大和。然后比较一下。
但是如果数组全都是负数的,这样就有些问题。比如:
cpp
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
// 1. 创建 dp 表
// 2.初始化
// 3.填表
// 4.返回值
int n = nums.size();
vector<int> f(n + 1);
auto g = f;
int sum = 0;
int fmax = INT_MIN, gmin = INT_MAX;
for(int i = 1; i <= n; ++i)
{
f[i] = max(nums[i - 1], f[i - 1] + nums[i - 1]);
g[i] = min(nums[i - 1], g[i - 1] + nums[i - 1]);
fmax = max(fmax, f[i]);
gmin = min(gmin,g[i]);
sum += nums[i -1];
}
return sum == gmin ? fmax : max(fmax, sum - gmin);
}
};
3.乘积最大子数组
题目链接: 152. 乘积最大子数组
题目分析:
这道题和上面一样思想,不过求得是子数组中最大乘积
算法原理:
1.状态表示
经验 + 题目要求
dp[i] 表示 :以 i 位置元素为结尾的所有子数组中最大乘积。
现在我们先以这个状态表示分析状态转移方程。
i 本身也是子数组。还有以 i 位置为结尾的子数组,以 i 位置为结尾的子数组我们求最大,nums[i] 肯定要乘上,剩下的子数组都有一个特点就是以 i - 1结尾,所以我们可以将以i - 1位置为结束的子数组乘积最大拿到然后在乘nums[i],而dp[i-1]就是i - 1位置为结束的子数组乘积最大。
但是到这里先停住,我们发现了不对劲。i 位置 如果是 > 0的数,用dp[i-1]*nums[i]可以得到更大的数,但是如果 i 位置是 < 0 ,dp[i-1]*nums[i]就是一个很小的数。
因此我们的状态表示还不过,还要细分下去:
f[i] 表示:以 i 位置元素为结尾的所有子数组中最大乘积。
g[i] 表示:以 i 位置元素为结尾的所有子数组中最小乘积。
2.状态转移方程
有上面的基础,这里不在细说了
3.初始化
注意到填表填 0 位置的时候会越界。我们这里可以多申请一个节点
- 里面的值要保证后面填表的正确
- 下标映射关系
先不考虑虚拟节点填多少,先考虑不添加虚拟节点第一个位置填多少,是不是填自己本身啊。为了使填的值不影响后填表,可以给1,
下标映射,整体往右移动一位,回原本要往左移动一位
4.填表顺序
从左往右,两个表一起填
5.返回值
要的是最大乘积,f中放的是最大乘积,遍历一下f,找到最大乘积
cpp
class Solution {
public:
int maxProduct(vector<int>& nums) {
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int n = nums.size();
vector<double> f(n + 1);
auto g = f;
f[0] = g[0] = 1;
double ret = INT_MIN;
for(int i = 1; i <= n; ++i)
{
double x = nums[i - 1], y = f[i - 1] * nums[i - 1], z = g[i - 1] * nums[i - 1];
f[i] = max(x, max(y,z));
g[i] = min(x, min(y,z));
ret = max(ret,f[i]);
}
return ret;
}
};
4.乘积为正数的最长子数组长度
题目链接: 1567. 乘积为正数的最长子数组长度
题目分析:
求乘积为正数最大子数组长度。
算法原理:
1.状态表示
经验 + 题目要求
dp[i] 表示 :以 i 位置元素为结尾的所有子数组中乘积为正数的最大长度
还是先以这个分析
i 本身也是子数组,然后为空看nums[i] 是正还是负。还有以 i 位置为结尾的子数组,求所有子数组中乘积为正数的最大长度,nums[i]要分正还是负,剩下的子数组都有一个特点就是以 i - 1结尾,所以我们可以将以 i -1 位置元素为结尾的所有子数组中乘积为正数的最大长度拿到,然后在加上nums[i]为正为负的情况。
我们由上面的状态表示去分析,发现当子数组长度大于1,nums[i] < 0,分析不下去了,如果前面 i - 1 位置有乘积为负数最大长度,负数乘nums[i] > 0,乘积也大于0,那长度不也应该加+1嘛。
所以一个状态表示不够。
f[i] 表示:以 i 位置元素为结尾所有子数组中乘积为正数的最大长度
g[i] 表示:以 i 位置元素为结尾所有子数组中乘积为负数的最大长度
2.状态转移方程
当长度大于1,nums[i] < 0,所以我们要找到的是 i - 1位置元素为结尾所有子数组中乘积为负数的最大长度,但是要注意的是,万一 g[i - 1] 位置为0呢?也就是说前面根本就没有乘积为负数的最大长度。那nums[i] 又小于 0,乘积肯定不为正!
因此这里不能这样写,需要判断一下:
再来分析一下g
长度大于1,nums[i] > 0,所以我们要找到的是 i - 1位置元素为结尾所有子数组中乘积为负数的最大长度,但是万一 g[i - 1] 是 0呢?乘积不可能为负,而nums[i] > 0,这种情况以 i 位置元素为结尾所有子数组中乘积为负数的最大长度就是0,因此下面写的就不对。
所以最终状态转移方程如下:
有人可能会有疑问,为什么f 当长度大于1,nums[i] > 0,不去考虑 f[i -1] 为0的情况呢,其实我们已经考虑过了,f[i -1] 为0就为0好了,反正nums[i] > 0至少有1个。
同样g 当长度大于1,nums[i] < 0, 不去考虑 f[i -1]为0的情况,都是一样的,f[i -1] 为0就为0好了,反正nums[i] < 0至少有1个。如果大于0 就加上好了。
下面可以整体一下状态转移方程
f[i],可以把 nums[i] > 0 两种情况合并成 f[i - 1] + 1,因为nums[i] > 0 至少保证有一个了,如果f[i - 1] == 0 最终结果可以是两个1中任何一个,如果 f[i - 1] > 0,最大值是由f[i - 1] + 1来决定,所以不用考虑上面单独1了。
nums[i] < 0 两种情况合并成 g[i -1] == 0 ?:g[i -1] + 1,要么是0,要么是比0更大的数,最大值由g[i -1] == 0 ?:g[i -1]决定
同理g也是这样合并
3.初始化
注意到填表填 0 位置的时候会越界。我们这里可以多申请一个节点
- 里面的值要保证后面填表的正确
- 下标映射关系
考虑没有填表的时候g第一个位置填什么,nums[0] > 0 填0,nums[0] < 0填1。
代入是不是f[i - 1] 和 g[i -1] 的位置都填0啊,所以虚拟节点填0。
4.填表顺序
从左往右,两个表一起填
5.返回值
f表中最大值
cpp
class Solution {
public:
int getMaxLen(vector<int>& nums) {
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int n = nums.size();
vector<int> f(n + 1), g(n + 1);
int ret = INT_MIN;
for(int i = 1; i <= n; ++i)
{
//不要考虑nums[i - 1] == 0的情况,因为初始化为0
if(nums[i - 1] > 0)
{
f[i] = f[i - 1] + 1;
g[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;
}
else if(nums[i - 1] < 0)
{
f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;
g[i] = f[i - 1] + 1;
}
ret = max(ret, f[i]);
}
return ret;
}
};