• 最大子段和问题(dp)


    最大子段和问题

    给出一个整数数组a(正负数都有),如何找出一个连续子数组(可以一个都不取,那么结果为0),使得其中的和最大?
     
    例如:-2,11,-4,13,-5,-2,和最大的子段为:11,-4,13。和为20。
     
     
    看见这个问题你的第一反应是用什么算法? 

    (1) 枚举?对,枚举是万能的!枚举什么?子数组的位置!好枚举一个开头位置i,一个结尾位置j>=i,再求a[i..j]之间所有数的和,找出最大的就可以啦。好的,时间复杂度?

    (1.1)枚举i,O(n)
    (1.2)枚举j,O(n)
    (1.3)求和a[i..j],O(n)
     
    大概是这样一个计算方法:
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
     
     
    for(int 1<= ni++)
    {
        for(int i<= nj++)
        {
            int sum 0;
            for(int i<= jk++)
                sum += a[k];
                
            max Max(maxsum);
        }
    }
     
     
     
     
    所以是O(n^3), 复杂度太高?降低一下试试看?
    (2) 仍然是枚举! 能不能在枚举的同时计算和?
    (2.1)枚举i,O(n)
    (2. 2)枚举j,O(n) ,这里我们发现a[i..j]的和不是a[i..j – 1]的和加上a[j]么?所以我们在这里当j增加1的时候把a[j]加到之前的结果上不就可以了么?对!所以我们毫不费力地降低了复杂度,得到了一个新地时间复杂度为O(n^2)的更快的算法。
     
    大概是这样一段代码:
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
     
     
    for(int 1<= ni++)
    {
        int sum 0;
        
        for(int i<= nj++)
        {
            sum += a[j];
            max Max(maxsum);
        }
    }
     
     
     
     
    是不是到极限了?远远不止!
    (3)分治一下?

    我们从中间切开数组,原数组的最大子段和要么是两个子数组的最大子段和(分), 要么是跨越中心分界点的最大子段和(合)。 那么跨越中心分界点的最大子段合怎么计算呢?仍然是枚举! 从中心点往左边找到走到哪里可以得到最大的合,再从中心点往右边检查走到哪里可以得到最大的子段合,加起来就可以了。可见原来问题之所以难,是因为我们不知道子数组从哪里开始,哪里结束,没有“着力点”,有了中心位置这个“着力点”,我们可以很轻松地通过循环线性时间找到最大子段和。

    于是算法变成了

    (3.1)拆分子数组分别求长度近乎一半的数组的最大子段和sum1, sum2

    时间复杂度 2* T(n / 2)

    (3.2)从中心点往两边分别分别找到最大的和,找到跨越中心分界点的最大子段和sum3 时间复杂度 O(n)

    那么总体时间复杂度是T(n) = 2 * T(n / 2) + O(n) = O(nlogn), 又优化了一大步,不是吗?
    还能优化吗?再想想,别放弃!
     
    我们在解法(3)里需要一个“着力点”达到O(n)的子问题时间复杂度,又在解法(2)里轻易地用之前的和加上一个新的元素得到现在的和,那么“之前的和”有那么重要么?如果之前的和是负数呢?显然没用了吧?我们要一段负数的和,还不如从当前元素重新开始了吧?

    再想想,如果我要选择a[j],那么“之前的和”一定是最大的并且是正的。不然要么我把“之前的和”换成更优,要么我直接从a[j]开始,不是更好么?

    动态规划大显身手。我们记录dp[i]表示以a[i]结尾的全部子段中最大的和。我们看一下刚才想到的,我取不取a[i – 1],如果取a[i – 1]则一定是取以a[i – 1]结尾的子段和中最大的一个,所以是dp[i – 1]。 那如果不取dp[i – 1]呢?那么我就只取a[i]孤零零一个好了。注意dp[i]的定义要么一定取a[i]。 那么我要么取a[i – 1]要么不取a[i -1]。 那么那种情况对dp[i]有利? 显然取最大的嘛。所以我们有dp[i] = max(dp[i – 1] + a[i], a[i]) 其实它和dp[i] = max(dp[i – 1] , 0) + a[i]是一样的,意思是说之前能取到的最大和是正的我就要,否则我就不要!初值是什么?初值是dp[1] = a[1],因为前面没的选了。

    那么结果是什么?我们要取的最大子段和必然以某个a[i]结尾吧?那么结果就是max(dp[i])了。

    这样,我们的时间复杂度是O(n),空间复杂度也是O(n)——因为要记录dp这个数组。
    算法达到最优了吗? 好像是!还可以优化!我们注意到dp[i] = max(dp[i - 1], 0) + a[i], 看它只和dp[i – 1]有关,我们为什么要把它全记录下来呢?为了求所有dp[i]的最大值?不,最大值我们也可以求一个比较一个嘛。

    我们定义endmax表示以当前元素结尾的最大子段和,当加入a[i]时,我们有endmax’ = max(endmax, 0) + a[i], 然后再顺便记录一下最大值就好了。
     
    伪代码如下;(数组下标从1开始)
     
    1
    2
    3
    4
    5
     
     
     
    endmax answer a[1]
    for to do
        endmax max(endmax, 0a[i]
        answer max(answer, endmax)
    endfor
     
     
     
     
    时间复杂度?O(n)!空间复杂度?O(1)! 简单吧?我们不仅优化了时间复杂度和空间复杂度,还使代码变得简单明了,更不容易出错。

    老生常谈的问题来了。我们如何找到一个这样的子段?请看上面的为伪代码endmax = max(endmax, 0) + a[i], 对于endmax它对应的子段的结尾显然是a[i],我们怎么知道这个子段的开头呢? 就看它有没有被更新。也就是说如果endmax’ = endmax + a[i]则对应子段的开头就是之前的子段的开头。否则,显然endmax开头和结尾都是a[i]了,让我们来改一下伪代码:
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     
     
     
    start 1
    answerstart asnwerend 1
    endmax answer a[1]
    for end to do
        if endmax then
            endmax += a[end]
        else
            endmax a[end]
            start end
        endif
        if endmax answer then
            answer endmax
            answerstart start
            answerend end
        endif
    endfor
     
     
     
     
    这里我们直接用end作为循环变量,通过更新与否决定start是否改变。

    总结:通过不断优化,我们得到了一个时间复杂度为 O(n),空间复杂度为O(1)的简单的动态规划算法。动态规划,就这么简单!优化无止境!

    N个整数组成的序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的连续子段和的最大值。当所给的整数均为负数时和为0。

     
    例如:-2,11,-4,13,-5,-2,和最大的子段为:11,-4,13。和为20。
     
    输入

    第1行:整数序列的长度N(2 <= N <= 50000)
    第2 - N + 1行:N个整数(-10^9 <= A[i] <= 10^9)
    输出
     
    输出最大子段和。
     
    输入示例

    6
    -2
    11
    -4
    13
    -5
    -2

    输出示例

    20

    AC代码

    #include<stdio.h>
    int main()
    {
        __int64 sum,sum1,tem;
        sum=sum1=0;
        __int64 t,i;
        scanf("%I64d",&t);
        for(i=0;i<t;i++)
        {
            scanf("%I64d",&tem);
            sum1=sum1+tem;
            if(sum1<0)
            sum1=0;
            if(sum1>sum)
            sum=sum1;
    
        }
        printf("%I64d
    ",sum);
    }
    View Code

    我的代码,前面几组数据都过了,可是被卡在了第十五组数据

    #include<bits/stdc++.h>
    using namespace std;
    
    int n;
    int i,j;
    int a[50010];
    int maxn,ans;
    int start,end;
    int main()
    {
        while(scanf("%d",&n)!=-1)
        {
            start = 1; 
            for(i=0; i<n; i++)
            {
                scanf("%d",&a[i]);
            }
            maxn = ans = a[0];
            for(i=1; i<n; i++)
            {
                if(maxn >= 0)
                    maxn = max(maxn, 0) + a[i];
                else
                    maxn = a[i];
                if(maxn >= ans)
                    ans = maxn;    
            }
            printf("%d
    ", ans);
        }
        return 0;
     } 
     /*
     6
    -2
    11
    -4
    13
    -5
    -2
    */
    
    /*
    20
    */
    View Code
     
    永远渴望,大智若愚(stay hungry, stay foolish)
  • 相关阅读:
    Megcup 2017 决赛第一题 规则
    折正方体-------------给你出道题
    论公平
    Vue 将一个组件嵌入到另一个组件中
    Vue下的index.html
    Vue下的package.json
    Vue 一些用法
    chkconfig 系统服务管理
    Windows和Linux下的 telnet命令 不是内部或外部命令的解决方案
    JSONPlaceholder
  • 原文地址:https://www.cnblogs.com/h-hkai/p/7512268.html
Copyright © 2020-2023  润新知