2018CCPC网络预选赛1007(hdu6444) 单调队列

127 篇文章 0 订阅
85 篇文章 2 订阅

Neko’s loop

问题分析

题意:给一个元素个数为 n n 的环,选定任意一个起点i后,每次可以往前跳 (i+k)%n ( i + k ) % n ,然后会相应得到 ai a i 的收益,问跳 m m 次后总收益可以达到s(可以提前停止),在开始之前至少需要身上需要有多少收益。

显然这样选定一个起点后不断往前跳,所获得的 ai a i 会形成一个环。

那么我们就可以把所有环找出来,通过枚举环的起点来找每个环可以获得的最大收益。问题就转化成求每个环的长度为 m m 的最大子段和了。
我们用cnt=lenm(len)代表需要跑几次循环节,用 ret=len%m r e t = l e n % m 代表剩余步数。
但是我们要注意两点。
首先,如果循环节的总收益为负,那么我们直接求一个不超过 m m 的最大子段和就可以了。因为跑再更多次只会收益变得更小。
其次,如果len>m,我想很多人可能就会直接 cnt+(len%m) c n t ∗ 循 环 节 总 收 益 + 剩 余 ( l e n % m ) 所 能 获 得 最 大 总 收 益 ,这就是本题的坑点了。因为题目告诉了我们可以提前停止,如果最后一圈有负数,我们可以跑完正数的那一部分,剩下的负数不跑了。
就比如序列为 2,5,7,20 − 2 , − 5 , − 7 , 20 ,那么我们可以跑完 cnt1 c n t − 1 圈,再跑个 20 20 就够了。所以我们真正能确定的是前 cnt1 c n t − 1 前所能获得的最大收益,剩下的我们拿出来单独跑。

但是呢,还是要注意,我们跑剩余的时候,长度为 len+(len%m) l e n + ( l e n % m ) ,然后最后一步能跑到的范围为 (len1,3len) ( l e n − 1 , 3 ∗ l e n ) ,因此我们需要开三倍的数组。

代码

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 1e5+10;
const LL inf = 1e18;
LL n,m,k,s,a[N],sum[N];
bool vis[N];
vector<LL> v[N];

LL solve(int d,int n,LL m) {
    LL ans = 0;
    memset(sum, 0, sizeof sum);
    for (int i = 1; i <= n; ++i)
        sum[i] = sum[i - 1] + v[d][i - 1];
    for (int i = n + 1; i <= 2 * n; ++i)
        sum[i] = sum[i - 1] + v[d][i - n - 1];
    for (int i = 2 * n + 1; i <= 3 * n; ++i)
        sum[i] = sum[i - 1] + v[d][i - 2 * n - 1];

    deque<int> dq;

    for (int i = 1; i <= 3 * n; ++i) {
        while (!dq.empty() && sum[dq.back()] > sum[i - 1])
            dq.pop_back();
        while (!dq.empty() && dq.front() + m < i) //控制子段和长度为m
            dq.pop_front();
        dq.emplace_back(i - 1);
        ans = max(ans, sum[i] - sum[dq.front()]);
    }
    return ans;
}

int main() {

    int T;
    scanf("%d", &T);
    for (int t = 0; t < T; ++t) {
        memset(vis, 0, sizeof vis);
        LL ans = -inf;
        int tot = 0;
        for (int i = 0; i < N; ++i)
            v[i].clear();
        scanf("%lld%lld%lld%lld", &n, &s, &m, &k);
        for (int i = 0; i < n; i++) scanf("%lld", &a[i]);
        //处理循环节
        for (int i = 0; i < n; i++) {
            if (!vis[i]) {
                for (int j = i; !vis[j]; j = (j + k) % n) {
                    vis[j] = 1;
                    v[tot].push_back(a[j]);
                }
                tot++;
            }
        }
        for (int i = 0; i < tot; ++i) {
            LL res = 0, tmp = 0;
            int len = v[i].size();
            for (int j = 0; j < len; ++j)
                tmp += v[i][j];
            res = solve(i, len, m);
            ans = max(ans, res);
            //总收益小于0的话就跑一段长度不超过m的最大子段和就可以了
            if (tmp < 0)
                continue;
            LL ret = m % len;
            LL cnt = m / len;
            tmp = max(cnt - 1, 0LL) * tmp; // cnt-1圈
            if (cnt >= 1) ret += len;
            res = max(res, solve(i, len, ret) + tmp); //剩下的特判(单独算)
            ans = max(ans, res);
        }
        ans = max(s - ans, 0LL);
        printf("Case #%d: %lld\n", t + 1, ans);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值