最大子序列和问题

 [question]:给定整数(A_1,A_2,A_3,...,A_N)(可能有负数),求(sum_{k=i}^{j}A_k)的最大值(方便起见,若所有整数为负数,则最大序列和为0)

     //例如:输入(-2,11,-4,13,-5,-2),答案为(20)(从(A_2)到(A_4))

  解法①:穷举法

      外两层循环遍历数组,确定序列上下界。最内层循环遍历序列求和。

 1 /*
 2  * maxSubSum1:解法①,穷举法
 3  * 输入数组a
 4  * 返回最大子序列和
 5  */
 6 int maxSubSum1( const vector<int> &a )
 7 {
 8     int maxSum = 0;//存储结果
 9     
10     for (int i = 0; i < a.size(); i++)
11         for (int j = i; j < a.size(); j++)
12         {
13             int thisSum = 0;
14             
15             for (int k = i; k <= j; k++)
16                 thisSum += a[k];
17             
18             if (thisSum > maxSum)
19             {
20                 maxSum = thisSum;
21             }
22         }
23     
24     return maxSum;
25 }

    显然,此方法时间复杂度为(O(N^3)),并没有什么实际意义,(N)取较大值时算法效果很差。   

  解法②:改进穷举法

    第二层每次循环顺便求出子序列值,并判断结果,取消第三层循环。

 1 /*
 2  * maxSubSum2:解法②,改进穷举法
 3  * 输入数组a
 4  * 返回最大子序列和
 5  */
 6 int maxSubSum2( const vector<int> &a )
 7 {
 8     int maxSum = 0;//存储结果
 9     
10     for (int i = 0; i < a.size(); i++)
11     {
12         int thisSum = 0;
13         
14         for (int j = i; j < a.size(); j++)
15         {
16             thisSum += a[j];
17             
18             if (thisSum > maxSum)
19                 maxSum = thisSum;
20         }
21     }
22     
23     return maxSum;
24 }

     显然,此方法时间复杂度为(O(N^2)),在穷举法的基础上稍作改进,(N)取较大值时算法效果仍然很差。 

  解法③:分治策略

    将原问题分成两个大致相等的子问题,然后递归的对他们求解。最大子序列可能出现的地方:输入数据的左半部,输入数据的右半部,跨越数据中部占据左右两半部分。

 1 /*
 2  * max3:递归函数
 3  * 输入数组a,递归下界,递归上界
 4  * 返回一次递归最大子序列和
 5  */
 6 int maxSumRec(const vector<int> &a, int left, int right)
 7 {
 8     if (left == right)//基准情况
 9     {
10         if (a[left] > 0)//只有一个元素时返回其值(元素全为负时返回0)
11             return a[left];
12         else
13             return 0;
14     }
15     
16     int center = (left + right) / 2;
17     int maxLeftSum = maxSumRec(a, left, center);
18     int maxRightSum = maxSumRec(a, center + 1, right);
19     
20     int maxLeftBorderSum = 0, leftBorderSum = 0;
21     for (int i = center; i >= left; i--)
22     {
23         leftBorderSum += a[i];
24         if (leftBorderSum > maxLeftBorderSum)
25             maxLeftBorderSum = leftBorderSum;
26     }//前半部分最大子序列和
27     
28     int maxRightBorderSum = 0, rightBorderSum = 0;
29     for (int j = center + 1; j <= right; j++)
30     {
31         rightBorderSum += a[j];
32         if (rightBorderSum > maxRightBorderSum)
33             maxRightBorderSum = rightBorderSum;
34     }//后半部分最大子序列和
35     
36     return max3(maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum);//max3为求三个整型数的最大值函数
37 }
38 
39 /*
40  * maxSubSum3:解法③,分治策略
41  * 输入数组a
42  * 返回最大子序列和
43  */
44 int maxSubSum3( const vector<int> &a )
45 {
46     return maxSumRec(a,0,a.size() - 1);
47 }

  由于使用了递归,且递归函数内使用了一层循环,所以算法时间复杂度为(O(Nlog_2N))。此时已经为较理想的结果。  

解法④:终极方法

  再次改进穷举法,显然我们可以推出一个结论,如果(a[i])是负的,那么它不可能代表最优序列的起点,因为任何包含(a[i])的作为起点的子序列都可以通过用(a[i+1])做起点而得到改进。类似地,任何负的

子序列不可能是最优子序列的前缀。如果在内循环中检测到(a[i])到(a[j])的子序列是负的,那么我们可以推进(i)。关键结论是:我们不仅可以把(i)推进到(i+1),而且我们实际上还可以把它一直推进到(j+1)。为了证明,我们由前面的结论知道(a[i])是非负的,其次(j)是使从下标(i)开始使序列和为负的第一个下标,令(p)为(i+1)到(j)之间的任一下标,那么从下标(i)到(j)中,序列(a[i]...a[p-1])的和始终大于序列(a[p]...a[j])的和。因此把(i)推进到(j+1)是安全的,我们不会错过最优解。

 1 /*
 2  * maxSubSum4:解法④,终极方法
 3  * 输入数组a
 4  * 返回最大子序列和
 5  */
 6 int maxSubSum4( const vector<int> &a )
 7 {
 8     int maxSum = 0, thisSum = 0;
 9     
10     for (int j = 0; j < a.size(); j++)
11     {
12         thisSum += a[j];
13         
14         if (thisSum > maxSum)
15             maxSum = thisSum;
16         else if (thisSum < 0)//关键步骤:推进到j+1(使序列和小于0下标j)
17             thisSum = 0;
18         else
19             continue;
20     }
21     
22     return maxSum;
23 }

  显然,算法的时间复杂度为(O(N)),并且只对数据一次扫描,一旦(a[i])被读入并处理,它就不需要被记忆,因此仅需常量空间并以线性时间运行的联机算法

总结:要善于思考问题的结构,仔细研究数据的结构寻找最优解法。没有思路时也可以先写出最笨的方法,然后逐步改进。

参考:数据结构与算法分析(c++描述).第三版.【美】Mark Allen Weiss.人民邮电出版社

欢迎交流指正,欢迎转载,转载请注明作者及出处。

原文地址:https://www.cnblogs.com/nslogmeng/p/4461603.html