求最大子数组/子序列

来源:互联网 发布:移民美国知乎 编辑:程序博客网 时间:2024/04/27 19:13

问题:求一个数组 / 序列的满足条件的子数组 / 子序列。

条件: 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 。也就是说全部为负数的话,返回最小的负数。

这是《编程之美》上的一道题,比较充分的体现了算法改进的思路。

 

 

解法一:

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

复杂度可以简单的想出来:设置两个变量ij为子数组边界,这两个变量都要遍历整个数组,然后还需要一个游标k,来遍历整个子数组以求和。所以总的复杂度是On^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移动时进行一下比较,就不会把最大值漏掉。所以只有ij移动,复杂度降低到On^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*On/2=On)。

综合以上两种情况,满足分治算法递归式:Tn=2Tn/2+On=On*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]自己满足条件,此时MaxA[0]……A[n-1]=A[0]。如果包含A[1],则MaxA[0]……A[n-1]=A[0]+MaxA[1]……A[n-1])。

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

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

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

代码如下:

 1 int MaxSubSum(int *A,int n)
 2 {
 3     //假设满足最大和的子数组就是从StartFrom[i]开始
 4     int *StartFrom = new int[n];
 5     memset(StartFrom,n,0);
 6     StartFrom[n-1= A[n-1];
 7     //假设A[i]之后满足最大和的子数组的和为Longest[i](不一定包括A[i])
 8     int *Longest = new int[n];
 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

原创粉丝点击