最大子序列和 [详细注释+解释更新情况+滚动更新]

思考:

分析:

  1. 最大子序列和:连续子序列的元素和最大的段,既然是连续的,即为一个区间。意味着我们要去枚举所有的区间。如何做到不重不漏地枚举呢?并且还要从所有区间里选出元素和最大的那个区间。那么必须要有一个区间划分的依据,这里采用以右端点划分的方式:
  2. 状态表示:
    f[i]:表示以第i个元素为区间右端点的集合。i的取值从1~n,即是一个集合。
    比如:i=k:表示以第k个元素为区间右端点的最大区间和,而以k为区间右端点,那么它的左端点又有许多种情况,一共1~k-1种情况,所以说我们要求出以k为区间右端点的集合里的区间和的最大值。然后不同的集合的区间最大值进行比较,再取一个最大值。
  3. 状态转移:
    对于本题的状态转移,找最后一个不同点,假设当前以第k个元素为区间右端点,思考下它的不同点,有人会说是区间的左端点不同,所以使得状态转移的划分应该以区间的左端点划分,但是这样的话:即固定每个区间的右端点,每个右端点所对应的左端点都是一个集合,该集合里包含了不同的左端点,造成了不同的区间,从以k为区间的右端点里找出所有由左端点造成的不同的区间里的和最大的的区间,然后求出其余的k (k的取值为:1~n);所以觉得应该划分为1 ~ n,实际上这是错误的!
    正确的划分方式是,虽然区间的右端点固定后,是相同的,不同的在于它的左端点,但是它的左端点的不同会造成区间长度的不同,所以这才是能往后推导的不同的,通过固定右端点,然后枚举区间长度,从而做到不重不漏地枚举。另外一个划分依据就是:当区间元素和为负数的时候,与其将区间右端点第k个元素加入区间,还不如将区间右端点第k个元素划分到长度为1的区间,即它本身自己一个区间里面去,这样的话效果更好,这就是体现了贪心的思想。
    所以对于最后状态的转移我们可以划分为如下两种:一个是区间长度为1,一个是区间长度 > 1;
    f[i] = max (w[i], f[i-1] + w[i]) 看哪个更大。
    可转化为:f[i] = max(0, f[i-1]) + w[i];
    这就是我们的状态转移方程。

既然集合已经划分出来了:那么接下来就是推敲代码部分:

推敲代码:考虑各种不同的的情况。
  1. 由上面已知,f[i]表示以第i个元素为区间右端点的最大子序列;且转移公式为:
f[i] = w[i] + max(f[i-1], 0);
  1. 考虑更新的情况:当前区间和f[i] > res(之前记录的最大值),则此时不仅需要更新最大值,还需要更新区间的左右端点,右端点就是right = i;而左端点left = 区间的起点;而对于区间的起点又是一种情况更新:当区间和小于0为负数的时候,我们还不如不要前面的区间和,直接从第i个元素开始,所以此时记录新的左端点为:new_left = i;如果下次从新的左端点开始的时候,更新的区间和大于之前的res时,则更新res的同时,左端点就是 left = new_left,如果以新的起点为左端点的区间和 没有之前的 res大,那么就没必要更新左端点啊
if (f[i-1] < 0) new_left = i;
if (f[i] > res) 
{
	res = f[i];
	left = new_left;	//新晋逼王的左端点
	right = i;
}

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1e4 + 10, INF = 0x3f3f3f3f;
int f[N], a[N];
int n;

int main()
{
    cin >> n;
    for (int i=1; i <= n; i ++)
        cin >> a[i];
    //new_left的作用:每当我们决定从一个新的连续的区间开始的时候,则用new_left记录这个新的区间的左端点
    //而新的区间诞生的原因是因为前面的元素区间和<0为负数,及时止损,新开一个区间。
    int res = -INF, left=1, right=1, new_left=1;
    
    for (int i=1; i <= n; i ++)
    {
        f[i] = a[i] + max(0, f[i-1]);
    //前i-1个元素的区间和小于0,此时新开一个区间,新区间的起点是new_left;
    //所以说new_left是新区间的标志;
        if (f[i-1] < 0) new_left = i;
    //如果新区间的区间和大于了之前的区间和的最大值,那么更新最值,还有区间端点。
        if (f[i] > res) res = f[i], right=i, left = new_left;
    }
    if (res < 0 ) res = 0, left = 1, right = n;
    cout << res << ' ' << a[left] << ' ' << a[right];
    return 0;
}

滚动更新:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 1e4 + 10;
int a[N];
int n;

int main()
{
    cin >> n;
    for (int i=0; i < n; i ++) cin >> a[i];
    int sum=0, res=-0x3f3f3f3f, left=0, right=0, new_left=0;
    for (int i=0; i < n; i ++)
    {
        sum += a[i];
        if (sum > res)
        {
            res = sum;
            right = i;
            left = new_left;
        }
        if (sum < 0)
        {
            sum=0;
            new_left = i+1;
        }
    }
    
    if (res < 0) res = 0, left=0, right=n-1;
    cout << res << ' ' << a[left] <<  ' ' << a[right];
    
    return 0;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值