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个小组的组合

  1. 第一个组合是以第一个数字结尾的序列,即[-2],最大值-2
  2. 第二个组合是以第二个数字结尾的序列,即[-2,1],[1],最大值1
  3. 第三个组合是以第三个数字结尾的序列,即[-2,1,3],[1,3],[3],最大值4
  4. 以此类推

如果我们能够得到每一个子组合的最优解,整体的最大值就可以通过比较这9个子组合的最大值得到.我们找到了最优子问题,重叠子问题就需要通过比较每个子问题找出.

从第二个子组合和第三个子组合可以看到,组合3只是在组合2的基础上每一个数组添加了第3个数字,然后增加了一个只有第三个数字的数组[3].这样两个组合之间的关系就出现了.题目不需要关心这个序列怎么生成,只关心最大值之间的关系

  • 将子组合3分成两种情况:
  1. 继承组合二得到的序列[-2,1,3],[1,3](最大值1=第二个组合的最大值+第三个数字)
  2. 单独第三个数字的序列[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):只是用了常数空间
原文地址:https://www.cnblogs.com/ningdeblog/p/12541987.html