您的位置:首页 > 其它

求最大子数组/子序列/子段和问题

2008-07-19 19:14 351 查看
求最大子数组/子序列/子段和问题
问题:求一个数组 / 序列的满足条件的子数组 / 子序列。
条件: 1. 子数组必须是连续的。 2. 求和即可,不需要返回子数组是哪段。 3. 数组元素为整数。
例子:
1. [1,-2,3,5,-3,2] 应该返回 8 。
2. [0,-2,3,5,-1,2] 应该返回 9 。
3. [-9,-2,-3,-5,-3] 应该返回 -2 。也就是说全部为负数的话,返回最小的负数。
这是《编程之美》上的一道题,比较充分的体现了算法改进的思路。

解法一:

最直接的解法当然是穷举遍历了,把所有的子数组列出来,然后计算和。

复杂度可以简单的想出来:设置两个变量i和j为子数组边界,这两个变量都要遍历整个数组,然后还需要一个游标k,来遍历整个子数组以求和。所以总的复杂度是O(n^3)。

代码如下:

1 int MaxSubSum(int *A,int n)
2 {
3 int max = -INFINITE;
4 int sum = 0;
5 for (int i = 0 ; i < n ; i++)
6 {
7 for (int j = i ; j < n ; j++)
8 {
9 for (int k = i ; k <= j ; k++)
10 {
11 sum += A[k];
12 }
13 if (sum > max)
14 {
15 max = sum;
16 }
17 }
18 }
19 return max;
20 }
21
解法一改进版:

仔细琢磨就会发现,其实不需要再使用k去遍历子数组,因为每次j移动都会产生新的子数组,所以只要在每次j移动时进行一下比较,就不会把最大值漏掉。所以只有i和j移动,复杂度降低到O(n^2)。

代码如下:

1 int MaxSubSum(int *A,int n)
2 {
3 int max = -INFINITE;
4 int sum = 0;
5 for (int i = 0 ; i < n ; i++)
6 {
7 sum = 0;
8 for (int j = i ; j < n ; j++)
9 {
10 sum += A[j];
11 if (sum > max)
12 max = sum;
13 }
14 }
15 return max;
16 }
17
解法二:分治算法
跟二分查找的思想相似,我们可以分情况讨论这个问题是不是符合二分查找的条件。

情况1.这个满足最大和的子数组全部在本数组的左半部或者右半部。例如:左半部A[i]……A[n/2-1]或者右半部A[n/2]……A[j]。这种情况下可以直接使用递归调用。

情况2.满足最大和的子数组跨过了本数组的中间点。例如:A[i]……A[n/2-1] A[n/2]……A[j]连续。则这种情况下只要在左半部寻找以A[n/2-1]结尾,在右半部寻找以A[n/2]开头的两个满足最大和的连续数组,并求和即可。由于这个已知起点,只需要一个游标即可,所以复杂度是2*O(n/2)=O(n)。

综合以上两种情况,满足分治算法递归式:T(n)=2T(n/2)+O(n)=O(n*logn)。
代码如下:

