目录
题目一:最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1] 输出:1
示例 3:
输入:nums = [5,4,-1,7,8] 输出:23
题目很简单,找出数组中和最大的连续子数组
①状态表示
以 i 位置为结尾,.....
②状态转移方程
此时可以有两种情况, i 位置结尾的子数组长度为1只有 i 位置的元素,或是子数组的长度大于1
dp[i] = max(nums[i], dp[i-1] + nums[i])
③初始化
初始化可以在dp表前面加上虚拟节点,dp表的下标从1开始表示实际的值
所以为了保证后面的填表是正确的,dp[0]应该置为0
并且也需要注意下标的映射关系
④填表顺序
从左往右填写
⑤返回值
返回整个dp表里的最大值
代码如下:
class Solution
{
public:
int maxSubArray(vector<int>& nums)
{
int n = nums.size(), ret = INT_MIN;
// 加上虚拟节点,初始化默认为 0,所以不需要写dp[0] = 0
vector<int> dp(n + 1);
for(int i = 1 ; i <= n; i++)
{
// 下标映射需要减 1,每填一个更新一个最大值
dp[i] = max(nums[i-1], dp[i-1] + nums[i-1]);
ret = max(ret, dp[i]);
}
return ret;
}
};
不需要虚拟节点也可以,代码如下:
class Solution
{
public:
int maxSubArray(vector<int>& nums)
{
// 最开始需要初始化dp[0],此时的最大值ret就是nums[0]
int n = nums.size(), ret = nums[0];
vector<int> dp(n);
dp[0] = nums[0];
for(int i = 1 ; i < n; i++)
{
dp[i] = max(nums[i], dp[i-1] + nums[i]);
ret = max(ret, dp[i]);
}
return ret;
}
};
题目二:环形子数组的最大和
给定一个长度为 n
的环形整数数组 nums
,返回 nums
的非空 子数组 的最大可能和 。
环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i]
的下一个元素是 nums[(i + 1) % n]
, nums[i]
的前一个元素是 nums[(i - 1 + n) % n]
。
子数组 最多只能包含固定缓冲区 nums
中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j]
,不存在 i <= k1, k2 <= j
其中 k1 % n == k2 % n
。
示例 1:
输入:nums = [1,-2,3,-2] 输出:3 解释:从子数组 [3] 得到最大和 3
示例 2:
输入:nums = [5,-3,5] 输出:10 解释:从子数组 [5,5] 得到最大和 5 + 5 = 10
示例 3:
输入:nums = [3,-2,2,-3] 输出:3 解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3
这道题与上一题的区别就是,这道题是环形的数组,也就是最后一个元素之后可以继续选第一个元素作为子数组的元素
此时会分为下面两种情况:
f:第一种在数组中间找到了和最大的子数组,此时与上一题的方法相同
g:第二种在两边找到了和最大的子数组,此时可以转换思路,在两边找到了最大的子数组和,就说明中间是最小的子数组和,所以这种情况就是在中间找到最小的子数组和即可
①状态表示
②状态转移方程
同样,不论是 f[i] 还是 g[i] 都可以分为两类:
1、单独 i 位置自己一个元素,长度等于1
2、i 位置及之前的元素,长度大于1
f[i]:
长度为1:nums[i]
长度大于1:nums[i] + f[i-1]
f[i] = max(nums[i], nums[i] + f[i-1])
g[i]:
长度为1:nums[i]
长度大于1:nums[i] + g[i-1]
f[i] = min(nums[i], nums[i] + g[i-1])
③初始化
观察状态转移方程可以知道,需要初始化 0 位置的值,此时可以加上虚拟节点
f[0]和g[0]这两个虚拟节点,都可以初始化为0,此时不会影响后面填表的正确性
加上虚拟节点后,需要注意下标的映射关系,所以需要需要nums时,需要 - 1
④填表顺序
从左往右
⑤返回值
返回值需要特殊注意
常规情况下:
1、找到 f表 的最大值 -> fmax
2、找到 g表 的最小值 -> gmin -> sum - gmin
接着比较 fmax 和 sum - gmin 的最大值,返回即可
但是有个特殊情况:如果nums数组中全部为负数,这时 sum == gmin,sum - gmin 就等于0了, 明显不符合结果,所以需要特殊处理一下, 判断 sum 如果等于 gmin,就直接返回 fmax 就可以了
代码如下:
class Solution
{
public:
int maxSubarraySumCircular(vector<int>& nums)
{
int sum = 0, n = nums.size(), fmax = INT_MIN, gmin = INT_MAX;
for(auto& it : nums) sum += it;
vector<int> f(n + 1), g(n + 1);
for(int i = 1; i <= n; i++)
{
f[i] = max(nums[i-1], f[i-1] + nums[i-1]);
fmax = max(fmax, f[i]);
g[i] = min(nums[i-1], g[i-1] + nums[i-1]);
gmin = min(gmin, g[i]);
}
// 返回前需要判断
return sum == gmin ? fmax : max(fmax, sum - gmin);
}
};
题目三:乘积最大子数组
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续
子数组
(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1] 输出: 0 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
①状态表示
以 i 位置为结尾 ....
②状态转移方程
同样可以分为两类:子数组是单独的自己(长度为1),子数组除了自己还有其他元素(长度大于1)
第一种情况长度为1,没有特殊情况,而第二种长度大于1会有不同的情况:
因为所求的是乘积,表示应为 f[i - 1] * nums[i],但是这里的nums[i]有可能是负数,此时越乘越小了,所以这里需要分类讨论,需要判断nums[i]是大于0还是小于0的
如果nums[i]是大于0的,那就取 f[i-1],此时两个正数相乘是最大的,f[i-1] * nums[i]
如果nums[i]是小于0的,那就取 g[i-1],因为小于0的数乘最大值,越乘越小,所以应该乘最小值,g[i-1] * nums[i]
g表和f表刚好相反,就不详细描述了,f表是求最大,所以g表就是求最小
所以状态转移方程为:
f[i] = max(nums[i], f[i-1] * nums[i], g[i-1] * nums[i]);
g[i] = min(nums[i], g[i-1] * nums[i], f[i-1] * nums[i]);
③初始化
i 位置需要用到 i - 1 位置的值,所以需要初始化第一个位置,这里继续采用加上虚拟节点的方式
原本不加虚拟节点时,第一个位置应该是它本身nums[0],所以此时加上虚拟结点,也要保证f[1]和g[1]是nums[0],所以 f[0] 和 g[0] 的初始值应为1,因为此时1乘任何数都是任何数,不会影响后续的结果
④填表顺序
从左往右,两个表一起填
⑤返回值
返回值是:f表中的最大值
代码如下:
class Solution
{
public:
int maxProduct(vector<int>& nums)
{
int n = nums.size(), ret = INT_MIN;
vector<int> f(n+1), g(n+1);
f[0] = g[0] = 1;
for(int i = 1; i <= n; i++)
{
// 映射的下标需要减 1
int x = nums[i-1], y = f[i-1]*nums[i-1], z = g[i-1]*nums[i-1];
f[i] = max(max(x, y), z);
g[i] = min(min(x, y), z);
ret = max(ret, f[i]);
}
return ret;
}
};
题目四:乘积为正数的最长子数组长度
给你一个整数数组 nums
,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。
请你返回乘积为正数的最长子数组长度。
示例 1:
输入:nums = [1,-2,-3,4] 输出:4 解释:数组本身乘积就是正数,值为 24 。
示例 2:
输入:nums = [0,1,-2,-3,-4] 输出:3 解释:最长乘积为正数的子数组为 [1,-2,-3] ,乘积为 6 。 注意,我们不能把 0 也包括到子数组中,因为这样乘积为 0 ,不是正数。
示例 3:
输入:nums = [-1,-2,-3,0,1] 输出:2 解释:乘积为正数的最长子数组是 [-1,-2] 或者 [-2,-3] 。
①状态表示
因为一个状态表示无法解决问题,本题与之前的题目一样,都需要两个状态转移方程,一个表示正数的,一个表示负数的
②状态转移方程
这里需要注意的就是:
如果nums[i] < 0时,此时需要找以 i - 1 位置为结尾的所有子数组乘积为负数的最长长度,但是前面时有可能不存在负数,此时 g[i-1] + 1 = 1,不符合要求,所以当 g[i-1] == 0时,此时 f[i] = 0 即可
而上面的 num[i] > 0 就不需要考虑前面不存在乘积为正数子数组的情况,因为如果前面不存在,此时 f[i-1] == 0,最终 f[i] = 0 + 1 = 1,符合正常结果,所以不需要考虑
上面的第一种和第三种都是 nums[i] > 0的情况,而这两种情况的最大值是由第三种情况决定的,所以可以合并为一个
第二种和第四种同理可知,最大值是由第四种决定的,所以也合并为一个
同理可得:g[i]的状态转移方程如下:
③初始化
使用 i 位置需要用到 i - 1 位置的值,所以需要初始化,这里依旧采用虚拟头结点的方式
使用虚拟投头结点,只需要保证不影响后面的填表的结果即可,以 g[i] 的状态转移方程为例:
当 nums[i] > 0 时,g[1]应该为0,因为此时不存在乘积为负数的子数组,所以 g[0] 初始化为 0
当 nums[i] < 0 时,g[1]应该为1,因为此时只有一个乘积为负数的子数组,所以 f[0] 也初始化为 0,此时就能够保证 g[1] = f[0] + 1 = 1,满足要求
④填表顺序
从左往右,两个表一起填
⑤返回值
返回 f表 的最大值,因为题目要求的是乘积为正数的最长子数组的长度,f表 就是以 i 位置结尾的乘积为正数的最长子数组的长度
代码如下:
class Solution
{
public:
int getMaxLen(vector<int>& nums)
{
int n = nums.size(), ret = INT_MIN;
// 初始化默认为0,不用处理
vector<int> f(n + 1), g(n + 1);
for(int i = 1; i <= n; i++)
{
// 每次都分两种情况
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;
}
};
动态规划:子数组系列到此结束