在《编程珠玑》第八章,作者提出一道算法题:
程序描述:
输入是一个具有N个浮点数字的向量X: 其输出是在输入的任何相邻子向量中找出的最大和。
例如,输入如下10个元素:
31 -41 59 26 -53 58 97 -93 -23 84
返回 x[2..6]的总和,或为187
作者分别给出了三次算法O(n^3),两个二次算法O(n^2),分治算法O(n logn )和扫描算法O(n)。
我对该问题作了研究后,给出自己的一种解法。
考虑数组 x[ N ],我们新开辟一个数组sumX[ N + 1 ],其中sumX[ 0 ] = 0, 而 sumX[ i ] 为 x[ 0..i-1 ] 的和。
这样,对于 x[ i..j ]的和,有 sum( x[ i..j ] ) = sumX[ j+1 ] - sumX[ i ].
对于数组sumX, 假设最大值为 sumX[ posMax ], 最小值为 sumX[ posMin ],下面分三种情况:
1. posMin < posMax,也即最小值在最大值左边,我们可以肯定 x[ posMin..posMax-1 ] 就是所要求的总和。
否则,若x[ i..j ]是所要求的总和,且 i != posMin或 j != posMax,
则有sum( x[ i..j ] ) <= sum( x[ posMin, j ] ) <= sum( x[ posMin, posMax ] )
或sum( x[ i..j ] ) <= sum( x[ i, posMax ] ) <= sum( x[ posMin, posMax ] )
上面两不等式至少有一个严格小于,所以sum( x[ i..j ] ) < sum( x[ posMin, posMax ] )
由上可知,这种情况中返回结果 sumX[ posMax ] - sumX[ posMin ]
2. posMin == posMax,说明除了 x[ posMin ]外,其余x[ i ] 全为0
又由于sumX[ 0 ] = 0,所以 sumX[ posMax ] > 0
所以 x[ posMax - 1 ]是所要求的总和
这种情况中返回结果 sumX[ posMax ],也就是 x[ posMax-1 ] 一个元素
3.posMin > posMax
这种情况比较复杂,我们可以把sumX分为三部分:
a)sumX[ 0 ]..sumX[ posMax ]; b)sumX[ posMax + 1 ] ... sumX[ posMin - 1 ], c)sumX[ posMin ].. sumX[ N + 1 ].
我们可以肯定,要找出的x[ i..j-1 ],也可以认为是sumX[ i],sumX[j ]必在同一个小部分中。
否则,若 0 <= i <= posMax 而 posMax < j
则sum( x[ i..j-1 ] ) = sumX[ j ] - sumX[ i ] < sumX[ posMax ] - sumX[ i ]。
对于其他情况,也可以同样证明。
于是我们可以分别对数组a,b,c处理,找出最大和。
对数组a,找出最小值sumX[ posMin1 ],必有posMin1 < posMax,
于是结果 tmpMax1 = sumX[ posMax ] - sumX[ posMin1 ]
对数组c,找出最大值sumX[ posMax3 ],必有posMin < posMax3,
于是结果 tmpMax3 = sumX[ posMax3 ] - sumX[ posMin ]
对数组b,这是一个比开始问题规模更小的子问题,可再迭代处理,得到结果tmpMax2
这种情况中返回结果 tmpMax1, tmpMax2, tmpMax3中最大值
这种算法在最优情况下(posMin <= posMax)可看作一线性算法,运行时间O(n)
在最坏情况下(每次处理均有posMax = 0, posMin = 数组索引最大值,每个子问题规模减2),
这时可看作二次算法,运行时间O(n^2)
使用随机数调试,发现大部分情况运行时间在O( n logn ) 与 O( n^2 )之间。
测试结果:
N=10 N*logN=23 N*sqrtN=31 N^2=100 程序运行次数:27
N=100 N*logN=460 N*sqrtN=1000 N^2=10000 程序运行次数:404
N=1000 N*logN=6907 N*sqrtN=31622 N^2=1000000 程序运行次数:13009
N=10000 N*logN=92103 N*sqrtN=1000000 N^2=100000000 程序运行次数:683867