最大子序列和问题 一步一步到最优

来源:互联网 发布:游戏服务端数据库架构 编辑:程序博客网 时间:2024/05/01 22:18

       在《数据结构和算法分析 C++描述》上看到了一个例子。看过之后,我就在想,这是怎么一步一步的递推出来的,想了好长时间,才整理成这篇博文。

问题描述:

        给定一个整数序列,a0, a1, a2, …… , an(项可以为负数),求其中最大的子序列和。如果所有整数都是负数,那么最大子序列和为0;

                  例如:对于序列-2, 11, -4, 13, -5, –2。 所求的最大子序列和为20(从11到13,即从a1到a3)。

       用于测试下面代码的的主函数代码如下:(注意要更改调用的函数名)

int main(int argc, char **argv) {    vector<int> a;    a.push_back(-2);    a.push_back(11);    a.push_back(-4);    a.push_back(13);    a.push_back(-5);    a.push_back(-2);        int result;    result = maxSubSum1(a);  //在这里更改调用的函数名    cout<<result<<endl;  //正确结果为 20    return 0;}

方法一:对所有的子序列求和,在其中找到最大的

        这是最容易想到的,也是最直接的,就是对所有的子序列求和,代码如下:

/** * 方法 1:计算所有两点之间的序列之和,每次循环固定i,把a[i]当做起点 */int maxSubSum1( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        for(int j=i; j<a.size(); j++ ) {            int thisSum =0;            for( int k=i; k<=j; k++ ) {                thisSum += a[k];            }            if(thisSum>maxSum) {                maxSum = thisSum;            }        }    }    return maxSum;}

分析:

         ① 三层循环嵌套,时间复杂度为O(n^3)

         ② 包含有大量的重复计算,例如i=1时: 当 j=3,则计算a1+a2+a3;j=4,则计算a1+a2+a3+a4;其中a1+a2+a3是重复计算的。

另一种思路:上面的方法在每一次循环中,固定i,并把a[i]当做起点,下面的方法将a[i]当做终点。

