[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.人民邮电出版社
欢迎交流指正,欢迎转载,转载请注明作者及出处。