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.
在本篇文章中贴几个除了暴力求解外的方法,整理自博客http://www.cnblogs.com/waytofall/archive/2012/04/10/2439820.html和博客http://www.cnblogs.com/boring09/p/4252780.html。
方法一:动态规划
贴一个知乎上关于动态规划的讨论链接https://www.zhihu.com/question/23995189
令sum[i]是以第i个元素结尾且和最大的子串。对于数组元素a[i],以a[i]结尾且和最大的子串,要么是以第i-1个元素结尾且和最大(sum[i-1])的子串加a[i],要么是只包含a[i]元素,即sum[i] = max(sum[i-1] + a[i], a[i]),亦即等价于判断sum[i-1]是否大于0。
可以得到此题的代码如下:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int sum = nums[0];
int maxSum = sum;
for (int i=1;i<nums.size();i++) {
sum = max(nums[i], sum + nums[i]);
maxSum = max(maxSum, sum);
}
return maxSum;
}
};
时间复杂度O(n),空间复杂度O(1)。
方法二:扫描法
个人感觉和动态规划一样只是理解方式不同而已。。后加注:这里提到的扫描法存在一个问题就是如果最大字段和小于0则算法没法给出正确答案。其实这个问题用动态规划就好,这里的扫描法其实真的不是个好方法,只是因为很有名所以还是粘出来了。当我们加上一个正数时,和会增加;当我们加上一个负数时,和会减少。如果当前得到的和是个负数,那么这个和在接下来的累加中应该抛弃并重新清零,不然的话这个负数将会减少接下来的和。实现:
//copyright@ July 2010/10/18
//updated,2011.05.25.
#include <iostream.h>
int maxSum(int* a, int n)
{
int sum=0;
//其实要处理全是负数的情况,很简单,如稍后下面第3点所见,直接把这句改成:"int sum=a[0]"即可
//也可以不改,当全是负数的情况,直接返回0,也不见得不行。
int b=0;
for(int i=0; i<n; i++)
{
if(b<0) //...
b=a[i];
else
b+=a[i];
if(sum<b)
sum=b;
}
return sum;
}
int main()
{
int a[10]={1, -2, 3, 10, -4, 7, 2, -5};
//int a[]={-1,-2,-3,-4}; //测试全是负数的用例
cout<<maxSum(a,8)<<endl;
return 0;
}
/*-------------------------------------
解释下:
例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5,
那么最大的子数组为3, 10, -4, 7, 2,
因此输出为该子数组的和18。
所有的东西都在以下俩行,
即:
b : 0 1 -1 3 13 9 16 18 13
sum: 0 1 1 3 13 13 16 18 18
其实算法很简单,当前面的几个数,加起来后,b<0后,
把b重新赋值,置为下一个元素,b=a[i]。
当b>sum,则更新sum=b;
若b<sum,则sum保持原值,不更新。。July、10/31。
时间复杂度O(n),空间复杂度O(1)
方法三:分而治之
假设求A[l..r]的最大子串和
首先将其分成两半A[l..m]和A[m+1..r],其中m=(l+r)/2,并分别求递归求出这两半的最大子串和,不妨称为left,right。如下图所示:
A[l..r]的连续子串和可能出现在左半边(即left),或者可能出现在右半边(即right),还可能出现在横跨左右两半的地方(即middle),如下图橙色部分所示:
当然,middle完全有可能覆盖left或right,它可能的范围入下图所示:
那么,如何求middle?貌似没有什么简单的方法,只能从中间向两遍扫,也就是把上图种的范围扫一遍。具体怎么扫呢?见方法I和方法II
是不是突然觉得很坑爹?既然知道最后求middle要扫一遍,还不如一开始就从l到r扫一遍求max得了,还费什么劲儿求left和right呢?求left和right的作用仅限于缩小扫描的范围。
int diveNConquer(int A[], int l, int r) {
if (l == r)
return A[l];
int m = (l + r) / 2;
int left = diveNConquer(A, l, m);
int right = diveNConquer(A, m + 1, r);
int middle = A[m];
for (int i = m - 1, tmp = middle; i >= l; i--) {
tmp += A[i];
middle = max(middle, tmp);
}
for (int i = m + 1, tmp = middle; i <= r; i++) {
tmp += A[i];
middle = max(middle, tmp);
}
return max(middle, max(left, right));
}
int maxSubArray(int A[], int n) {
return diveNConquer(A, 0, n - 1);
}
分析一下时间复杂度,设问题的工作量是T(n),则有T(n) = 2T(n/2) + O(n),解得T(n) = O(nlogn)。看看,效率反而低了不少。