/** * 方法 1_2:每次循环固定i,把a[i]当做终点 */int maxSubSum1_2( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        for(int j=0; j<i; j++ ) {            int thisSum =0;            for(int k=j; k<=i; k++) {                thisSum +=a[k];     //从节点j开始 累加到节点i                if(thisSum>maxSum) {                    maxSum = thisSum;                }            }        }    }    return maxSum;}

方法二:从某点开始的所有序列中,找最大的

         如果你意识到,子序列总要有一个位置开始,那么变换一下循环方式,只要求出在所有位置开始的子序列,找到最大的。代码如下:

/** * 方法 2:从某一个节点开始的序列中,找最大的子序列和 */int maxSubSum2( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        int thisSum =0;        for(int j=i; j<a.size(); j++ ) {            thisSum +=a[j];     //从节点i开始 累加到结尾            if(thisSum>maxSum) {                maxSum = thisSum;            }        }    }    return maxSum;}

分析:

         ① 两层循环嵌套,时间复杂度为O(n^2)

         ② 虽然比方法一要少,但同样包含重复计算。例如:当i=1时,要计算a1+a2+a3+a4+……;当i=2时,要计算a2+a3+a4+……;其中a2+a3+a4+……是重复的。

注意:下面是一个错误的方法,因为它的起始点固定了,每次都从a0开始,是不能保证遍历所有的子序列的。

/** * 方法 2_2:(错误的方法)起始点固定了 */int maxSubSum2_2( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        int thisSum =0;        for(int j=0; j<=i; j++ ) {            thisSum +=a[j];     //从节点0开始 累加到节点i            if(thisSum>maxSum) {                maxSum = thisSum;            }        }    }    return maxSum;}

如果希望将固定终点,那么计算的时候就要从终点开始,依次往前累加。代码如下:

/** * 方法 2_3:(正确的代码)从终点开始,依次往前累加 */int maxSubSum2_3( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        int thisSum =0;        for(int j=i; j>=0; j-- ) {//从节点i开始,向前累加到结尾0            thisSum +=a[j];                 if(thisSum>maxSum) {                maxSum = thisSum;            }        }    }    return maxSum;}

方法三:从某一个正数开始

第一步:

        到目前为止,题目的信息你只用到了:“最小子序列之和为0”(若有一项大于0,那么子序列的和一定大于或等于该项,也就大于0;因为若所有项都是负数,那么结果为0

        如果你再挖掘一下题意:你就会发现,如果a[i]是负的,那么a[i]一定不是最终所有结果子序列的起始点。代码可以改造为:

/** * 方法 3_1:如果a[i]是负数,那么不可能作为序列的起点 */int maxSubSum3_1( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        //(相对方法2,新增)如果a[i]<=0,那么a[i]一定不是所要求的起点,所以直接跳过去(利用for循环中有i++)        if( a[i]<=0 ) {            continue;        }        int thisSum =0;        for(int j=i; j<a.size(); j++ ) {            thisSum +=a[j];            if(thisSum>maxSum) {                maxSum = thisSum;            }        }    }    return maxSum;}

第二步:

        如果你又再一步发现:任何负的子序列,不可能作为最优子序列的前缀

        又因为上一步已经保证,序列以正数开头a[i]>0,所以若a[i]到a[j]之间元素的序列和 thisSum<=0时,则i+1和j之间元素不会为最优子序列的前缀,可以让i=j,即不需要判断在i和j之间元素开头。代码如下

/** * 方法 3_2:任何负的子序列,不可能作为最优子序列的前缀 *              (在上一步已经保证,序列以正数开头,所以若thisSum<=0, *                  那么可以令i=j,即不需要判断在i和j之间元素开头的序列) */int maxSubSum3_2( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        //(相对方法2,新增)如果a[i]<=0,那么a[i]一定不是所要求的起点,所以直接跳过去(利用for循环中有i++)        if( a[i]<=0 ) {            continue;        }        int thisSum =0;        for(int j=i; j<a.size(); j++ ) {            thisSum +=a[j];            if(thisSum>maxSum) {                maxSum = thisSum;            } else if( thisSum <= 0 ) { //(相对方法3_1 新添)                thisSum = 0;                i = j;            }        }    }    return maxSum;}

 

第三步:

        如果你又进一步发现:因为要求序列开始元素大于0,
                若以a[i]开头的序列,a[i]>0,那么可以知道,所求的最终子序列一定不会以a[i+1]开始,
                      因为若到相同的元素终止,那么从a[i]开始序列,一定大于从a[i+1]开始的序列。因为s[i, k]=s[i+1, k]+a[i]
                      例如:a1+a2+a3>0,而又由于这时a1>0, 那么所求子序列一定不会以a2开始,因为从a1开始会更大。
                更进一步,如若一个子序列thisSum>0(其中thisSum是从第m项到第n项的和),那么序列一定不会以a[m]和a[n]之间的项开始。
                      因为一直thisSum第一个元素a[m]是大于0的,且以a[m]开始的所有子序列都是大于0的,因为若存在子序列小于0,就会提前返回了。
                      例如:若程序执行到thisSum (为a2+a3+a4),若thisSum>0,则能说明,a2>0,并且a2+a3>0, a2+a3+a4>0。那么 也可以跳过a[m]和a[n]之间的项,即另 i=j。

/** * 方法 3_3:若程序执行到thisSum (为a2+a3+a4),若thisSum>0,则能说明,a2>0,并且a2+a3>0, a2+a3+a4>0。 */int maxSubSum3_3( const vector<int> &a ) {    int maxSum = 0;    for(int i=0; i<a.size(); i++ ) {        //(相对方法2,新增)如果a[i]<=0,那么a[i]一定不是所要求的起点,所以直接跳过去(利用for循环中有i++)        if( a[i]<=0 ) {            continue;        }        int thisSum =0;        for(int j=i; j<a.size(); j++ ) {            thisSum +=a[j];            if( thisSum>0 ) {   //(相对方法3_2 新添)                if(thisSum>maxSum) {                    maxSum = thisSum;                }                i = j;  //(相对方法3_2 新添)            } else if( thisSum <= 0 ) { //(相对方法3_1 新添)                thisSum = 0;                i = j;            }        }    }    return maxSum;}

第四步:

最后如果你又了解一些程序结构上的优化的知识,那么你会发现下面的问题:

① 循环的分支可以改变一下,去除嵌套分支结构。

② 判断语句的分支中有共同部分,( i=j ),可以抽取出来。

以上两步以后,循环部分的代码编程 变成:

for(int i=0; i<a.size(); i++ ) {    if( a[i]<=0 ) {        continue;    }    int thisSum =0;    for(int j=i; j<a.size(); j++ ) {        thisSum +=a[j];        if(thisSum>maxSum) {            maxSum = thisSum;        } else if( thisSum>0 ) {            //do nothing        } else if( thisSum <= 0 ) {            thisSum = 0;        }        i = j;    }}

 

③ 下面这步非常重要,如果你发现,内层循环的循环变量j 和 外层循环的循环变量i 同步增长,那么你是否能够想到,外层循环可能没有存在的必要。 在这里到底能不能去除外层循环,取决于外层循环中是否有额外的工作要做。这里的额外工作是是if(a[i] <=0) 判断语句,如果你能发现内层循环的 if(thisSum < 0)的判断能够替代 if( a[i]<=0 ) 的工作。因为thisSum是由a[j]得到的。

到这里,代码就可以神奇的变为如下的形式:常量空间,线性时间

/** * 方法 3_4:最终形式 */int maxSubSum3_4( const vector<int> &a ) {    int maxSum = 0;    int thisSum = 0;    for(int j=0; j<a.size(); j++ ) {        thisSum += a[j];        if(thisSum>maxSum) {            maxSum = thisSum;        } else if( thisSum>0 ) {            //do nothing        } else if( thisSum < 0 ) {            thisSum = 0;        }    }    return maxSum;}

分析:

        ① 只有一层循环,时间复杂度为O(n)常量空间,线性时间,这是最优解法

方法四:分治算法(divide and conquer)

本方法和前面三种方法没有直接关系,只是思路的开阔。

因为最大子序列只可能出现在三个地方:①整个出现在原数组的左半部, 或者②整个出现在右半部, 或者③跨越中间、从左半部到右半部。

对于第①种和第②种情况,采用递归的方式可以解决。对于第③种情况,分别求出从中间位置开始的最大值然后相加即可。最后比较这三种情况的结果,三者的最大值就是所要求的结果。

/** * 方法 4:分治策略 */ //计算三个整数的最大值int max3(int i, int j, int k){    return (i>j) ? ((i>k)?i:k) : ((j>k)?j:k);}//分治策略的递归函数int maxSumRec( const vector<int> &a, int left, int right ){    //终止条件    if( left==right ){        if( a[left]>0 ){ //如果该项大于0,才返回            return a[left];        }else{  //如果该项小于0,返回0            return 0;        }    }    //计算左半部 和 右半部    int center = (left+right)/2;    int maxLeftSum = maxSumRec(a, left, center);    int maxRightSum = maxSumRec(a, center+1, right);        //计算跨越中间    int maxLeftBorderSum=0, leftBorderSum=0;    for( int i=center; i>=left; i-- ){        leftBorderSum += a[i];        if(leftBorderSum > maxLeftBorderSum ){            maxLeftBorderSum = leftBorderSum;        }    }        int maxRightBorderSum=0, rightBorderSum=0;    for( int i=center+1; i<=right; i++ ){        rightBorderSum += a[i];        if(rightBorderSum > maxRightBorderSum ){            maxRightBorderSum = rightBorderSum;        }    }    //返回三个部分的最大值    return max3(maxLeftSum, maxRightSum, maxLeftBorderSum+maxRightBorderSum ); }int maxSubSum4( const vector<int> &a ) {    return maxSumRec( a, 0, a.size()-1 );}

分析:

        ① 时间复杂度为O(n*logn)。时间复杂度的递归公式为:T(N) = 2T(N/2) + N。解方程可以得到 T(N)=O(N*logN)。(计算过程较复杂,没有深究的必要)

总结:

        上面罗列的一步一步的演变过程,并不是绝对的因果关系,有可能你根本没有从方法一逐渐的演变,直接就能写出方法三的最后一种方法(最优解法)。

        但是如果你没有直接写出来,那么你是否会考虑重构你的代码?其实在逐步追求优化的过程中,那感觉还是非常好的!

 

本文链接:http://blog.csdn.net/daheiantian/archive/2011/03/03/6453295.aspx