问题引出
《编程珠玑》第8章引出一个模式识别问题:输入是具有n个浮点数的向量x,输出是输入向量的任何连续子向量中的最大和。例如:
31 -41 59 26 -53 58 97 -93 -23 84
那么应该输出想x[2..6]的综合,即187。
分析
当所有书都是正数时,问题很容易解决,此时最大子向量就是整个输入向量。当输入向量中有负数的时候,是否应该包含某个负数并期望旁边的正数会弥补它呢?这里定义当所有输入都是负数的时候,总和最大的子向量是空向量,总和为0。
解决
1.平方算法
即对任何的正数对(i,j),计算x[i..j]的总和,求出最大总和。对于输入规模n来书,需要执行O(n2)步。
int MaxSum1(int *a,int n)
{
int maxsofar = 0;
int sum;
for(int i = 0;i < n;i++)
{
sum = 0;
for(int j = i;j < n;j++)
{
sum = sum + a[j];
maxsofar = maxsofar>sum?maxsofar:sum;
}
}
return maxsofar;
}
2.分治算法
分治原理:要解决规模为n的问题,可递归地的解决俩个规模近似为n/2的子问题,然后对它们的答案进行合并并得到整个问题的答案。
在本例中,初始问题要处理大小为n的向量,所以将它划分为两个大小近似相等的子向量a,b,然后递归地找出a,b中元素总和最大的子向量,分别为ma,mb。要注意这时候,最大子向量要么在整个a中(即ma),要么在整个b中(即mb),要么跨越a和b之间的边界。我们将跨越边界的最大子向量称为mc。
那么如何计算mc?mc在a中的部分是包含右边界的最大子向量,在b中的部分是包含左边界的最大子向量。
所以最终递归计算ma和mb,并且通过上诉方法计算mc,然后返回3个总和中的最大者。
int max(int a,int b)
{
return a>b?a:b;
}
int MaxSum2(int *a,int l,int u)
{
if(l == u)//一个元素
return max(0,a[l]);
int m = (l+u)/2;
int lmax,rmax,sum;
lmax = sum = 0;
for(int i = 0;i >= l;i--)
{
sum = sum + a[i];
lmax = max(lmax,sum);
}
rmax = sum = 0;
for(int i = m+1;i <= u;i++)
{
sum = sum + a[i];
rmax = max(rmax,sum);
}
return max(lmax+rmax,max(MaxSum2(a,l,m),MaxSum2(a,m+1,u)));
}
该算法实在每层递归中都执行O(n)次操作,而总计有O(logn)层递归,所以所需时间为O(nlogn)。
3.扫描算法
可以采用一种类似动态规划的算法,从数组的最左端开始扫描,一直到最右端为止,并记下所遇到的总和最大的子向量。假设我们已经解决了x[0..i-1]的问题,那么前i个元素中,最大总和子数组要么在前i-1个元素中(我们将其存储在maxsofar中),要么其结束位置为i(我们将其存储在maxendinghere中)。
int MaxSum3(int a[],int n)
{
int maxsofar = 0;
int maxendinghere = 0;
for(int i = 0;i < n;i++)
{
maxendinghere = max(0,maxendinghere + a[i]);
maxsofar = max(maxsofar,maxendinghere);
}
return maxsofar;
}
该算法的关键在于理解maxendinghere。在循环中的第一个赋值语句之前,maxendinghere是结束位置为i-1的最大子向量的和,赋值语句将其修改为结束位置为i的最大子向量的和。若加上a[i]之后结果为正,则该赋值语句使maxendinghere增大a[i];若加上a[i]后结果为负,则该赋值语句将maxendinghere重设为0。其运行时间为O(n)。