hdoj 1003 Max Sum---动态规划,最大子序列求和

初来乍到,动态规划不会呀,刚开始用暴力法,超时了!超时代码如下:

思路:大致是选中一个数当做结尾,然后加和。例如在6 -1 5 8 -7选中5作为结尾大致有如下序列

6  -1   5

    -1   5

          5

就这样遍历所以的结尾,结果超时。代码如下:

#include<iostream>
#include<algorithm>
using namespace std;

#define VALUE 101
#define INF 999999999

int D[VALUE];
int n;
int t;
int sum;
int maxSum[VALUE][VALUE]={0};
int  x,y;
int T=1;
int main() {
    int i, j, k;
    cin >> n;
    while (n--) {
        cin >> t;
        for (i = 1; i <= t; i++) {
            cin >> D[i];
        }

        for (i = 1; i <= t; i++) { //选那个结束
            for (j = 1; j <= i; ++j) {  //有多少行
                sum = 0;
                for (k = j; k <= i; k++) {  //每一个行有多少个相加
                    sum = sum + D[k];
                }
                maxSum[j][i] = sum;
            }
        }

        int max = -INF;
        for (i = 1; i <= t; i++) {
            for (j = 1; j <= i; j++) {
                if (max < maxSum[j][i]) {
                    max = maxSum[j][i];
                    x = j;
                    y = i;
                }
            }
        }
        cout << "Case " << T << ":" << endl;
        T++;
        cout << max << " " << x << " " << y << endl;
        if (n != 0) {
            cout << endl;

        }
    }
}

      在网上搜了代码。大多是只说是动态规划经典问题、求最大子序列和,终于算是有了眉目想通了。

对于整个序列a[n]来说,它的所有子序列有很多很多,但是可以将它们归类。
注意,是以**结尾的子序列,其中肯定是要包含**的了
 
以a[0]结尾的子序列只有a[0]
以a[1]结尾的子序列有    a[0]a[1]和a[1]
以a[2]结尾的子序列有    a[0]a[1]a[2]   /  a[1]a[2]  / a[2]
……
以a[i]结尾的子序列有a[0]a[1]……a[i-2]a[i-1]a[i]   / a[1]a[2]……a[i-2]a[i-1]a[i]  /  a[2]a[3]……a[i-2]a[i-1]a[I]  / …… /  a[i-1]a[i] / a[i]
 
所有以a[0] ~a[n]结尾的子序列分组构成了整个序列的所有子序列。
 
这样,我们只需求以a[0]~a[n]结尾的这些分组的子序列中的每一分组的最大子序列和。然后从n个分组最大子序列和中选出整个序列的最大子序列和。
 
观察可以发现,0,1,2,……,n结尾的分组中,
maxsum a[0] = a[0]
maxsum a[1] = max( a[0] + a[1] ,a[1])  = max( maxsum a[0] + a[1] ,a[1]) 
maxsum a[2] = max( max ( a[0] + a[1] + a[2],a[1] + a[2] ),a[2])  
= max(  max( a[0] + a[1] ,a[1]) + a[2] , a[2]) 
= max(  maxsum a[1] + a[2] , a[2])
……
依此类推,可以得出通用的式子。
maxsum a[i] = max( maxsum a[i-1] + a[i],a[i])
 

最大子段和:给定一个序列(元素可正可负),找出其子序列中元素和最大的值。

我们用dp(i)表示序列中以元素ans(i)结尾的序列的最大子段和,那么有:dp(i)=max(dp(i-1)+ans(i),ans(i));

首先来看一道比较基础的最大子段和问题--HDOJ 1003   Max Sum:

#define INF 999999999
int solve(int ans[],int n)  
{  
    dp[0]=0;  
    int nmax=-INF;  
    for(int i=1;i<=n;i++)  
    {  
        dp[i]=max(dp[i-1]+ans[i],ans[i]);  
        nmax=max(nmax,dp[i]);  
    }  
    return nmax;  
}  

