1.(53)最大子序和
2020/3/19
1.(53)最大子序和
Maximum Subarray
Given an integer array nums
, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
Example:
Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
Follow up:
If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
方法1: 分治法
分治法解决问题的模板:
- 定义基本情况
- 将问题分解为子问题并递归地解决他们
- 合并子问题的解以获得原问题的解
算法
当最大子数组有n个数字:
- 如果
n==1
,返回此元素 left_sum
:left到(left+right)/2的最大值right_sum
:(left+right)/2+1到right的最大值cross_sum
是包含左右子数组且含索引(left+right)/2
的最大值
class Solution{
public int maxSubArray(int nums[]){
return helper(nums,0,nums.length-1);
}
public int helper(int[] nums,int left,int right){
if(left==right) return nums[left];
int p=(left+right)/2;
int leftSum=helper(nums,left,p);
int rightSum=helper(nums,p+1,right);
int crossSum=crossSum(nums,left,right,p);
return Math.max(Math.max(leftSum,rightSum),crossSum);
}
public int crossSum(int[] nums,int left,int right,int p){
if(left==right) return nums[left];
int leftSubsum=Integer.MIN_VALUE;
int currSum=0;
for(int i=p;i>left-1;--i){
currSum+=nums[i];
leftSubsum=Math.max(leftSubsum,currSum);
}
int rightSubsum=Integer.MIN_VALUE;
currSum=0;
for(int i=p+i;i<right+1;++i){
currSum+=nums[i];
rightSubsum=Math.max(rightSubsum,currSum);
}
return leftSubsum+rightSubsum;
}
}
复杂度分析
- 时间复杂度:O(nlogn)
- 空间复杂度:O(logn),递归时栈使用的空间
解析
与归并排序类似,先切分,再合并结果
关键在于如何切分这些组合才能使每个小组之间不会有重复的组合:
从题目给的案例来看[-2,1,-3,4,-1,2,1,-5,4]
,共有9个元素center=(start+end)/2
这个原则,得到中间元素的索引为4,拆分为三个组合:
[-2,1,-3,4,-1]
及它的子序列(在-1左边的并包含它的为一组)[2,1,-5,4]
及它的子序列(在-1右边不包含它的为一组)- 任何包含-1以及它右边元素2以及它右边元素2的序列为一组(即包含左边序列的最右边元素以及右边序列最左边元素的序列,就保证这个组合里的任何序列都不会和上面两个重复)
以上三个组合的序列没有任何重复的部分,而且一起构成所有子序列的全集,计算出这三个子集合的最大值,然后取其中的最大值,就得到问题的答案
前两个子组合可以用递归解决,第三个跨中心的组合的解决方式:
- 先从左边序列的最右边元素向左累加 ,记录最大值;再从右边序列最左端元素向右累加,记录最大值
- 左右两边的最大值相加,就是包含这两个元素的子序列的最大值
在计算过程中,累加和比较是关键操作,一个长度为n的数组在递归的每一层都会进行n次操作,分治法的递归层级在logn级别,所以整体复杂度是O(nlogn)
连续子序列的最大和主要由这三部分子区间里元素的最大和得到:
- 第1部分:子区间
[left,mid]
; - 第2部分:子区间
[mid+1,right]
- 第3部分:包含子区间
[mid,mid+1]
的子区间,即nums[mid]
和num[mid+1]
一定会被选取
对它们三者求最大值即可
public int maxSubArray(int[] nums){
return maxSubArrayDivideWithBorder(nums,0,nums.length-1);
}
private int maxSubArrayDivideWithBorder(int[] nums,int start,int end){
if(start==end){
//只有一个元素,即递归的结束情况
return nums[start];
}
//计算中间值
int center=(start+end)/2;
//计算左侧子序列最大值
int leftMax=maxSubArrayDivideWithBorder(nums,start,center);
//计算右侧子序列最大值
int rightMax=maxSubArrayDivideWithBorder(nums,center+1,end);
//下面计算横跨两个子序列的最大值
//计算包含左侧子序列最后一个元素的子序列的最大值
int leftCrossMax=Integer.MIN_VALUE;//初始化
int leftCrossSum=0;
for(int i=center;i<=start;i--){
leftCrossSum += nums[i];
leftCrossMax = Math.max(leftCrossSum,leftCrossMax);
}
//计算包含右侧子序列最左端元素的子序列最大值
int rightCrossMax = nums[center+1];
int rightCrossSum = 0;
for (int i = center + 1; i <= end ; i ++) {
rightCrossSum += nums[i];
rightCrossMax = Math.max(rightCrossSum, rightCrossMax);
}
//计算跨中心的子序列的最大值
int crossMax=leftCrossMax+rightCrossMax;
//比较三者,返回最大值
return Math.max(crossMax,leftMax,rightMax);
}
方法2: 动态规划
按照 排列组合的数学算法,9个数组,以第i个数字结尾的串,有i种组合,一共有个45个组合
如果有n个数字,时间复杂度为O(n^2),明显不能接受
首先需要把这个问题分解成最优子问题来解,最主要的思路就是将上面的45进行分解,分解成数量较少的子问题.这里我们一共有9个数字,顺理成章把组合分解成9个小组的组合
- 第一个组合是以第一个数字结尾的序列,即
[-2]
,最大值-2
- 第二个组合是以第二个数字结尾的序列,即
[-2,1],[1]
,最大值1
- 第三个组合是以第三个数字结尾的序列,即
[-2,1,3],[1,3],[3]
,最大值4
- 以此类推
如果我们能够得到每一个子组合的最优解,整体的最大值就可以通过比较这9个子组合的最大值得到.我们找到了最优子问题,重叠子问题就需要通过比较每个子问题找出.
从第二个子组合和第三个子组合可以看到,组合3只是在组合2的基础上每一个数组添加了第3个数字,然后增加了一个只有第三个数字的数组[3].这样两个组合之间的关系就出现了.题目不需要关心这个序列怎么生成,只关心最大值之间的关系
- 将子组合3分成两种情况:
- 继承组合二得到的序列
[-2,1,3],[1,3]
(最大值1=第二个组合的最大值+第三个数字) - 单独第三个数字的序列
[3]
(最大值2=第三个数字)
如果第二个序列的最大值大于0,那么最大值1比最大值2大,反之最大值2比较大,这样,我们就通过第二个组合的最大值和第三个数字,得到第三个组合的最大值.因为第二个组合的结果被重复用到了,所以符合重叠子问题的定义.
-
步骤一:定义状态->定义数组元素的含义
- 定义dp[i]为以i结尾的子串的最大值
-
步骤二:状态转移方程->找出数组元素间的关系式
if(dp[i-1]>=0) dp[i]=dp[i-1]+nums[i];
if(dp[i-1]<0) dp[i]=nums[i];
-
步骤三:初始化->找出初始条件
- dp[0]=nums[0];
-
步骤四:状态压缩->优化数组空间
- 每次状态的更新只依赖于前一个状态,即dp[i]的更新只取决于dp[i-1],我们只用一个存储空间保存上一次的状态即可.
-
步骤五:选出结果
- 有的题目结果是dp[i]
- 本题结果是dp[0]...dp[i]中最大值
public class Solution{
public int maxSubArray(int[] nums){
int len=nums.length;
if(len==0){
return 0;
}
int[] dp=new int[len];
dp[0]=nums[0];
for(int i=1;i<len;i++){
if(dp[i-1]>0){
dp[i]=dp[i-1]+nums[i];
}else{
dp[i]=nums[i];
}
}
int res=dp[0];
for(int i=1;i<len;i++){
res=Math.max(res,dp[i]);
}
return res
}
}
- 状态压缩,我们只需要一个变量subMax保存前面子组合的最大值,另一个max保存全局最大值
public int maxSubArray(int[] nums){
if(nums==null){
return 0;
}
int max=nums[0];
int subMax=nums[0];
for(int i=1;i<nums.length;i++){
if(subMax>0){
subMax=subMax+nums[i];
}else{
subMax=nums[i];
}
max=Math.max(max,subMax);
}
return max;
}
延伸--获取最大序列的起始和结束位置
public int maxSubArrayPosition(int[] nums){
if(nums==null){
return 0;
}
int start = 0;
int end=0;
int subStart=0;
int subEnd=0;
int max=nums[0];
int subMax=nums[0];
for(int i=1;i<nums.length;i++){
if(subMax>0){
//前一个子组合最大值大于0,正增益,更新最后元素的位置
subMax=subMax+nums[i];
subEnd++;
}else{
//前一个子组合最大值小于0,抛弃前面的结果,更新最大值位置
subMax=nums[i];
subStart=i;
subEnd=i;
}
//计算全局最大值,更新位置,将全局最优解的位置更新
if(subMax>max){
max=subMax;
start=subStart;
end=subEnd;
}
}
if(start==end){
System.out.println("["+start+"]");
}else{
System.out.println("["+start++","+end+"]");
}
return max;
}
方法3: 贪心算法
- 使用单个数组作为输入来查找最大/最小元素/总和的问题,贪心算法是可以在线性时间解决的方法之一
- 每一步都选择最佳方案,到最后就是全局最优方案
算法
遍历数组并在每个步骤中更新:
- 当前元素
- 当前元素位置的最大和
- 迄今为止的最大和
class Solution{
public int maxSubArray(int[] nums){
int n=nums.length;
int currSum=nums[0],maxSum=nums[0];
for(int i=0;i<n;++i){
currSum=Math.max(nums[i],currSum+nums[i]);
maxSum=Math.max(maxSum,currSum);
}
return maxSum;
}
}
复杂度分析
- 时间复杂度O(n),只遍历一次数组
- 空间复杂度O(1):只是用了常数空间