算法:最大子序列求和问题

最大子序列求和是指给定一组序列,求所有连续子序列的和中的最大值,例如给定数列:

[5,-2,-5,6]最大子序列和是6;[1, 2, -3, 4, -5, 6, 7, 8, -9, 10]最大子序列和是22;

下面将利用几种不同的算法来解决此问题,重要的是理解不同算法中所代表的思想

1、穷举法

穷举法的思想比较简单,它是指列举所有的可能,来得到问题最终的解;

在此问题中,可以利用穷举法将所有的子序列的和计算出来,来得到最大子序列的和;

假设子序列的起点为i,那么i的范围在数组下标中可以是:[0,arrays.length-1];

针对起点为i的子序列,由于子序列是连续的,那么它的终点的范围是[i,arrays.length-1];

最后我们需要对[i,j]的子序列进行求和,并把结果每次与max比较,以此得到最大子序列和max;

    //穷举法,时间复杂度为O(N^3)
    public static int method_1(int[] arrays) {
        int max = 0;
        //每个子序列的起点 = i
        for (int i = 0; i < arrays.length; i++) {
            //每个子序列的终点 = j
            for (int j = i; j < arrays.length; j++) {
                int sum = 0;
                //子序列求和
                for (int k = i; k <= j; k++) {
                    sum = sum + arrays[k];
                }
                //当出现子序列和大于max,用sum替换掉max
                if (sum > max) {
                    max = sum;
                }
            }
        }
        return max;
    }

2、穷举优化

显然,上面算法的时间复杂度O(N^3)并不能让人满意,不过我们可以简单优化一下:

列举所有的子序列[i,j]依然不变,但是针对子序列[i,j]求和,我们完全可以省略这一步,当固定起点i时,以i为起点的子序列的终点j的范围[i,arrays.length-1]是连续的,可以发现,j=i+1为子序列的终点时,它的和为:

        SUM(i,j)=SUM(i,i+1)=SUM(i,i)+arrays[i+1];

同理,j=i+2时:

        SUM(i,j)=SUM(i,i+2)=SUM(i,i+1)+arrays[i+2]

……

也就是说,我们可以把上一次子序列的求和保存起来,留待下次j递增(j++)后使用,即不用针对每次子序列[i,j],去重新计算它的和,在代码里,只需要把对sum的初始化int sum = 0 提到上一层循环里就可以了,并去掉重复计算的for循环就可以了,此方法的时间复杂度为O(N^2)

    //穷举优化:时间复杂度为O(N^2)
    public static int method_2(int[] arrays) {
        int max = 0;
        //每个子序列的起点 = i
        for (int i = 0; i < arrays.length; i++) {
            int sum = 0;
            //每个子序列的终点 = j
            for (int j = i; j < arrays.length; j++) {
                sum = sum + arrays[j];
                //当出现子序列和大于max,用sum替换掉max
                if (sum > max) {
                    max = sum;
                }
            }
        }
        return max;
    }

这里稍微有点动态规划的思想,但并不完全,下面我们将介绍基于动态规划的思想的Kadane算法对此问题的解法思路

3、Kadane算法-动态规划

        

        穷举法是针对具体的子序列[i,j]去求解,虽然做了优化减少了重复计算,但依然需要比较高的时间复杂度。

        最大子序列和的最终答案是值,而不用去求具体的子序列,所以这里我们可以巧妙的运用动态规划的思想来解决,动态规划的核心思想是:拆分成若干子问题,记住过往,减少重复计算。

        假设我们求长度为N的序列的最大子序列和,可以拆分成N个子问题来计算,假设此数组序列下标是i,那么这N个子问题分别是:i=[0]、i=[0,1]、i=[0,1,2]、......、i=[0,1,2,...,N-1]的子序列的最大子序列和,我们这里可以不用逆推,直接采用顺推的方式来实现。

        我们可以根据i=[0]的子序列的结果,去推算i=[0,1]的结果,然后用i=[0,1]的子序列的结果去推算i=[0,1,2]的结果,以此类推,最终推算出i=[0,1,2,...,N-1]的结果;

        然而值得我们注意的是,我们需要知道使用前一个子问题去推算后一个子问题,它们之间的连接关系:

        

        当 i(k)=[0,1,2,3,...k] --> i(k+1)=[0,1,2,3,...k,k+1],即用i(k)结果去推算i(k+1)的结果的时候:

        我们定义两个变量maxsum;max(k)代表i(k)的结果(即最大子序列和),sum(k)代表

i(k)序列的累加,(max ≠ sum);

                sum(k+1) = sum(k) + arrays[k+1]

                当sum(k+1) > max(k) 时,则 max(k+1) = sum(k+1)

                否则max(k+1)=max(k);

                要使上述成立我们必须所做的一个操作是,当sum < 0时,需要把sum = 0(结合下面i = 4/5时更容易理解)

