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) 的解法,尝试使用更为精妙的分治法求解。
枚举
枚举起点和终点,记录最大的序列和
时间复杂度
附加空间复杂度
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]。所以,不如从新开始子段。
这么定义状态转移函数:
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。可以很容易想到:
如果sum>=0,就可以和A[i]拼接在一起构成新的sum’。因为不管A[i]多大,加上一个正数总会更大,这样形成一个新的candidate。(贪心的思想)
反之,如果sum<0,就没必要和A[I]拼接在一起了。因为不管A[i]多小,加上一个负数总会更小。此时由于题目要求数组连续,所以没法保留原sum,所以只能让sum等于从A[i]开始的新的一段数了,这一段数字形成新的candidate。
如果每次得到新的candidate都和全局的max_sum进行比较,那么必然能找到最大的max sum subarray.
在循环过程中,用max_sum记录历史最大的值。从A[0]到A[n-1]一步一步地进行。