1 int MaxSubSum(int *A,int Left,int Right)
2 {
3 //
4 int MaxLeftSum,MaxRightSum;
5 //左右子数组的和的最大值
6 int MaxLeftPartSum,MaxRightPartSum;
7 //临时变量,用于存储计算出来的和
8 int LeftPartSum,RightPartSum;
9 int Center,i;
10
11 //其中某一部分只有一个元素
12 if(Left == Right)
13 {
14 if(A[Left] > 0)
15 return A[Left];
16 else
17 return 0;
18 }
19
20 //递归调用。分别计算左右子数组的最大和子数组。
21 //即假设最大和子数组没有被Center切割
22 Center = (Left+Right)/2;
23 MaxLeftSum = MaxSubSum(A,Left,Center);
24 MaxRightSum = MaxSubSum(A,Center+1,Right);
25
26 //假设最大和子数组被Center切开的情况
27 //那么需要从Center开始向两侧计算
28 MaxLeftPartSum = 0;
29 LeftPartSum = 0;
30 for(i = Center ; i >= Left; --i )
31 {
32 LeftPartSum += A[i];
33 if(LeftPartSum > MaxLeftPartSum)
34 MaxLeftPartSum = LeftPartSum;
35 }
36 MaxRightPartSum = 0;
37 RightPartSum = 0;
38 for(i = Center+1 ; i <= Right ; ++i)
39 {
40 RightPartSum += A[i];
41 if(RightPartSum > MaxRightPartSum)
42 MaxRightPartSum = RightPartSum;
43 }
44 //返回三者中的最大值。
45 return max(max(MaxLeftSum,MaxRightSum),MaxLeftPartSum+MaxRightPartSum);
46 }
47
解法三:
我们试着再观察这个数组的特点,一个元素一个元素的看。

根据A[0]是否在这个满足最大和的子数组中,我们可以分为两种情况。

1. 在。那么可以从A[0]开始求(比较容易)。

2. 不在。那么这种情况,又可以继续分为两种情况:A[1]在不在这个满足最大和的子数组中。

从这里我们可以观察出一种递归的特点,可以把一个规模为N的问题转化为规模为N-1的问题。所以这个从A[0]到A[n-1]的最大和子数组问题分解成:

1. 所求子数组中包含A[0]。如果不包含A[1],则A[0]自己满足条件,此时Max(A[0]……A[n-1])=A[0]。如果包含A[1],则Max(A[0]……A[n-1])=A[0]+Max(A[1]……A[n-1])。

2. 所求子数组中不包含A[0]。Max(A[0]……A[n-1])=Max(A[1]……A[n-1])。

最终结果取以上三者的最大值即可,即Max(A[0]……A[n-1])=max( A[0], A[0]+Max(A[1]……A[n-1]), Max(A[1]……A[n-1]))。

这个的复杂度为线性:因为只要把数组遍历一遍即可。

代码如下:


1 int MaxSubSum(int *A,int n)
2 {
3 //假设满足最大和的子数组就是从StartFrom[i]开始
4 int *StartFrom = new int
;
5 memset(StartFrom,n,0);
6 StartFrom[n-1] = A[n-1];
7 //假设A[i]之后满足最大和的子数组的和为Longest[i](不一定包括A[i])
8 int *Longest = new int
;
9 memset(Longest,n,0);
10 Longest[n-1] = A[n-1];
11
12 for (int i = n-2 ; i >= 0 ; i--)
13 {
14 //如果从i开始,那么要么最大和只包括A[i]自己,要么就是后面的那个序列连上A[i]
15 StartFrom[i] = max(A[i],A[i]+StartFrom[i+1]);
16 //最大和,要么是从i开始的,要么还是以前的
17 Longest[i] = max(StartFrom[i],Longest[i+1]);
18 }
19 //最后结果是在号元素中保存
20 return Longest[0];
21 }
22
由于这种前后单元素的相关性,实际上不需要两个数组来储存这个信息,只需要两个变量即可,这样可以减小程序的空间复杂度。
代码如下:
1 int MaxSubSum(int *A,int n)
2 {
3 //假设满足最大和的子数组就是从StartFrom开始
4 int StartFrom = A[n-1];
5 //假设A[i]之后满足最大和的子数组的和为Longest(不一定包括A[i])
6 int Longest = A[n-1];
7
8 for (int i = n-2 ; i >= 0 ; i--)
9 {
10 //如果从i开始,那么要么最大和只包括A[i]自己,要么就是后面的那个序列连上A[i]
11 StartFrom = max(A[i],A[i]+StartFrom);
12 //最大和,要么是从i开始的,要么还是以前的
13 Longest = max(StartFrom,Longest);
14 }
15 //最后结果是在0号元素中保存
16 return Longest;
17 }
18
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: