小猫照片


数据规模:1≤n≤50001 \le n \le 5000,1≤k,x≤n1 \le k, x \le n,1≤ai≤1091 \le a_i \le 10^9。

思路

先把条件化简成长度恰为 kk 的窗口。 如果某段长度大于 kk 的区间里一张都没选,那么它内部任意一段长度恰为 kk 的区间里同样一张都没选;反过来,长度恰为 kk 的区间本身就是长度不小于 kk 的区间。所以「任意长度不小于 kk 的连续区间至少选一张」等价于「任意长度恰为 kk 的窗口至少选一张」。

再把窗口条件翻译成对被选位置本身的约束。 把选出的位置从小到大记为 s1<s2<⋯<sxs_1 < s_2 < \dots < s_x:

  • 窗口 [1,k][1,k] 必须含选中点,所以 s1≤ks_1 \le k;
  • 若相邻两点间隔 sj+1−sj>ks_{j+1}-s_j > k,则窗口 [sj+1, sj+k][s_j+1,\ s_j+k] 整段落在 [1,n][1,n] 内且不含选中点,矛盾;而间隔不超过 kk 时,任何长度恰为 kk 的窗口都不会跨过这对相邻点却不碰到它们,所以 sj+1−sj≤ks_{j+1}-s_j \le k;
  • 窗口 [n−k+1, n][n-k+1,\ n] 必须含选中点,所以 sx≥n−k+1s_x \ge n-k+1。

这三个条件都只涉及相邻的两个选中点,于是可以按「选了几张、最后一张在哪」做 DP。「恰好 xx 张」与它们并不冲突:只要间隔约束满足,在中间额外多选几个点不会破坏任何窗口,多选只会让窗口更容易被覆盖。

状态与转移。 设 dpj,idp_{j,i} 表示已选 jj 张、最后一张在位置 ii 时的最大价值和。

dp1,i={ai,i≤k−∞,i>kdp_{1,i}=\begin{cases}a_i, & i \le k\\ -\infty, & i > k\end{cases}

dpj,i=ai+max⁡i−k≤p≤i−1dpj−1,pdp_{j,i}=a_i+\max_{i-k \le p \le i-1} dp_{j-1,p}

初值把第一张限制在 i≤ki \le k,正对应 s1≤ks_1 \le k;转移只允许前一张落在 [i−k, i−1][i-k,\ i-1],正对应相邻间隔不超过 kk。答案为 max⁡i≥n−k+1dpx,i\max_{i \ge n-k+1} dp_{x,i},即最后一张落在末尾那个窗口内;若下标 ≥n−k+1\ge n-k+1 处全部不可达,说明不存在合法方案,输出 −1-1。

用单调队列把转移降到均摊 O(1)O(1)。 固定 jj 后,转移取的是一段随 ii 右移的滑动窗口,且只需要窗口最大值。用下标递增、值单调递减的双端队列维护 dpj−1,⋅dp_{j-1,\cdot}:每步先尝试把 i−1i-1 从队尾插入(弹掉所有值不大于它的元素),再把下标小于 i−ki-k 的元素从队首弹出,队首恰好是 max⁡p∈[i−k, i−1]dpj−1,p\max_{p \in [i-k,\ i-1]} dp_{j-1,p}。每层转移于是从 O(nk)O(nk) 降到 O(n)O(n),总复杂度 O(nx)O(nx)。

边界与实现细节。 第 jj 层只依赖第 j−1j-1 层,滚动保存两层即可,空间 O(n)O(n)。不可达状态用极小值标记,转移前先判断是否可达,避免把标记值当成真实和累加。几个退化情形:k=1k=1 时要求首张在位置 11 且相邻间隔不超过 11,即必须选满全部位置,x<nx<n 无解;k=nk=n 时只有整段这一个窗口,x=1x=1 的答案就是所有 aia_i 的最大值,x=nx=n 只能全选;xx 很小时也可能无解(如 n=5n=5、k=2k=2、x=1x=1:首张要落在前两张内、末张要落在后两张内,一张做不到)。

复杂度

时间 O(nx)O(nx),最多约 2.5×1072.5 \times 10^7 次转移;空间 O(n)O(n),只需要两层 DP 数组和一个双端队列。

仓颉实现

位置统一用 00 起下标,取答案时对应下标 ≥n−k\ge n-k 的后缀。

查看仓颉实现
import std.env.*
import std.convert.*

main() {
    let first = getStdIn().readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
    let n = first[0]
    let k = first[1]
    let x = first[2]
    let a = getStdIn().readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
    let NEG: Int64 = -1152921504606846976
    var prev = Array<Int64>(n, { _ => NEG })
    for (i in 0..n) {
        if (i + 1 <= k) {
            prev[i] = a[i]
        }
    }
    let dq = Array<Int64>(n + 1, { _ => 0 })
    var j: Int64 = 2
    while (j <= x) {
        let cur = Array<Int64>(n, { _ => NEG })
        var h: Int64 = 0
        var t: Int64 = 0
        for (c in 0..n) {
            if (c >= 1) {
                let idx = c - 1
                while (t > h && prev[dq[t - 1]] <= prev[idx]) {
                    t -= 1
                }
                dq[t] = idx
                t += 1
            }
            while (t > h && dq[h] < c - k) {
                h += 1
            }
            if (t > h && prev[dq[h]] > NEG / 2) {
                cur[c] = prev[dq[h]] + a[c]
            }
        }
        prev = cur
        j += 1
    }
    var ans = NEG
    for (c in (n - k)..n) {
        if (prev[c] > ans) {
            ans = prev[c]
        }
    }
    if (ans < 0) {
        println(-1)
    } else {
        println(ans)
    }
}