[R13F] 答题比赛
- 难度 提高
- 时限 1s
- 空限 512m
- 动态规划单调队列前缀最大值
数据规模:,,,。
思路
定义连续跳过 道题的扣分函数
设 表示前 道题中恰回答 道、且第 道被回答的最大总得分,初始只有 ,其余为 。设上一次回答的是第 道题,则中间连续跳过 道:
为了处理末尾跳过,虚拟出第 道、,答案即 。朴素三重循环是 ,会超时。
拆窗口 + 单调队列 + 前缀最大值
把 按 与 分两段,对固定的 、枚举 :
窗口内(,即 ):,转移值为
其中括号内只与 有关,但要求 ,是滑动窗口最大值,用 单调队列 维护:队列中 按 递减,每次弹出 的队首即可。
窗口外(,即 ):,转移值为
括号内同样只与 有关,且无下界限制(只要 ),用 前缀最大值 直接 查 。
两段取较大即 。注意单调队列要按 分开:每层 先把所有 算完,再把 推入 这层的队列(避免算 时用到同层)。
整体 ,、数组只有 4 个长度 的 Int64 数组,内存远低于上限。
仓颉实现
import std.env.*
import std.convert.*
main(): Int64 {
let reader = getStdIn()
let p1 = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let n = p1[0]
let m = p1[1]
let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let p3 = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let kk = p3[0]
let b = p3[1]
let c = p3[2]
let nn = n + 1 // 虚拟题 n+1
let size = nn + 1
let NEG = Int64.Min / 2
var prevdp = Array<Int64>(size, { _ => NEG })
var curdp = Array<Int64>(size, { _ => NEG })
prevdp[0] = 0
var j = Int64(1)
while (j <= m + 1) {
var t = 0
while (t < size) {
curdp[t] = NEG
t = t + 1
}
var iStart = j
var iEnd = n
if (j == m + 1) {
iStart = nn
iEnd = nn
}
// 前缀最大 g[x] = max_{0<=y<=x}(prevdp[y] + c*y)
var gArr = Array<Int64>(size, { _ => NEG })
var gBest = NEG
var gg = 0
while (gg < size) {
if (prevdp[gg] > NEG + 1) {
let val = prevdp[gg] + c * gg
if (val > gBest) {
gBest = val
}
}
gArr[gg] = gBest
gg = gg + 1
}
// 单调队列:x 按 prevdp[x]+b*x 递减
var dqIdx = Array<Int64>(size, { _ => 0 })
var head = 0
var tail = 0
var nextPush = 0
var i = iStart
while (i <= iEnd) {
while (nextPush <= i - 1) {
if (prevdp[nextPush] > NEG + 1) {
let newKey = prevdp[nextPush] + b * nextPush
while (tail > head) {
let backx = dqIdx[tail - 1]
if (prevdp[backx] + b * backx <= newKey) {
tail = tail - 1
} else {
break
}
}
dqIdx[tail] = nextPush
tail = tail + 1
}
nextPush = nextPush + 1
}
var ai = Int64(0)
if (i <= n) {
ai = a[i - 1]
}
var best = NEG
let lo = i - kk
// 窗口内:i-kk<=x<=i-1,扣 b*L
if (tail > head) {
while (tail > head && dqIdx[head] < lo) {
head = head + 1
}
if (tail > head) {
let qx = dqIdx[head]
let val2 = ai - b * (i - 1) + prevdp[qx] + b * qx
if (val2 > best) {
best = val2
}
}
}
// 窗口外:0<=x<=i-kk-1,扣 b*k + c*(L-k)
let gi = i - kk - 1
if (gi >= 0) {
if (gArr[gi] > NEG + 1) {
let val3 = ai - b * kk - c * (i - kk - 1) + gArr[gi]
if (val3 > best) {
best = val3
}
}
}
curdp[i] = best
i = i + 1
}
let tmp = prevdp
prevdp = curdp
curdp = tmp
j = j + 1
}
println("${prevdp[nn]}")
return 0
}
要点:
- 题眼是把扣分按「跳过长度 / 」拆成两段,每段都能把 相关项整理成 ,从而可以用单调队列或前缀最大值 查。
- 末尾跳过用虚拟题 ()兜底,答案直接是 ,不用单独处理尾部。
- 单调队列要 按 分层:每层 内先算完所有 ,再把 入队,避免转移时误用同层数据。
- 窗口内取下界 (),窗口外取 (),两者恰好不重不漏。
- 用
NEG=Int64.Min/2做负无穷,加减运算不会溢出;判断有效状态用> NEG+1。