[SMOJ1762&2136]放假/假期

97 篇文章 0 订阅
9 篇文章 0 订阅

我们先把问题一般化,减少限制,看看有没有什么思路。

如果不对数列长度进行任何限制,只要求选取一段连续的数,使总和最大,那么可以用动态规划解决。

fi 表示在前 i 个数中以 Ai 结尾的数列的最大总和,决策的时候有两种选择:要么延续前面以 Ai1 结尾的数列,要么自己重新开始一个数列,即

fi=max{fi1+Ai,Ai}

其实也就是

fi=max{fi1,0}+Ai

最后再取 max{fi} 就是答案了,时间复杂度为 O(n)

如果我们现在只把 Pk 这个限制去掉,而要求 kQ ,那么也并不难,用单调队列可以轻松解决。

先预处理一个前缀和数组 sum[] ,令

sumi=j=1iAj

现在参考上面的 DP 思路。不妨依次考虑每一个 i ,让它作为所选数列的结尾。

设数列开头为 j(显然必须满足 ij<Q ),则整个数列的和为 sumisumj1 。其中 sumi 是一定的,要使和最大,必须 sumj1 尽可能小

可以发现,这其实就是单调队列的经典模型,维护一个有长度限制的区间,求区间内的极值。可以在 O(n) 的时间内解决问题。

但本题不仅限制了区间的最大长度,同时还限制了最小长度,看似有点复杂,其实可以通过巧妙的方法解决这个限制。

也就是说,顺其道而行之,强制单调队列的元素队尾元素位置和 i 相差 P,使得所有元素都和 i 相差大于等于 P,这样一来就不存在区间长度的问题了。

因为这个思想比较巧妙,可以对照下面的参考代码,结合上图进行分析。

#include <algorithm>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <deque>

using namespace std;

const int MAXN = 5e5 + 100;

int N, P, Q;
long long W[MAXN], sum[MAXN];

deque <int> dq;

int main(void) {
    freopen("1762.in", "r", stdin);
    freopen("1762.out", "w", stdout);
    scanf("%d%d%d", &N, &P, &Q);
    for (int i = 1; i <= N; i++) {
        scanf("%lld", &W[i]);
        sum[i] = sum[i - 1] + W[i];
    }

    long long ans = LONG_LONG_MIN;
    dq.push_back(0); //设队首元素为 x,则区间为 (x, i]
    for (int i = P; i <= N; i++) { //第一个区间起码要是 (0, P],i 要从 P 开始考虑
        while (!dq.empty() && dq.front() + Q < i) dq.pop_front(); //维护上界 Q ,判断队头是否合法
        while (!dq.empty() && sum[dq.back()] >= sum[i - P]) dq.pop_back(); //i-P 插入队中对应位置,维护下界 P
        dq.push_back(i - P);
        ans = max(ans, sum[i] - sum[dq.front()]); //取最优的作为答案,注意维护的是左开右闭区间,队首的值已经相当于 j-1,不要再减 1
    }

    printf("%lld\n", ans);
    return 0;
}


其实写成两边都是闭的也可以,但是要小心 +1、-1 的地方,大小于是否要严格,以及算答案和维护队列的操作顺序,都会受到影响。

总而言之,整个程序内的含义统一就好了,个人认为不存在孰优孰劣的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值