Leetcode 53 Maximum Subarray
题目原文
Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
For example, given the array [-2,1,-3,4,-1,2,1,-5,4]
, the contiguous subarray[4,-1,2,1]
has the largest sum =6
.
题意分析
题目要求找到数组中一个连续的子数组,它的和最大,输出这个最大的和。可以看出如果采用Brute Force,算法的复杂度应该为O(n^2)。
解法分析
本题我选择的编程语言是C++,本题解法主要有以下两个:
- Divide and Conquer(分治法)
- Dynamic Programming(动态规划)
Divede and Conquer
要在数组里找一个和最大的数组,自然想到把用分治法这个数组分成两个数组,分别找到其中和最大的子数组。本题将数组分为左右两个子数组后,原数组和最大子数组的位置有以下三种情况:
- 为左子数组的和最大数组
- 为右子数组的和最大数组
- 在左右数组分界处的子数组
需要判断上面三个子数组哪一个的和最大,输出最大和。C++代码如下:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n=nums.size();
int res=maxSub(0,n-1,nums);
return res;
}
private:
int maxSub(int left,int right,vector<int>& nums){
if(left==right)
return nums[left];
int mid=(left+right)/2;
int maxLeft=maxSub(left,mid,nums);
int maxRight=maxSub(mid+1,right,nums);
int lMaxNearMid=nums[mid];
int lTemp=nums[mid];
int rMaxNearMid=nums[mid+1];
int rTemp=nums[mid+1];
int i,j;
for(i=mid-1;i>=left;i--){
lTemp=lTemp+nums[i];
if(lTemp>lMaxNearMid)
lMaxNearMid=lTemp;
}
for(j=mid+2;j<=right;j++){
rTemp=rTemp+nums[j];
if(rTemp>rMaxNearMid)
rMaxNearMid=rTemp;
}
if((maxLeft>=maxRight)&&(maxLeft>=(lMaxNearMid+rMaxNearMid)))
return maxLeft;
else if(maxRight>=(lMaxNearMid+rMaxNearMid))
return maxRight;
else
return lMaxNearMid+rMaxNearMid;
}
};
由于mid=n/2位置是确定的,所以由这一点开始向左、右求最大和算法复杂度为O(n),所以整个算法的递归式为T(n)=2T(n/2)+O(n),O(n)为’合并‘两子列时的时间消耗,所以总的算法复杂度为O(nlogn)。其中将原数组分成两个子数组可以利用迭代器来完成,可以用如下代码定义生成左子数组left:
vector<int> left(nums.begin(),nums.begin()+nums.size()/2);
上述代码相当于指定了vector left的begin()和end()。
Dynamic Programming
动态规划方法常常用于求解最优化问题,和分治法相同的是,动态规划方法也是将原问题分解为许多字问题,通过组合子问题的解来求解原问题。而它们之间的不同点在于,分治法往往将问题分解为互不相交的子问题,而动态规划方法用于子问题有重叠的情况,也就是有相同子子问题,这时候利用分治的思想会反复求解同一个子问题,也就是那些公共子问题,而动态规划方法对每个子问题只求解一次,将其解存在数组(下标有序)或散列表(下标无序)中,避免了不必要的计算工作,这也同时要求问题满足最有子结构,也即问题的最优解是由相关子问题的最优解组合而成的。这也是典型的空间换时间的时空权衡例子。
动态规划方法主要有两种实现形式:
- 带备忘的自顶向下法
这种方法采用递归形式编写,但会保存每个子问题的解,通常用数组或散列表存储,当需要返回一个子问题的解时,首先检查是否已经保存过这个解,如果是,则直接返回,不是,则用通常方法计算这个解。
- 自底向上法
这种方法保证了在求解每一个子问题时,规模比他小的相关子问题都已经被求解,只需要通过数组或者散列表查询到所需子问题的值,而不需要递归调用。
目前看来,对于《算法导论》上的钢条切割问题,原问题即所选规模最大问题,不需要做相应变形;而本题需要将规模最大问题变形为找长度为n的序列的和最大数组,且最后一个元素包含在和最大数组中。对于钢条切割问题,最大规模问题的解就是我们最终要求的最优解,而对于本题,最大规模问题的解不一定是原问题的最优解,所以需要用一个值来动态存储目前的到的最优解。但他们的共同特点均是需要保存每个子问题的解,免于重复计算。
上述问题均是要求最优解的值,如果需要得到最优解本身,则需要在执行过程中维护一些额外的信息。以下是C++代码:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n=nums.size();
int *DP=new int[n];
DP[0]=nums[0];
int i;
int maxA=DP[0];
for(i=1;i<n;i++){
DP[i]=(DP[i-1]>0)?DP[i-1]+nums[i]:nums[i];
maxA=max(maxA,DP[i]);
}
return maxA;
}
};
代码中用DP[n]存储每个子问题的最优解,用maxA动态保存目前为止产生的最优解的值。由于只有一个循环,所以算法复杂度为O(n)。