连续子序列最大和的O(NlogN)算法
对于一个数组,例如:
int[] a = {4,-3,5,-2,-1,2,6,-2}
找出一个连续子序列,对于任意的i和j,使得a[i]+a[i+1]+a[i+2]+.......+a[j]他的和是所有子序列中最大的,这个连续子序列
被称为和最大的连续子序列,上面那个例子的连续子序列最大和应该是11,
由4 + -3 + 5 + -2 + -1 + 2 + 6 = 11得出,但是如果我们用程序
表示应该如何进行又快又好地计算呢?
最近正在看《数据结构和问题求解》这本书,书上介绍了一个分治算法(至少含有两个递归的算法叫分治),
看的我晕晕乎乎的,所以在这里写一下我对这个O(NlogN)算法的理解,加深一下理解程度= =
计算一个序列的连续子序列最大和
如:int[] a = {4,-3,5,-2,-1,2,6,-2}
考虑将这个序列划分成两个部分
即 4,-3,5,-2 ||| -1,2,6,-2
有三种情况
1.这个和最大的连续子序列出现在这个序列的左边
2.这个和最大的连续子序列出现在这个序列的右边
3.这个和最大的连续子序列横跨左右,是从左边某一个地方到右边某一个地方
我们先来考虑第3种情况,和最大的连续子序列是在中间的情况,
首先,如果连续子序列在中间的话,以下面这个序列为例:
4,-3,5,-2 ||| -1,2,6,-2
那-2和-1是肯定在这个连续子序列里的,因为要横跨分开来的左右两个子序列,
必然包括左边序列的最后一个元素和右边序列的最后一个元素,有了这个认识之后
后面的要做的事就变得简单了,我们可以先找①左边这个序列包括-2的和最大子序列,
再找②右边这个序列包括-1的和最大子序列,那么这个横跨左右序列的和最大子序列
的必然是由前面那两个子序列①②相加而来。
再①②上找最大子序列的操作实质是一个复杂度为O(N)的操作,即
从-2开始,往前面扫,到5的时候,和是3,到-3的时候和是0,到4的时候和是4,由此确定4是
右边这个序列的最大的和,-2 + 5 + -3 + 4则是包含-2的和最大连续子序列,以下写出在运算过程
的值
序列: 4 , -3 , 5 , -2 ||| -1 , 2 , 6 , -2
运算过程的值: *4 0 3 -2 -1 1 *7 5
带星号的是在当前分到的序列中最大的和
所以最后答案就是4+7 = 11
有此我们可以知道第三种情况是可以在O(N)时间内解决的
接着我们看第一和第二种情况,即和最大的连续子序列在左边或者在右边的情况,此时如果我们
对这种情况依然进行穷举的O(N2)的n方算法的话,实际上最后时间也没有减少多少,所以我们要
想到更好的办法来对付这个第一和第二种情况。首先,想想看既然第三种情况时间复杂度这么短(O(N))
要是每次操作都是第三种情况,不是最好么,那么有没有可能把第一种和第二种情况变成第三种情况呢,
那要是把第一种和第二种情况下的序列再分成两半直到分到不能再分为止呢?也就是,递归的解决情况1和
情况2,使情况1和2变成了更小的问题,简单来说
步骤@1
一个序列:
4,-3,5,-2,-1,2,6,-2
先把他分成两半
4 , -3 , 5 , -2 ||| -1 , 2 , 6 , -2
这个时候,我们的目标从最初的计算整个序列(4,-3,5,-2,-1,2,6,-2)的连续子序列最大和转化成了计算下面三个里面最大的那个
即max(①,②,③)
①计算左边(4 , -3 , 5 , -2)的序列的最大和和
②右边的(-1 , 2 , 6 , -2)序列的最大和
③中间(即肯定包含-2和-1的连续子序列)的(4 , -3 , 5 , -2 ||| -1 , 2 , 6 , -2)序列的最大和,
中间序列由我们上面的方法可以算出来,这个时候我们再单独看左边这个序列
步骤@2
一个序列:
4 , -3 , 5 , -2
先把他分成两半
4 , -3 ||| 5 , -2
是的,这个新的序列(4 , -3 , 5 , -2),我们的目标要计算他的所有可能的连续子序列的最大和,那么,我们的目标又可以
进一步的转化成求max(①,②,③)
①计算左边(4 , -3)的序列的最大和和
②右边的(5 , -2)序列的最大和
③中间(即肯定包含-3和5的连续子序列)的(4 , -3 ||| 5 , -2)序列的最大和,
再单独看左边这个序列,嗯?是不是有一种很熟悉的感觉,是的,依然是
步骤@3
一个序列:
4 , -3
先把他分成两半
4 ||| -3
现在的情况是基线情况,也就是,把他分到不能再分了,那么,左边的连续子序列的最大和不是一目了然么
(就是4啦),右边的连续子序列最大和是0(注意如果一个序列全是负数,那他的连续子序列和是0,也就是
什么也不选的情况)
这个时候知道了步骤@2的左边序列的最大和(也就是4),也知道了中间序列的最大和(依我们前面提到的方法),我们再
看步骤@2的右边序列....那么,又开始一波熟练的操作了:
步骤@4
一个序列:
5 , -2
先把他分成两半
5 ||| -2
可以知道步骤@2的右边序列的最大和是5,那么现在让我们回到步骤@2,步骤@2的中间序列的最大和是6,这个时候终于可以知道
步骤@2的那个一整个序列的连续子序列最大和是多少啦,也就是5,6,4三个里的最大值,那么当然是6啦,然后我们再手算验证一下
步骤@2的序列(4 , -3 , 5 , -2)
4 + -3 + 5 = 6
的确是这个序列所有可能的连续子序列里的是最大和的子序列,这个时候我们已经知道了步骤@1(也就是我们的目标序列)的左边的序列的
最大和了,哦,当然,还有我们中间序列的最大和,距离我们的目标已经完成了三分之二了>.< , 接着就是按照刚刚那样同样的方法
进行右边序列的最大和的递归计算,至此,我们达成了前面提到的那三个目标(①,②,③)
①计算左边(4 , -3 , 5 , -2)的序列的最大和和
②右边的(-1 , 2 , 6 , -2)序列的最大和
③中间(即肯定包含-2和-1的连续子序列)的(4 , -3 , 5 , -2 ||| -1 , 2 , 6 , -2)序列的最大和
那么求这个最终序列(4 , -3 , 5 , -2,-1 , 2 , 6 , -2)的连续子序列最大和就可以顺利完成了~~~~~~
以下是我的java代码:
错误版本:
1 public static int calculate_continuous_sequence(int[] a,int left,int right){ 2 //在左边的序列的连续子序列最大和 3 int maxLeftSum_continuous_sequence = 0; 4 //在右边的序列的连续子序列最大和 5 int maxRightSum_continuous_sequence = 0; 6 //在中间的序列的连续子序列最大和 7 int maxMidSum_continuous_sequence = 0; 8 9 //基线情况,变成一个元素的情况,那么 10 //这个元素就是她自己的连续子序列的最大和(负数是0) 11 if(left==right){ 12 if(a[left]<0){ 13 return 0; 14 } 15 return a[left]; 16 } 17 18 int mid = (left+right)/2; 19 //计算左边的连续子序列和的最大值 20 maxLeftSum_continuous_sequence = calculate_continuous_sequence(a,left,mid); 21 //计算右边的连续子序列最大值 22 maxRightSum_continuous_sequence = calculate_continuous_sequence(a,mid+1,right); 23 24 25 int mid_left_Sum = 0; //计算中间的时候左边的最大和 26 int mid_right_Sum = 0; //计算中间的时候右边的最大和 27 int result = 0; 28 //接着计算中间的连续子序列最大值 29 for(int i=mid;i>=0;i--){ 30 result += a[i]; 31 if(result>mid_left_Sum){ 32 mid_left_Sum = result; 33 } 34 } 35 result = 0; 36 for(int i=mid+1;i<=a.length-1;i++){ 37 result += a[i]; 38 if(result>mid_right_Sum){ 39 mid_right_Sum = result; 40 } 41 } 42 43 maxMidSum_continuous_sequence = mid_left_Sum+mid_right_Sum; 44 45 return max3(maxLeftSum_continuous_sequence,maxRightSum_continuous_sequence,maxMidSum_continuous_sequence); 46 } 47 public static int max3(int i,int j,int k){ 48 int a = Math.max(i, j); 49 return Math.max(a,k); 50 }
果然我还是太渣渣,以为自己还算熟练了...结果一写就犯了小毛病= =,在上面我的错误代码里面,计算连续子序列最大和在中间的情况错了
计算中间的情况应该是从每一个序列的左边出发到中间,从中间出发到右边,然后两两相加,结果我写成了从a序列的左边到中间,从a序列
的中间到右边,而不是每次递归下去变成小问题的新序列..所以就错的离谱啦~~~还有另一个错误,问题依然在计算中间的那段代码中~~~
下面是正确版本:
1 /** 2 * @param a : 要求的序列 3 * @param left : 从要求的序列的哪里开始 4 * @param right : 从要求的序列哪里结束 5 * @return 返回序列a的ai~aj范围内连续子序列最大和 6 */ 7 public static int calculate_continuous_sequence(int[] a,int left,int right){ 8 //在左边的序列的连续子序列最大和 9 int maxLeftSum_continuous_sequence = 0; 10 //在右边的序列的连续子序列最大和 11 int maxRightSum_continuous_sequence = 0; 12 //在中间的序列的连续子序列最大和 13 int maxMidSum_continuous_sequence = 0; 14 15 //基线情况,变成一个元素的情况,那么 16 //这个元素就是她自己的连续子序列的最大和(负数是0) 17 if(left==right){ 18 if(a[left]<0){ 19 return 0; 20 } 21 return a[left]; 22 } 23 24 int mid = (left+right)/2; 25 //计算左边的连续子序列和的最大值 26 maxLeftSum_continuous_sequence = calculate_continuous_sequence(a,left,mid); 27 //计算右边的连续子序列最大值 28 maxRightSum_continuous_sequence = calculate_continuous_sequence(a,mid+1,right); 29 30 31 int mid_left_Sum = 0; //计算中间的时候左边的最大和 32 int mid_right_Sum = 0; //计算中间的时候右边的最大和 33 int result = 0; 34 //接着计算中间的连续子序列最大值 35 for(int i=mid;i>=left;i--){ 36 result += a[i]; 37 if(result>mid_left_Sum){ 38 mid_left_Sum = result; 39 } 40 } 41 result = 0; 42 for(int i=mid+1;i<=right;i++){ 43 result += a[i]; 44 if(result>mid_right_Sum){ 45 mid_right_Sum = result; 46 } 47 } 48 49 maxMidSum_continuous_sequence = mid_left_Sum+mid_right_Sum; 50 51 return max3(maxLeftSum_continuous_sequence,maxRightSum_continuous_sequence,maxMidSum_continuous_sequence); 52 } 53 public static int max3(int i,int j,int k){ 54 int a = Math.max(i, j); 55 return Math.max(a,k); 56 }
因为本人只是个刚开始学算法的渣渣,如果有什么错误,希望大家可以指出