例如对于数组[1, 2, -3, 4, -5, 6, 7, 8, -9, 10],

Arrays12-34-5678-910
i(下标)0123456789

详解,当遍历到:

        i=0时:

                sum=1  ----> sum>max ----> max = 0 : 序列[1]的最大子序列和为 1

        i=1时:

                sum = 3 ----> sum>max ----> max = 3 : 序列[1,2]的最大子序列和为3

        i=2时:

                sum=0 ----> sum<max ----> max = 3 : 序列[1,2,-3]的最大子序列和为3

        i=3时:

                sum=4 ----> sum>max ----> max = 4 : 代表序列[1,2,-3,4]的最大子序列和为4

(注意:此时的和为最大的值的子序列有两个[1,2,-3,4],[4],但我们并不在意,我们需要在意的是状态(sum是否小于0)以及max(sum是否大于max),sum如果一直没有小于0,那么sum的值对于后面的累加都是有效的增大,而sum只要一小于0,我们需要把sum=0,它才不影响后面最大值的累加。强调一下:我们只关注当前所遍历过的这整个一块的最大子序列的和max以及sum值的状态

        i=4时:

                sum=-1 ----> sum<max ----> max = 4 ---->sum=0:序列[1,2,-3,4,-5]的最大子序列和为4

        i=5时:

                sum=6 ----> sum>max ----> max = 6 : 序列[1,2,-3,4,-5,6]的最大子序列和为6

        ......

        i=8时:

                sum=6+7+8-9 ----> sum<max(6+7+8) ----> max=21 : 序列[1, 2, -3, 4, -5, 6, 7, 8, -9]的最大子序列和为21

        i=9时:

                sum=6+7+8-9+10=22 ----> sum > max ----> max = 22 :序列[1, 2, -3, 4, -5, 6, 7, 8, -9, 10]的最大子序列和为22。

        

        可以清晰的看到,我们只需要一次遍历,就可以得到最终的解,时间复杂度为O(N);

代码如下:               

    //动态规划,时间复杂度O(N)
    public static int method_3(int[] arrays) {
        int max = 0;
        int sum = 0;
        for (int i = 0; i < arrays.length; i++) {
                sum = sum + arrays[i];
                if (sum > max) {
                    max = sum;
                }else if(sum < 0){
                    sum = 0;
                }
        }
        return max;
    }

4、分治策略

        分治策略的核心思想为:分而治之。

        它跟动态规划相同点:都是将问题分成若干个子问题。

        不同点:动态规划的场景一般为:每个子问题与前后都是有联系的,以1推2,以2推3,依次推到最终结果;而分治策略的场景一般为:先独立的计算出每个子问题的结果,再合并结果得到最终结果。

左半部分                  起点                    右半部分
12-34-5678-910

        在此问题中,我们也同样可以使用分治策略来解决:我们很容易知道最大子序列和可能在三处出现,如:

        A、左半部分:

        B、右半部分:

        C、中间部分(左右都占):

        AB两种情况我们可以通过递归求解;C情况我们可以算出以左半部分的起点向左依次遍历的最大和值,以右半部分的起点向右依次遍历得出的最大和值,两者相加;最终我们比较ABC三者的最大值,即可返回最终结果。

        此种方法的时间复杂度为O(N*logN),想比较动态规划而言,它有些地方重复计算了,但是如果将问题转换为求最大子序列的起点跟终点,动态规划或许不太适用了。

                ​​​
    //分治策略,时间复杂度为O(N*logN)
    public static int method_4(int[] arrays,int left,int right) {
        //基准情况
        if(left == right){
            if(arrays[left] > 0){
                return arrays[left];
            }else{
                return 0;
            }
        }

        int center = (left+right)/2;
        //递归求左半部分(A情况)
        int maxLeft = method_4(arrays,left,center);
        //递归求右半部分(B情况)
        int maxRight = method_4(arrays,center+1,right);
        
        //求C情况的以左半部分起点向左遍历的最大值
        int maxLeftBorder = 0;
        int sumLeftBorder = 0;
        for(int i = center;i>=left;i--){
            sumLeftBorder = sumLeftBorder+arrays[i];
            if(sumLeftBorder>maxLeftBorder){
                maxLeftBorder = sumLeftBorder;
            }
        }
        
        //求C情况的以右半部分起点向右遍历的最大值
        int maxRightBorder = 0;
        int sumRightBorder = 0;
        for(int i = center+1;i<=right;i++){
            sumRightBorder = sumRightBorder+arrays[i];
            if(sumRightBorder>maxRightBorder){
                maxRightBorder = sumRightBorder;
            }
        }
        //返回 maxLeft 、maxRight、maxLeftBorder+maxRightBorder中的最大值
        int temp = Math.max(maxLeft, maxRight);
        return Math.max(temp,maxLeftBorder+maxRightBorder);
    }

  • 10
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SRG仁港

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值