连续子数组问题是算法中经常可以见到的一类题目,通过几个典型的题目分析,可以发现这类题目主要分为两大类,其解题思路通过最简单的子串枚举(枚举所有的子串起点和终点)来暴力解决大都不难,但是如果考虑到对空间和时间的要求,其解答就需要一定的算法技巧。
- 子数组和问题(前缀和+哈希表)
- 子数组最值问题(多阶段决策过程最优化问题,动态规划)
子数组和的问题可以通过前缀和解决,而关于子数组的第二类题目往往会涉及到一些最值问题,比如最大子数组和、最长子数组、乘积最大子数组等等,根据我们的算法积累经验,这类求最值的问题,往往会用到动态规划的思路。因为寻找满足一个最值条件的子数组就相当于一个多阶段的决策过程最优化问题,这里的决策就是如何选取子数组的起点和终点,最优化可以通过dp的思路进行记忆化搜索。
一般情况下,我们可以用dp[i]
代表以第 i 个元素结尾的子数组的最值,而递推关系就是考虑和dp[i-1]
之间的关系,然后我们要求的答案就是子数组中的某一个,也就是max(dp[i])
,然后我们只需要枚举所有的i,边遍历边记录最大值即可。
53、最大子序和(Easy)
题目描述:给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
解题思路:
这里用dp[i]
代表以第 i 个元素结尾的子数组的最大和,则max(dp[i])
就是要求的最终结果,那么关键是如何写出递推关系式,明显的,因为我们考虑的子数组以nums[i]
结尾,那么该数组一定是包含nums[i]
这个元素的,因此需要考虑两种情况:即nums[i]
单独成为一段还是与前面的dp[i-1]
一起构成子数组,因此,可以得到如下的递推关系式:
dp[i]=max(dp[i-1]+nums[i],nums[i])
从以上递推关系式可以看出,dp[i]
只与dp[i-1]
相关,因此也不需要维护整个dp数组,只需要维护一个变量用以更新递推关系即可。
代码实现:
class Solution {
public int maxSubArray(int[] nums) {
int temp=nums[0];
int res=temp;
for(int i=1;i<nums.length;i++){
temp=Math.max(temp+nums[i],nums[i]); //递推关系
res=Math.max(res,temp);
}
return res;
}
}
152、乘积最大子数组(Medium)
题目描述:给你一个整数数组 nums
,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4] 输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1] 输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
解题思路:
解法一:动态规划
上一题我们解决的是子数组和最大,而这里是子数组的积最大,非常相似,这里我们很容易想到类似于上题的递推关系:dp[i]=max(dp[i-1]*nums[i],nums[i])
,仅仅是将和转化为积,但是这确实错误的,原因在于和与积具有一定的差异,关键在于乘积涉及到正负符号的问题,如果当前是一个负数,而以前一项结尾的子数组乘积最大为正数,最小为负数,那么我们的结果在两项中取其大,无论如何都会得到一个负数,而前一项的最小值*当前值,负负得正,会得到一个正数,显然乘积更大,所以很明显上面的递推关系不够全面。比如{5,6,−3,4,−3}
.
因此,这里我们可以对正负进行分别考虑。具体说来,如果当前位置如果是一个负数的话,那么我们希望以它前一个位置结尾的某个段的积也是个负数,这样就可以负负得正,并且我们希望这个积尽可能「负得更多」,即尽可能小。如果当前位置是一个正数的话,我们更希望以它前一个位置结尾的某个段的积也是个正数,并且希望它尽可能地大。
基于这个想法,我们就不难得到进一步的解法,也就是对于每一个结尾元素,既维护一个最大值,也维护一个最小值,在递推得到结果时,三者取其大。具体见代码。
解法二:利用乘积的性质
考虑到这里是数组的乘积,因此,我们可以得到一个结论:数组里如果全是正数和偶数个负数,乘起来就最大,如果是奇数个负数,那么要么是没有第一个负数,要么是没有最后一个负数,不可能是中间,因为中间会使数组不连续。
基于这样的结论,就可以正向进行一次乘法,反向进行一次乘法,同时记录和比较最大值,需要注意的是如果有0存在需要特殊判断。
代码实现:
//解法一:动态规划,时间复杂度O(n),空间复杂度O(1)
class Solution {
public int maxProduct(int[] nums) {
//动态规划,注意符号,类似于最大子序和
if(nums==null||nums.length==0)
return 0;
int len=nums.length;
//int[] dp_max=new int[len]; //以nums[i]为结尾的乘积最大子数组
//int[] dp_min=new int[len];
//因为递推关系dp[i]只和dp[i-1]相关,没必要用数组,可以用一个变量节省空间
int temp_max=nums[0];
int temp_min=nums[0];
int product=nums[0];
for(int i=1;i<len;i++){
//注意递推关系为三者取最大(小),因为负负得正,前一项的最小值乘以当前的一个负数很可能就成为最大值
int pre_max=temp_max;
temp_max=Math.max(nums[i],Math.max(temp_max*nums[i],temp_min*nums[i]));
temp_min=Math.min(nums[i],Math.min(pre_max*nums[i],temp_min*nums[i]));
product=Math.max(product,temp_max);
}
return product;
}
}
//解法二:利用乘积的性质,时间复杂度O(n),空间复杂度O(1)
class Solution {
public int maxProduct(int[] nums) {
if(nums==null || nums.length==0)
return 0;
int res=nums[0];
int max=1;
//正向乘,三种情况:全是正数,偶数个负数,或者没有最后一个负数
for(int i=0;i<nums.length;i++){
max*=nums[i];
res=Math.max(res,max);
if(max==0) //等于0的话需要重新从1开始
max=1;
}
max=1;
//反向乘,没有第一个负数的情况
for(int i=nums.length-1;i>=0;i--){
max*=nums[i];
res=Math.max(res,max);
if(max==0)
max=1;
}
return res;
}
}