小猫照片
- 难度 提高
- 时限 1s
- 空限 256m
- 单调队列优化 DP
数据规模:,,。
思路
先把条件化简成长度恰为 的窗口。 如果某段长度大于 的区间里一张都没选,那么它内部任意一段长度恰为 的区间里同样一张都没选;反过来,长度恰为 的区间本身就是长度不小于 的区间。所以「任意长度不小于 的连续区间至少选一张」等价于「任意长度恰为 的窗口至少选一张」。
再把窗口条件翻译成对被选位置本身的约束。 把选出的位置从小到大记为 :
- 窗口 必须含选中点,所以 ;
- 若相邻两点间隔 ,则窗口 整段落在 内且不含选中点,矛盾;而间隔不超过 时,任何长度恰为 的窗口都不会跨过这对相邻点却不碰到它们,所以 ;
- 窗口 必须含选中点,所以 。
这三个条件都只涉及相邻的两个选中点,于是可以按「选了几张、最后一张在哪」做 DP。「恰好 张」与它们并不冲突:只要间隔约束满足,在中间额外多选几个点不会破坏任何窗口,多选只会让窗口更容易被覆盖。
状态与转移。 设 表示已选 张、最后一张在位置 时的最大价值和。
初值把第一张限制在 ,正对应 ;转移只允许前一张落在 ,正对应相邻间隔不超过 。答案为 ,即最后一张落在末尾那个窗口内;若下标 处全部不可达,说明不存在合法方案,输出 。
用单调队列把转移降到均摊 。 固定 后,转移取的是一段随 右移的滑动窗口,且只需要窗口最大值。用下标递增、值单调递减的双端队列维护 :每步先尝试把 从队尾插入(弹掉所有值不大于它的元素),再把下标小于 的元素从队首弹出,队首恰好是 。每层转移于是从 降到 ,总复杂度 。
边界与实现细节。 第 层只依赖第 层,滚动保存两层即可,空间 。不可达状态用极小值标记,转移前先判断是否可达,避免把标记值当成真实和累加。几个退化情形: 时要求首张在位置 且相邻间隔不超过 ,即必须选满全部位置, 无解; 时只有整段这一个窗口, 的答案就是所有 的最大值, 只能全选; 很小时也可能无解(如 、、:首张要落在前两张内、末张要落在后两张内,一张做不到)。
复杂度
时间 ,最多约 次转移;空间 ,只需要两层 DP 数组和一个双端队列。
仓颉实现
位置统一用 起下标,取答案时对应下标 的后缀。
查看仓颉实现
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)
}
}