题目大意:给定一个序列,让求出此序列的最大子段和,并输出该子序列在原序列中的位置。

这里我们用变量sum来代替dp()数组,如果sum+a(i)<a(i),就更新sum的值,并重新纪录head的值,如果sum+a(i)>=a(i),则只需更新sum的值;

代码如下:

#include <iostream>
#include <algorithm>
#include <stdio.h>
using namespace std;

#define  MAX 100005
int n;
int t;
int a[MAX]={0};
int main() {
    int i, j, k;
    cin >> t;
    int head, tail;
    int sum, max;
    int x;
    int T = 0;

    while (t--) {
        T++;
        cin >> n;
        for (i = 1; i <= n; i++) {
            cin >> a[i];
        }
        head = tail = x = 1;
        sum = max = a[1];
        for (i = 2; i <= n; i++) {
            if (sum + a[i] < a[i]) {   //重新定义开始的头
                x = i;
                sum = a[I];  //会导致sum大于max
            } else {
                sum = sum + a[i];
            }
            if (sum > max) {  //一般sum和max相等,sum大于max,说明加到一个正数。或者上一次循环中sum等于max,更新head和tail。
                max = sum;
                tail = i;
                head = x;
            }
        }
        cout << "Case " << T << ":" << endl << max << " " << head << " " << tail << endl;
        if (t) {
            cout << endl;
        }

    }
}

其他人代码:
    #include <iostream>  
    using namespace std;  
    int main()  
    {  
        int j,i,k,n,m,t;  
        int a[100002];  
        scanf("%d",&t);  
        for (j=1;j<=t;j++)  
        {  
            scanf("%d",&n);  
            for (i=0;i<n;i++)  
            {  
                scanf("%d",&a[i]);  
            }  
            int sum=0,maxsum=-1001,first =0, last = 0, temp = 1;  
            for (i=0;i<n;i++)  
            {  
                sum += a[i];  
                if (sum > maxsum)  
                {  
                    maxsum = sum;first = temp;last = i+1;  
                }  
                if (sum < 0)  
                {  
                    sum = 0;temp = i+2;  
                }  
            }  
      
            printf("Case %d:\n%d %d %d\n",j,maxsum,first,last);  
            if (j!=t)  
            {  
                printf("\n");  
            }  
        }  
          
        return 0;  
    } 
对于整个序列a[n]来说,它的所有子序列有很多很多,但是可以将它们归类。
注意,是以**结尾的子序列,其中肯定是要包含**的了
 
以a[0]结尾的子序列只有a[0]
以a[1]结尾的子序列有 a[0]a[1]和a[1]
以a[2]结尾的子序列有 a[0]a[1]a[2] / a[1]a[2] / a[2]
……
以a[i]结尾的子序列有a[0]a[1]……a[i-2]a[i-1]a[i]  / a[1]a[2]……a[i-2]a[i-1]a[i] /  a[2]a[3]……a[i-2]a[i-1]a[i] / …… /  a[i-1]a[i] / a[i]
 
所有以a[0] ~a[n]结尾的子序列分组构成了整个序列的所有子序列。
 
这样,我们只需求以a[0]~a[n]结尾的这些分组的子序列中的每一分组的最大子序列和。然后从n个分组最大子序列和中选出整个序列的最大子序列和。
 
观察可以发现,0,1,2,……,n结尾的分组中,
maxsum a[0] = a[0]
maxsum a[1] = max( a[0] + a[1] ,a[1])  =  max( maxsum a[0] + a[1] ,a[1]) 
maxsum a[2] = max( max ( a[0] + a[1] + a[2],a[1] + a[2] ),a[2])  
= max(  max( a[0] + a[1] ,a[1]) + a[2] , a[2]) 
= max(  maxsum a[1] + a[2] , a[2])
……
依此类推,可以得出通用的式子。
maxsum a[i] = max( maxsum a[i-1] + a[i],a[i])
 
 
用递归……当然,不递归也应该是可以解决的。
 
我们从maxsum  a[0]开始算起。
以后的每个就是  maxsum a[i-1] + a[i] 和 a[i] 中取大的那个
 
程序中判断 前一个的最大子序列和小于零时,将其置为0,然后再加a[i] ,这样不就是和a[i] 一样大的么;前一个的最大子序列和只要大于零,那么再加上a[i] 肯定比 a[i] 要大,这样,带有归零的这个 maxsum a[i-1] + a[i] 就是以表示当前位置结束的子序列的最大和了。
 
 
剩下的就是要判断起始和终点位置了。
 
在循环的过程中,每循环一次就算出一个以当前位置结束的最大子序列和。每次循环中最大的那个保存下来,不就是最终所有最大子序列和中的最大值了么。
 
其中temp保存的是前一个位置的最大子序列和的开始位置(题目中是从1开始的哦);当 sum > maxsum 时( 程序中的条件,与说明时的maxsum不太一样哦 )就记录最大值,并保持它的开始位置为temp,终止位置即为当前位置(i +1是因为题目中第一个为1,而不是0);
 
当最大子序列和小于0时,将 temp = i + 2; 其中 i + 1 表示当前位置(理由如上),i + 2就表示当前位置的下一个位置了。既此最大子序列和为负值,那么下一个的最大子序列和应该是它本身,而不再累加前边的。
 
程序中就两个if 语句,想要说明白还真不容易。
 
还有,有人会问,当整个序列全是负数时,还对吗?负数也是成立的,如果全是负数的时候,它就是每次都只取当前值作为最大值了,因为负的跟负的不就越加越小了吗。
 
因为题目中给出的范围是-1000 ~1000,所以 这里初始的maxsum 初始化为-1001 ,只有比所有可能的值都小时才行。maxsum初始化为0;那么当序列全是负数时,得出的最大值将是0……这就wrong了
 
 总之,只要上一个子序列最大和为正,那么无论当前值的正负,都会与当前的相加,这样以当前值结尾的子序列最大和就会增大。(一个正数 加一个 正数2 或者负数 那么都会比这个正数2 或负数原来要增大,同理,一个负数加任何一个数,都会使这个数减小,因此当前一子序列最大和小于零时,我们就归零它了,相当于是不加任何数,而保留当前位置值本身)
 

内存优化版:
    理解了以上思想后,观察上一个代码我们发现,那个a[10000]基本上就没用啊,保存了一些输入数据,可是那些数据只用了一次就没用了。输入数据的for循环和处理数据的for循环是一模一样的,而且处理数据也只是用到当前输入的数据。
于是,数组也可以省去了,直接将两个循环合并。输入一个数据,直接累加……省下不少空间哦。
复制代码
 1     #include <iostream>  
 2     using namespace std;  
 3     int main()  
 4     {  
 5         int j,i,k,n,m,t;  
 6         int a;  //不需要数组,只需要一个输入变量
 7         scanf("%d",&t);  
 8         for (j=1;j<=t;j++)  
 9         {  
10             scanf("%d",&n);  
11             int sum=0,maxsum=-1001,first =0, last = 0, temp = 1;  
12             for (i=0;i<n;i++)  
13             {  
14                 scanf("%d",&a); 
15                 sum += a;  
16                 if (sum > maxsum)  
17                 {  
18                     maxsum = sum;first = temp;last = i+1;  
19                 }  
20                 if (sum < 0)  
21                 {  
22                     sum = 0;temp = i+2;  
23                 }  
24             }  
25       //注意格式,我就因为将冒号写到了数的前边而wrong answer,郁闷半天才发现……
26             printf("Case %d:\n%d %d %d\n",j,maxsum,first,last);  
27             if (j!=t)  
28             {  
29                 printf("\n");  
30             }  
31         }  
32           
33         return 0;  
34     }  
复制代码

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值