1.枚举方法O(n*n*n)
1、算法 最简单、最暴力的算法是枚举法,即-一罗列所有可能的子序列,从中找出和值最大的,如代码清单1-2所示。该函数的输入参数为一个待查找的数组a、数组的大小size,输出参数是最大子序列的起始位置start、最大子序列的终止位置end。返回值为最大子序列的和。程序的主体是一对用来遍历所有可能的子序列的循环。循环变量i控制子席列的起始位置,循环变量j控制子序列的终止位置。对于每个可能的子序列,用一个循环变量为k的计数循环计算其和。如果当前子席列之和大于目前所遇到的最大的子序列之和,则更新maxSum和start及end的值。最后返回maxSum。
int maxSubsequenceSum(int a[], int size, int &start, int &end) {
int maxSum = 0; // 当前的最大子序列和
for (int i = 0; i < size; i++) // 子序列的起始位置
for (int j = i; j < size; j++) { // 子序列的终止位置
int thisSum = 0;
for (int k = i; k <= j; k++) // 求从i开始到j结束的序列和
thisSum += a[k];
if (thisSum > maxSum) { // 找到一个更好的序列
maxSum = thisSum;
start = i;
end = j;
}
}
return maxSum;
}
2.两重循环O(n*n)
2、从算法中删除一层嵌套循环,通常可以降低其运行时间。那么,怎样删除一层循环呢?答案是,不一定总能删除。然而,代码1-3中的算法有一些不必要的计算。在计算第i到第j个元素的子序列和时,算法用了一个循环。事实上,在计算从第i到第j-1个元素的子序列和时,已经得到了第i到第j-1个元素的子序列和。而2-Ak= A;+2Ak,因此,只需要再做一次加法即可。利用这一结果,就得到了如代码清单1-3所示的改进算法。这个算法有一个两重循环而不是三重循环,且运行时间是O(n)。
int maxSubsequenceSum(int a[], int size, int &start, int &end) {
int maxSum = 0; // 已知的最大连续子序列之和
for (int i = 0; i < size; i++) { // 连续子序列的起始位置
int thisSum = 0; // 从i开始的连续子序列之和
for (int j = i; j < size; j++) { // 连续子序列的终止位置
thisSum += a[j]; // 计算从第i到第j个元素的连续子序列之和
if (thisSum > maxSum) {
maxSum = thisSum;
start = i;
end = j;
}
}
}
return maxSum;
}
3.分治法O(n*logn)
3、最长连续子序列和的问题还可以用分治法解决。假设输入是4,-3,5,-2,-1,2,6,-2。这个输入可以被划分成两部分,即前4个和后4个。这样最大和值的连续子序列可能出现在下面3种情况中。 情况1:最大和值的子序列位于前半部分。 情况2:最大和值的子序列位于后半部分。 情况3:从前半部开始但在后半部结束。 这3种情况中的最大值就是本问题的解。 在前两种情况中,只需要在前半部分或后半部分找最长连续子序列,这通过递归调用就可解决。问题是第三种情况如何解决?可以从两半部分的边界开始,通过从右到左的扫描来找到左半段的最长序列。类似地,通过从左到右的扫描找到右半段的最长序列。把这两个子序列组合起来,形成跨越分割边界的最长连续子序列。在上述输入中,通过从右到左扫描得到的左半段的最长序列和是4包括前半部分的所有元素。从左到右扫描得到的右半段的最长序列和是7,包括-1.2和6。因此从前部分开始到后半部分结束的最长子序列和为4+7=11。总结一下用分治法解决最长子序列和问题的算法由4步组成。(1)递归地计算整个位于前半部的最长连续子席列。(2)递归地计算整个位于后半部的最长连续子席列。(3)通过两个连续循环,计算从前半部开始但在后半部结束的最长连续子序列的和。(4)选择上述3个子问题中的最大值,作为整个问题的解。根据此算法,可得到如代码清单1-4所示的程序。由于此方案使用递归解决,所以函数包含了控制递归的参数left和right。这个函数原型与用户所用的原型不符,所以需要定义了一个包裹函数。
// 递归解决方案
int maxSum(int a[], int left, int right, int &start, int &end) {
int maxLeft, maxRight, center;
// maxLeft和maxRight分别为前半部分、后半部分的最大连续子序列和
int leftSum = 0, rightSum = 0; // 情况3中,左、右半段的连续子序列和
int maxLeftTmp = 0,
maxRightTmp = 0; // 情况3中,情况3中,左、右半段的最大连续子序列和
int startL, startR, endL,
endR; // 前半部分、后半部分的最大连续子序列的起点和终点
if (left == right) { // 仅有一个元素,递归终止
start = end = left;
return a[left] > 0 ? a[left] : 0;
}
center = (left + right) / 2;
// 找前半部分的最大连续子序列和
maxLeft = maxSum(a, left, center, startL, endL);
// 找后半部分的最大连续子序列和
maxRight = maxSum(a, center + 1, right, startR, endR);
// 找从前半部分开始但在后半部分结束的最大连续子序列
start = center;
for (int i = center; i >= left; --i) {
leftSum += a[i];
if (leftSum > maxLeftTmp) {
maxLeftTmp = leftSum;
start = i;
}
}
end = center + 1;
for (int i = center + 1; i <= right; ++i) {
rightSum += a[i];
if (rightSum > maxRightTmp) {
maxRightTmp = rightSum;
end = i;
}
}
// 找3种情况中的最大连续子序列和
if (maxLeft > maxRight)
if (maxLeft > maxLeftTmp + maxRightTmp) {
start = startL;
end = endL;
return maxLeft;
} else
return maxLeftTmp + maxRightTmp;
else if (maxRight > maxLeftTmp + maxRightTmp) {
start = startR;
end = endR;
return maxRight;
} else
return maxLeftTmp + maxRightTmp;
}
// 包裹函数
int maxSubsequenceSum(int a[], int size, int &start, int &end) {
return maxSum(a, 0, size - 1, start, end);
}
完整代码:
#include <iostream>
using namespace std;
const int N = 1e5+10;
int maxSum(int a[],int left,int right,int &start,int &end){
int startL,startR,endL,endR;
if(left==right){
start=end=left;
return a[left]>0?a[left]:0;
}
int center=(left+right)/2;
int leftmax=maxSum(a,left,center,startL,endL);
int rightmax=maxSum(a,center+1,right,startR,endR);
start=center;
int leftsum=0;
int leftsumTmp=0;
for(int i=center;i>=left;i--){
leftsum+=a[i];
if(leftsum>leftsumTmp){
leftsumTmp=leftsum;
start=i;
}
}
end=center+1;
int rightsum=0;
int rightsumTmp=0;
for(int i=center+1;i<=end;i++){
rightsum+=a[i];
if(rightsum>rightsumTmp){
rightsumTmp=rightsum;
end=i;
}
}
if(leftmax>rightmax){
if(leftmax>leftsumTmp+rightsumTmp){
start=startL;
end=endL;
return leftmax;
}
else{
return leftsumTmp+rightsumTmp;
}
}
else{
if(rightmax>leftsumTmp+rightsumTmp){
start=startR;
end=endR;
return rightmax;
}
else{
return leftsumTmp+rightsumTmp;
}
}
}
int main(void){
int n,a[N];
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int start,end;
printf("%d\n",maxSum(a,1,n,start,end));
printf("%d %d\n",start,end);
return 0;
}
4、线性算法优化
4、最长连续子序列和问题的算法还可以继续优化。进一步观察平方算法,如果能再删除一个循环,就可以变成一个线性算法。然而,从代码1-2到代码1-3所示的循环删除很简单,但要删除另个循环就不那么容易了。这个问题在于平方算法仍然是一个枚举法;也就是说,还是尝试了所有可能的子序列。平方算法和立方算法的唯一的不同就是计算每个连续子序列和的代价是常量O(1)而不是线性的O(n)。因为平方数目的子序列数是可能的,要得到一个线性上限的唯一方法就是找出一个聪明的方法来排除对很多子序列的考虑,而不用真正地计算所有的这些子序列的和。首先,可以通过分析来排除很多可能的子序列。如果一个子序列的和是负的,则它不可能是最大连续子序列的开始部分,因为可以通过不包含它来得到一个更大的连续子序列。如{-2,11,-4,13,-5,2的最大子序列不可能从-2开始,{1,-3,4,-2,-1,6}的最大子序列不可能包含{1,-3}。所有与最大连续子序列毗邻的连续子序列一定有负的(或0)和(否则会包含它们)。因此,在代码1-4的算法中当检测出一个负的子序列和时,不但可以从内层循环中跳出来而且还可以让i直接增加到j+1。如{1,-3,4,-2,-1,6},当检测序列{1,-3}后,发现是负值,则表示该子序列不可能包含在最大子序列中。接下去i就可以从4开始检测。这样对序列中的元素只要顺序检查一遍就可以了。
int maxSubsequenceSum(int a[], int size, int &start, int &end) {
int maxSum, starttmp, thisSum;
start = end = maxSum = starttmp = thisSum = 0;
for (int j = 0; j < size; ++j) {
thisSum += a[j];
if (thisSum <= 0) { // 排除前面的连续子序列
thisSum = 0;
starttmp = j + 1;
} else if (thisSum > maxSum) { // 找到一个连续子序列和更大的连续子序列
maxSum = thisSum;
start = starttmp;
end = j;
}
}
return maxSum;
}
完整代码:
#include <iostream>
using namespace std;
const int N = 1e5+10;
int maxSum(int a[],int &start,int &end,int n){
int sum=0,sumTmp=0;
for(int i=1;i<=n;i++){
sum+=a[i];
if(sum<=0){
start=i+1;
sum=0;
}
else if(sum>sumTmp){
sumTmp=sum;
end=i;
}
}
return sumTmp;
}
int main(void){
int n;
int a[N];
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int start,end;
printf("%d\n",maxSum(a,start,end,n));
printf("%d %d\n",start,end);
return 0;
}