LeetCode 53. Maximum Subarray 最大子序和 (DP)

https://leetcode.com/problems/maximum-subarray/description/

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

枚举

枚举起点和终点,记录最大的序列和
时间复杂度 O ( n 2 ) 附加空间复杂度 O ( 1 )

class Solution {
    public int maxSubArray(int[] nums) {
        int n=nums.length;
        int max=Integer.MIN_VALUE;
        for(int st=0;st<n;st++){
            int sum=0;
            for(int end=st+1;end<=n;end++){
                sum=sum+nums[end-1];
                if(sum>max){
                    max=sum;
                }
            }
        }
        return max;
    }
}

分治、递归

分治的思想意味着要将子数组分为两个规模尽可能相等的子数组,找到子数组的中央位置,比如mid,A[low,high]的最大连续子数组所处的位置[i,j]必然是一下三种情况之一:

  • 完全位于子数组A[low,mid]中,因此 low<=i<=j<=mid
  • 完全位于子数组A[mid+1,high]中,因此 mid < i<=j<=high
  • 跨越了中点,因此 low <= i <= mid < j <=high

时间复杂度:O(nlgn)

class Solution {
    public int maxSubArray(int[] nums) {
        return findMaxSubArray(nums,0,nums.length-1);
    }

    private int findMaxSubArray(int[] nums, int low, int high) {
        if(low==high){
            return nums[low];
        }
        int mid=(low+high)/2;
        int leftMax=findMaxSubArray(nums,low,mid);
        int rightMax=findMaxSubArray(nums,mid+1,high);
        int crossMax=findCrossingSubArray(nums,low,mid,high);
        int tempmax=Math.max(leftMax, rightMax);
        return Math.max(tempmax, crossMax);
    }

    private int findCrossingSubArray(int[] nums, int low, int mid, int high) {
        //从中间开始找左边最大序列和
        int leftsum=Integer.MIN_VALUE;
        int sum=0;
        for(int i=mid;i>=low;i--){
            sum=sum+nums[i];
            if(sum>leftsum){
                leftsum=sum;
            }
        }
        //从中间开始找右边最大序列和
        int rightsum=Integer.MIN_VALUE;
        sum=0;
        for(int i=mid+1;i<=high;i++){
            sum=sum+nums[i];
            if(sum>rightsum){
                rightsum=sum;
            }
        }
        return leftsum+rightsum;
    }
}

动态规划

如果把 状态 dp[i] 定为 0-i 的最大子序列和,最后只要返回 dp[n-1] 即可,但 决策 无法确定,不能根据dp[i-1] 得到 dp[i] , 因为 dp[i-1] 保存的最大子序列和可能不和dp[i-1]连续 ,如 [−2,1,−3,4,−1,2,1,−5,4] ,dp[0]=-2,dp[1]=1,dp[2]=1,dp[3] 不能等于5,因为dp[2] 没有记录序列是否是连续的。

定义状态:dp[i] 表示包含 a[i] 的最大连续子串长度,不一定从nums[0] 开始
起始装填:dp[0]=nums[0]
终止状态:dp[nums.length-1]
转移函数 : dp[i]=max( dp[i−1]+a[i] , a[i] )

注意本动态规划的终止状态并不是本题所求,需用另外定义一个变量ans,遍历依次比较以每一个位置结尾的最大子序列和,其中最大的即为所求。

class Solution {
    public int maxSubArray(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        int[] dp=new int[nums.length];//dp[i]表示包含a[i]的最大连续子串长度
        dp[0]=nums[0];
        int ans=dp[0];
        for(int i=1;i<nums.length;i++){
            dp[i]=Math.max(dp[i-1]+nums[i], nums[i]);
            if(dp[i]>ans){
                ans=dp[i];
            }
        }
        return ans;
    }
}

发现 dp[i] 只与前一个状态dp[i-1] 相关,所有可以用一个变量代替,每一个 dp[i]:

class Solution {
    public int maxSubArray(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        int sum=nums[0];
        int ans=sum;
        for(int i=1;i<nums.length;i++){
            sum=Math.max(sum+nums[i], nums[i]);
            if(sum>ans){
                ans=sum;
            }
        }
        return ans;
    }
}

dp[i]表示以a[i]结尾的最大连续子段和。 它和dp[i-1]的关系是,如果dp[i-1] < 0。那么和当前数字构成连续子段后,肯定有:dp[i-1] + a[i] < a[i]。所以,不如从新开始子段。

这么定义状态转移函数:

(1) d p [ i ] = { a [ i ] ,   d p [ i 1 ] 0 d p [ i 1 ] + a [ i ] ,   d p [ i 1 ] > 0  

class Solution {
    public int maxSubArray(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        int sum=nums[0];
        int ans=sum;
        for(int i=1;i<nums.length;i++){
            if(sum<=0){
                sum=nums[i];
            }else{
                sum=sum+nums[i];
            }
//          sum=Math.max(sum+nums[i], nums[i]);
            if(sum>ans){
                ans=sum;
            }
        }
        return ans;
    }
}

可以刚开始就这样想:

max_sum 必然是以A[i](取值范围为A[0] ~ A[n-1])结尾的某段构成的,也就是说max_sum的candidate必然是以A[i]结果的。如果遍历每个candidate,然后进行比较,那么就能找到最大的max_sum了。

假设把A[i]之前的连续段叫做sum。可以很容易想到:

  1. 如果sum>=0,就可以和A[i]拼接在一起构成新的sum’。因为不管A[i]多大,加上一个正数总会更大,这样形成一个新的candidate。(贪心的思想)

  2. 反之,如果sum<0,就没必要和A[I]拼接在一起了。因为不管A[i]多小,加上一个负数总会更小。此时由于题目要求数组连续,所以没法保留原sum,所以只能让sum等于从A[i]开始的新的一段数了,这一段数字形成新的candidate。

  3. 如果每次得到新的candidate都和全局的max_sum进行比较,那么必然能找到最大的max sum subarray.

在循环过程中,用max_sum记录历史最大的值。从A[0]到A[n-1]一步一步地进行。

猜你喜欢

转载自blog.csdn.net/zxm1306192988/article/details/80732455