zoukankan      html  css  js  c++  java
  • 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):只是用了常数空间
  • 相关阅读:
    Solr&SpringDataSolr
    Redis简单介绍与使用
    ueditor文本编辑器
    FastDFS
    Vue.js快速入门
    分布式架构Duboo+Zookeeper的基础使用
    Linux基本操作&&Linux操作MySQL
    23种设计模式之代理模式(动态代理)
    23种设计模式之代理模式(静态代理)
    MongoDB入门培训 | 8周入门NoSQL No.1数据库
  • 原文地址:https://www.cnblogs.com/ningdeblog/p/12541987.html
Copyright © 2011-2022 走看看