[USACO11OPEN] Mowing the Lawn G

GitHub跳转原题关系图返回列表

枚举最后一个不选的断点,把 DP 转移化为窗口最大值并用单调队列维护。

OJ: luogu

题目 ID: P2627

难度:普及+/提高

标签:动态规划单调队列前缀和

日期: 2026-01-05 10:39

题意

n 只奶牛排成一排,第 i 只奶牛效率为 e[i]。可以选择一些奶牛获得效率和,但不能选择超过 k 只连续的奶牛。求最大效率和。

思路

一个直接 DP 是:令 dp[i] 表示考虑前 i 只奶牛的最大效率,然后枚举末尾连续选了多少只。

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:枚举最后一个不选的位置 j,复杂度 O(nk),只适合小数据。

const int MAXN = 305;

int n, k;
long long e[MAXN];
long long prefix_sum[MAXN];
long long dp[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> e[i];
        prefix_sum[i] = prefix_sum[i - 1] + e[i];
    }

    for (int i = 1; i <= n; i++) {
        long long best = 0;
        for (int j = max(0, i - k); j <= i; j++) {
            long long left_best = 0;
            if (j > 0) {
                left_best = dp[j - 1];
            }
            long long value = left_best + prefix_sum[i] - prefix_sum[j];
            if (j == max(0, i - k) || value > best) {
                best = value;
            }
        }
        dp[i] = best;
    }

    cout << dp[n] << '\n';

    return 0;
}

把“末尾连续选了多少只”换成“最后一个不选的位置 j”更好写。若最后一个不选的位置为 j,那么 j+1..i 全部选中,长度要求 i-j <= k,贡献为:

text
dp[j-1] + S[i] - S[j]

其中 S 是前缀和。于是:

text
dp[i] = S[i] + max(dp[j-1] - S[j])    (i-k <= j <= i)

对固定的 iS[i] 是常数,只需要找窗口 [i-k, i]dp[j-1]-S[j] 的最大值。这个窗口随 i 单调右移,因此用单调队列维护断点 j 即可。

注意 j=0 表示前面没有断点,可以从第 1 只开始连续选择。这个初始断点必须先放入队列。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

int n, k;
long long e[MAXN];
long long prefix_sum[MAXN];
long long dp[MAXN]; // dp[i] 表示考虑前 i 只奶牛,合法选择的最大效率。
int que[MAXN];      // 单调队列存断点 j,按 dp[j-1]-prefix_sum[j] 从大到小维护。

long long value_of_break(int j) {
    long long left_best = 0;
    if (j > 0) {
        left_best = dp[j - 1];
    }
    return left_best - prefix_sum[j];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> e[i];
        prefix_sum[i] = prefix_sum[i - 1] + e[i];
    }

    int head = 1, tail = 1;
    que[1] = 0; // j=0 表示前面没有断点,可以从第 1 只开始连续选择。
    for (int i = 1; i <= n; i++) {
        int j = i;
        while (head <= tail && value_of_break(que[tail]) <= value_of_break(j)) {
            tail--;
        }
        tail++;
        que[tail] = j;

        // 断点 j 必须满足 i-k <= j <= i,保证 j+1..i 连续选择不超过 k 只。
        while (head <= tail && que[head] < i - k) {
            head++;
        }

        dp[i] = prefix_sum[i] + value_of_break(que[head]);
    }

    cout << dp[n] << '\n';

    return 0;
}

复杂度

时间复杂度 O(n)O(n),每个断点最多入队和出队一次。

空间复杂度 O(n)O(n)

总结

本题的重点是把“连续不超过 k 个”转成“枚举最后一个不选的位置”。整理出 S[i] + max(dp[j-1]-S[j]) 后,单调队列优化就很自然。