您的位置:首页 > 其它

连续子序列最大和问题的分析

2014-07-17 15:03 295 查看


from:http://shmilyaw-hotmail-com.iteye.com/blog/1616632


问题描述

给定(可能是负的)整数序列A1, A2,...,AN, 寻找(并标识)使Sum(Ak)(k >=i, k <= j)的值最大的序列。如果所有的整数都是负的,那么连续子序列的最大和是零。


最简单暴力的解法

这个问题有一个最简单直接的穷举解决法。我们看问题,既然要求里面最大的连续子序列。那么所有的连续子序列将由哪些组成呢?以数组的第一个元素为例,连续子序列必须是至少包含元素A1,也可能包含从A1到A2...以及从A1到AN。这样就有N种可能。后面的元素也按照这样类似的办法,以该元素开始,包含该元素的单元素数组,两个元素数组...直到包含数组末尾的数组。

基于上面的分析,我们可以采用一个两重的循环,假设两个循环的循环变量分别为i, j。第一层循环从第一个元素遍历到后面,第二个元素从>=第一个元素的位置开始到最后。这样就可以遍历到所有的点。然后,我们取所有从i到j的连续数组部分和再比较。这样最终就可以得到最大的连续和以及最大子序列的起始与结束点。

具体的实现代码如下:

Java代码


public static int maxSubSum1( int [ ] a )

{

int maxSum = 0;

for( int i = 0; i < a.length; i++ )

for( int j = i; j < a.length; j++ )

{

int thisSum = 0;

for( int k = i; k <= j; k++ )

thisSum += a[ k ];

if( thisSum > maxSum )

{

maxSum = thisSum;

seqStart = i;

seqEnd = j;

}

}

return maxSum;

}

笼统的来说,这种方法有一个3重循环,时间复杂度有O(N^3)。


改进

前面那个最简单暴力的方法虽然看起来能解决问题,但是循环遍历的次数太多了。里面还是有不少可以改进的空间。比如说,每次我们用变量k遍历i到j的时候,都要计算求和。实际上当每次j增加1时,k把前面计算的结果在循环里又计算了一遍。这是完全没必要的,完全可以重复利用前面的计算结果。这样,通过这么一点,改进,我们可以得到如下的算法代码:

Java代码


public static int maxSubSum2( int [ ] a )

{

int maxSum = 0;

for( int i = 0; i < a.length; i++ )

{

int thisSum = 0;

for( int j = i; j < a.length; j++ )

{

thisSum += a[ j ];

if( thisSum > maxSum )

{

maxSum = thisSum;

seqStart = i;

seqEnd = j;

}

}

}

return maxSum;

}

这种方法更进一步的在于,没必要在i和j之间进行遍历,所以时间复杂度为O(N^2)。对于每个外围循环i来说,当内层的循环j走完一遍,则获得了从i开头到j结束的所有子序列中最大的那个。


线性算法

这个问题还是存在着一个线性时间复杂度的解法。需要我们对数组的序列进行进一步的分析。我们在数组中间找到的连续子序列,可能存在和为负的序列。如果需要找到一个最大的子数组的话,肯定该序列不是在最大子序列当中的。我们可以通过反证的方式来证明。

假设数组A[i...j],里面的元素和为负。如果A[i....j]在一个最大子序列的数组中间,假定为A[i...k],k > j。那么既然从i到j这一段是负的,我把这一段去掉剩下的部分完全比我们假定的这个最大子序列还要大。这就和我的假设矛盾了。

这个假设还有一个限制,就是该数组就是从i开头的。否则有人可能会这么问,如果我A[i...j]这一部分确实是一个负数,但是在A[i]前面是一个很大的正数,使得他们的和为正数。那不就使得我们的结果不成立了么?如果我们是从某个数组的开头i开始的话,就不存在这个情况。

结合前面的讨论,我们就可以发现一个有意思的事情,就是假设我们从数组的开头A[0]开始,不断的往后面走,每一步判断是否当前和最大,并保存结果。当发现当前字串和为负数的时候,我们可以直接跳过。假设当前的索引为i的话,从0到i这一段的和是负数,可以排除。然后再从当前元素的后面开始找。这样可以得到最终最大子串和以及串的起点和终点。

详细的实现代码如下:

Java代码


public static int maxSubSum3( int [ ] a )

{

int maxSum = 0;

int thisSum = 0;

for( int i = 0, j = 0; j < a.length; j++ )

{

thisSum += a[ j ];

if( thisSum > maxSum )

{

maxSum = thisSum;

seqStart = i;

seqEnd = j;

}

else if( thisSum < 0 )

{

i = j + 1;

thisSum = 0;

}

}

return maxSum;

}

该方法只需要遍历数组一遍,通过跳过这些中间和为负的结果。算法时间复杂度为O(N).


总结分析

这是一个比较有意思的问题。以前和朋友们曾经讨论过。当然,是在没看过书上的那么多分析之后自己也想到了一个近似O(N)的解法。当时还利用了一种情形,将所有为相邻为正的元素以及为负的元素分别累加起来构成一个新的数组。因为如果要达到最大的子数组,要么就要覆盖所有相邻的正整数,要么会包含一段相邻的负整数子序列。然后形成一个正负数相间的数组。这样再利用前面的线性算法特性进行比较。虽然不如前面的好,但是这是自己思考的结果,总是有价值的。

另外,在某些情况下会出现上述问题的一个变种,就是假设我们需要支持负数的情况。最坏的情况就是所有元素都是负的,那么就相当于在里面找到最大的那个元素。我们的代码就需要稍微做一点修改。至于怎么改,如果你看明白了分析和代码的话,你懂的:)

还有一个需要说明一点的就是,在实现的代码中用到了seqStart和seqEnd两个变量。可以将这两个元素定义为类的static变量。这样就构成一个完整的程序了。


参考资料

Data structures
and problem solving using java
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: