动态规划经典问题-最大连续子序列和

题目

给出一个整数序列S,其中有N个数,定义其中一个非空连续子序列T中所有数的和为T的“序列和”。 对于S的所有非空连续子序列T,求最大的序列和。 变量条件:N为正整数,N≤1000000,结果序列和在范围(-263,263-1)以内。

输入描述:
第一行为一个正整数N,第二行为N个整数,表示序列中的数。

输出描述:
输入可能包括多组数据,对于每一组输入数据,
仅输出一个数,表示最大序列和。
示例1
输入
5
1 5 -3 2 4

6
1 -2 3 4 -10 6

4
-3 -1 -2 -5
输出
9
7
-1

分析

  例如给定一个序列A1A2…An 例如1,-2,3,4,-10,6。目标是找到一个连续的子序列Ai到Aj使得Ai+…+Aj最大。那么该例子的最大连续子序列就是3,4,它的和为7。
  如果直接来考察这个问题是非常难求解的,这是因为连续子序列的两端都是变化的。所以我们自然而然可以想到,将一端固定下来。例如我们可以考虑某一个元素Aj为末尾元素的情况下,其最大连续子序列的和。表示为…+Aj。那么…+Aj就有两种情况,一种是最大连续子序列只有一个元素Aj,例如上面例子的1,3,6。那么另外一种情况就是最大的连续子序列有多个元素即…+Aj-1+Aj。假设将以Aj为末尾元素的最大连续子序列和为F(j)。那么对于上面两种情况就可以写成F(j) = Aj或者F(j) = F(j-1) + Aj。所以得到F(j) = max{Aj , F(j-1)+Aj}。要么我们要求解该问题,只需依次的从小到大获取所有的F(j),然后从中找到最大的即为最大连续子序列和。

朴素的递归策略(未用到动态规划)

#include <iostream>
#include <algorithm>
#include <climits>
#include <cstdio>

using namespace std;

const int MAXN = 1e6 + 10;
const int INF = INT_MAX;
long long arr[MAXN];

//func(n)为以An为末尾元素的最大连续子序列和
long long func(int n){
    long long  answer;
    if(n == 0){  //以A0为末尾元素,显然最大连续子序列就是A0
        answer = arr[0];
    }
    else{
        answer = max(arr[n],func(n-1)+arr[n]);
    }
    return answer;
}

int main()
{
    int n;
    while(scanf("%d",&n)!=EOF){
        for(int i=0;i<n;++i){
            scanf("%lld",&arr[i]);
        }

        long long maxNum = -INF;
        for(int i=0;i<n;i++){
            maxNum = max(maxNum,func(i));
        }
        printf("%lld\n",maxNum);
    }
    return 0;
}

  这样用递归写虽然可以得到正确答案,但是一般都会超时,因为它的复杂度是n的平方的。而之所以会这么高,是因为在递归中产生了很多相同的子问题,而每次遇到这种子问题我们都要重新计算一次,造成时间浪费,所以复杂度高。

改进:自顶向下的备忘录法

  有了上面的分析,我们自然而然可以想到建立一个备忘录数组,将子问题的解存到备忘录里。如果下次又需要求解这个子问题的值,就可以在常数的时间内从备忘录里将这个子问题的值给取出来,而不是重新计算,这就大大的减少了时间复杂度。这就是递归策略+记忆化。其实,动态规划可以理解为加上了备忘录的递归算法。为什么是自顶向下呢?比如F(n) = max{Aj,F(n-1)+Aj},我们要求F(n),需要求出F(n-1)。要求F(n-1),需要求F(n-2)…以此类推,这不就是自顶向下吗。

#include <iostream>
#include <algorithm>
#include <climits>
#include <cstdio>

using namespace std;

const int MAXN = 1e6 + 10;
const int INF = INT_MAX;
long long arr[MAXN];

//func(n)为以An为末尾元素的最大连续子序列和
long long func(int n){
    long long  answer;
    if(n == 0){  //以A0为末尾元素,显然最大连续子序列就是A0
        answer = arr[0];
    }
    else{
        answer = max(arr[n],func(n-1)+arr[n]);
    }
    return answer;
}

long long memo[MAXN];  //备忘录

long long func1(int n){
    if(memo[n] != -1){  //表明f(n)已经在备忘录里,就可以直接取出
        return memo[n];
    }
    long long  answer;
    if(n == 0){  //以A0为末尾元素,显然最大连续子序列就是A0
        answer = arr[n];
    }
    else{
        answer = max(arr[n],func1(n-1)+arr[n]);
    }
    memo[n] = answer;  //将F(n)放入备忘录
    return answer;
}
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF){
        for(int i=0;i<n;++i){
            scanf("%lld",&arr[i]);
        }

//        long long maxNum = -INF;
//        for(int i=0;i<n;i++){
//            maxNum = max(maxNum,func(i));
//        }
//        printf("%lld\n",maxNum);

        fill(memo,memo+n,-1);   //初始化备忘录所有元素为-1
        long long maxNum = -INF;
        for(int i=0;i<n;i++){
            maxNum = max(maxNum,func1(i));
        }
        printf("%lld\n",maxNum);
    }
    return 0;
}

  这就相当于F(0)到F(n-1)每个都只计算了一次,不存在重复计算,那么时间复杂度就降低到了O(n)

自底向上的递推方法

  既然可以用递归的方法解决,那么就可以转化为递推的方法解决。

#include <iostream>
#include <algorithm>
#include <climits>
#include <cstdio>

using namespace std;

const int MAXN = 1e6 + 10;
const int INF = INT_MAX;
long long arr[MAXN];

//func(n)为以An为末尾元素的最大连续子序列和
//朴素递归
long long func(int n){
    long long  answer;
    if(n == 0){  //以A0为末尾元素,显然最大连续子序列就是A0
        answer = arr[0];
    }
    else{
        answer = max(arr[n],func(n-1)+arr[n]);
    }
    return answer;
}


//自顶向下备忘录法
long long memo[MAXN];  //备忘录

long long func1(int n){
    if(memo[n] != -1){  //表明f(n)已经在备忘录里,就可以直接取出
        return memo[n];
    }
    long long  answer;
    if(n == 0){  //以A0为末尾元素,显然最大连续子序列就是A0
        answer = arr[n];
    }
    else{
        answer = max(arr[n],func1(n-1)+arr[n]);
    }
    memo[n] = answer;  //将F(n)放入备忘录
    return answer;
}

//自底向上的递推

long long dp[MAXN];  //记录F(i)的值
void func3(int n){
    long long answer;
    for(int i=0;i<n;i++){
        if(i == 0){
            answer = arr[i];
        }
        else{
            answer = max(arr[i],dp[i-1] + arr[i]);
        }
        dp[i] = answer;
    }
    return;

}
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF){
        for(int i=0;i<n;++i){
            scanf("%lld",&arr[i]);
        }

//        long long maxNum = -INF;
//        for(int i=0;i<n;i++){
//            maxNum = max(maxNum,func(i));
//        }
//        printf("%lld\n",maxNum);

//        fill(memo,memo+n,-1);   //初始化备忘录所有元素为-1
//        long long maxNum = -INF;
//        for(int i=0;i<n;i++){
//            maxNum = max(maxNum,func1(i));
//        }
//        printf("%lld\n",maxNum);
        fill(dp,dp+n,-1);   //初始化备忘录所有元素为-1
        long long maxNum = -INF;
        func3(n);
        for(int i=0;i<n;i++){
            maxNum = max(maxNum,dp[i]);
        }
        printf("%lld\n",maxNum);
    }
    return 0;
}

  看完之后,有没有让你更加理解动态规划呢?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值