数组行走
- 难度 提高
- 时限 1s
- 空限 256m
- 线性 DP
数据规模:,,,,,所有测试用例的 之和不超过 。
思路
每次向左都必然配一次向右。 因为不能连续两步向左,所以除了整条路径的最后一步,每次向左之后紧接着的一步一定是向右,而且回到原来的位置。这样的一次「左—右」往返净位移为 ,只是把位置 与 又各访问一次,多拿 分。于是路径只有两种形态:
- 形态一: 次向左全部是往返,最后一步向右;
- 形态二: 次往返,最后一步独自向左()。
两种形态都只由「净前进了多远」和「往返了几次」决定。
形态一。 向右共 步,其中 步用于往返,所以净前进 ,最远只到位置 (往返不产生新的最远点)。往返只能发生在满足 的相邻对上:位置一旦超过 就无法再返回终点。每做一次往返,得分就多一份 ;各次往返互不影响,一路向右时在 处顺路做完即可,因此每一轮都取前缀里和最大的相邻对最优。记
( 为长度, 时不存在这样的对),则形态一的得分是 ,其中 为前缀和,,下标从 起。
形态二。 最后一步向左时,向右的 步里有 步用于往返返回,净前进 ,最后一步从 向左走到 结束。此前的 次往返仍受 限制,于是得分是
形态二不能省略。例如 、、:形态一最多只能到 ,最好得分是 ;而走位 在最高的一对上往返一次、再单独向左收尾,得分为 ,比它大得多。若只枚举形态一就会在这类数据上出错。
枚举与边界。 对 分别计算两种形态并取最大值:
- 形态一要求 或 —— 时必须存在一个可往返的相邻对;
- 形态二要求 且 (保证收尾的向左不出界)。
由于 保证了 ,只需再判上述条件。 时只剩「一路向右」,答案就是 ; 且 时两种形态里合法的只有 ,与直觉一致。
best 可以递推:,于是每组只需 预处理加 枚举。
复杂度
每组测试用例时间 ,空间 (前缀和与 best)。总体时间 ,空间 。
仓颉实现
best 的首项初始化为 作为哨兵:,真实的相邻对之和恒为正,配合「 时要求 」的判定即可避免取到非法状态。
查看仓颉实现
import std.env.*
import std.convert.*
main() {
let reader = getStdIn()
let t = Int64.parse(reader.readln().getOrThrow())
for (_ in 0..t) {
let head = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let n = head[0]
let k = head[1]
let z = head[2]
let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let pre = Array<Int64>(n, { _ => 0 })
pre[0] = a[0]
for (i in 1..n) {
pre[i] = pre[i - 1] + a[i]
}
// best[i] = max(a[p] + a[p + 1]) over 0 <= p <= i - 1
let best = Array<Int64>(n, { _ => -1 })
for (i in 1..n) {
let cand = a[i - 1] + a[i]
best[i] = if (i >= 2 && best[i - 1] > cand) { best[i - 1] } else { cand }
}
var ans: Int64 = -1
var j: Int64 = 0
while (j <= z && 2 * j <= k) {
let f = k - 2 * j
// 最后一步向右:最远到 f,做 j 次「左-右」循环
if (j == 0 || f >= 1) {
var val = pre[f]
if (j > 0) {
val += j * best[f]
}
if (val > ans) {
ans = val
}
}
// 最后一步向左:最远到 f + 1,做 j - 1 次循环,最后左到 f
let pp = f + 1
if (j >= 1 && pp <= n - 1) {
var val = pre[pp] + a[pp - 1]
if (j >= 2) {
val += (j - 1) * best[pp]
}
if (val > ans) {
ans = val
}
}
j += 1
}
println(ans)
}
}