【题目描述】

在一年前赢得了小镇的最佳草坪比赛后,FJ变得很懒,再也没有修剪过草坪。现在,新一轮的最佳草坪比赛又开始了,FJ希望能够再次夺冠。

然而,FJ的草坪非常脏乱,因此,FJ只能够让他的奶牛来完成这项工作。

FJ有N(1 <= N <= 100,000)只排成一排的奶牛。 每只奶牛的效率是不同的,奶牛i的效率为Ei (0 <= Ei <= 1,000,000,000)。

靠近的奶牛们很熟悉,因此,如果FJ安排超过K只连续的奶牛,那么,这些奶牛就会罢工去开派对)。

因此,现在FJ需要你的帮助,计算FJ可以得到的最大效率,并且该方案中没有连续的超过K只奶牛。

【题目链接】

CodeVS 4654 修剪草坪

【解题思路】

我们首先来考虑一个$O(n^2)$的线性DP。

设$f[i]$表示在$[0,i]$中选择若干只符合要求的奶牛可以得到的最大效率。

当$i \in [0, k)$,显然选择所有奶牛最优。

$$ f[i] = sum(0, i) $$

而当$i \geq k$时,考虑当前的奶牛,有两种决策。 一种是直接放弃当前奶牛$i$。 另一种是选择当前奶牛$i$,而为保证不超过有连续$k$只奶牛,所以要放弃之前的某只奶牛。 这个要放弃的奶牛$j$这个需要$O(n)$枚举。

式子如下: $$ f[i] = max: \\ max\{\ f[j - 1] + sum(j + 1, i)\ \}\ j \in [i - k, i]), \\ f[i - 1] $$

其中$sum$表示[l, r]的能量和,即: $$ sum(l, r) = \sum _{i = l} ^ r a_i $$ 显然可以借助前缀和在$O(1)$时间内得到。

核心代码如下:

for(int i = 0; i < k; i++) f[i] = sum(0, i);
for(int i = k; i < n; i++){
    int64 max = INT_MIN;
    for(int j = i - k; j < i; j++){
        max = std::max(max, (j ? f[j - 1] : 0) + sum(j + 1, i));
    }
    f[i] = std::max(f[i - 1], max);
}

【单调队列优化】

再次审视我们的转移方程,显然时间瓶颈在于求这样的一项: $$ \\ max\{\ f[j - 1] + sum(j + 1, i)\ \}\ k \in [i - k, i) $$ 求的是区间最值,而且区间左端点是单调递增的——区间在平移。

也许可以用单调队列来优化?

遗憾的是,这个方程并不能直接用单调队列来优化。

这是因为我们要求最值的区间中每一项的值都是和$i$有关的——显然这是无法接受的。

那么考虑作一些变形,我们将$sum$求和一项替换成前缀和的形式。

$$ \begin{align*} sum(j + 1, i) &= preSum(i) - preSum(j + 1 - 1) \\ &= preSum(i) - preSum(j) \end{align*} $$

那么我们要求的项就变成了:

$$ max\{\ f[j - 1] + preSum(i) - preSum(j)\ \}\ k \in [i - k, i) $$

注意到$preSum(i)$是固定的,那么我们将其提出来,就成了这样子:

$$ max\{\ f[j - 1] - preSum(j)\ \}\ k \in [i - k, i) + preSum(i) $$

这就形成了我们标准的单调队列优化DP的形式,于是我们可以愉快的使用单调队列将该DP优化到$O(n)$啦。

变形后总的转移方程为: $$ f[i] = max: \\ max\{\ f[j - 1] - preSum(j)\ \}\ k \in [i - k, i) + preSum(i) \\ f[i - 1] $$

【AC代码】

记得开long long。 注意边界是个好习惯。

#include <cstdio>
#include <algorithm>
#include <climits>
#include <deque>

#define MAXN 100000

#define int64 long long

template <typename Item> struct MonotonicQueue{
    std::deque<Item> data, aux;

    void push(const Item &x){
        data.push_back(x);
        while(!aux.empty() && aux.back() < x) aux.pop_back();
        aux.push_back(x);
    }

    void pop(){
        Item x = data.front();
        data.pop_front();
        if(x == aux.front()) aux.pop_front();
    }

    size_t size(){
        return data.size();
    }

    Item max(){
        return aux.front();
    }
};

int n, k;
int a[MAXN];
int64 sum[MAXN];

int64 f[MAXN];

int main(){
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++) scanf("%d", a + i);

    sum[0] = a[0];
    for(int i = 0; i < n; i++) sum[i] = sum[i - 1] + a[i];

    MonotonicQueue <int64> Q;

    f[0] = a[0], Q.push(0 - sum[0]);
    for(int i = 1; i < k; i++){
        f[i] = sum[i];
        Q.push(f[i - 1] - sum[i]);
    }

    for(int i = k; i < n; i++){
        if(Q.size() == k + 1) Q.pop();

        f[i] = std::max(Q.max() + sum[i], f[i - 1]);

        Q.push(f[i - 1] - sum[i]);
    }

    printf("%lld\n", f[n - 1]);

    return 0;
}

就是这样啦。