抛硬币奖励
- 难度 提高
- 时限 1s
- 空限 256m
- 线性 DP
数据规模:,,,所有 互不相同。
思路
状态。 硬币的正反面可以任意选择,所以每一步都是一个决策。由于每一次的收益只取决于「这一次选了正面还是反面」以及「选完之后计数器变成多少」,而计数器在反面时会立刻归零,真正影响未来的信息只有当前计数器的值:
初始时还没抛掷,,其余状态都不可达。
转移。 设当前要抛第 次(),从状态 出发:
- 选反面:计数器归零,本次不得钱,转移到计数器 ,即 ;
- 选正面:计数器变成 ,本次得到 ,并且计数器一旦等于某个 就额外得到 ,即
其中 (当 时),否则为 。
这里的关键是「计数器变为 」只发生在正面让计数从 涨到 的那一刻:计数每次正面只加 ,所以同一段连续正面里 至多被跨过一次,不会重复计奖;而一旦出现反面归零,之后再次连续正面又会重新经过 ,可以再次得奖——这正好由「归零后从 重新出发」自然刻画,不需要额外记录哪几种奖励已经领过。
为什么只用最大值就够。 从同一个状态出发,未来能拿到的钱只由当前计数决定,与到达它的具体过程无关;而收益是累加的,所以对每个计数只需保留收益最大的那个前驱。归零那一步把所有状态压到 ,同样取最大值即可。
边界与实现细节。
- 计数器最多涨到 ,所以第 次只需遍历 ,用滚动数组在 空间内完成;交换两个数组即可,不必清空被换下来的旧值(每一轮都会把所有会被读到的下标重写一遍)。
- ,用一个长度为 的
bonus数组把奖励预置好,避免每轮查找。 - 不可达状态置为一个极小值(如 )。它的转移结果最多再加 ,仍远小于 ,不会与真实收益混淆,也不会在 64 位整数内下溢。
- 答案取所有计数上的最大值,而不是 :最后一次抛掷可能是正面,计数器不必回到 。
以样例 1 为例,三次都选正面时依次到达计数 ,收益为 再加上 的 与 的 ,共 。
复杂度
时间 (第 轮遍历 个状态,合计约 次转移),空间 。
仓颉实现
用两个长度为 的数组滚动,每一轮把旧行换成新行,转移时只遍历 ,同时顺便求出归零状态需要的最大值。
查看仓颉实现
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 m = first[1]
let x = getStdIn().readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let bonus = Array<Int64>(n + 2, { _ => 0 })
for (_ in 0..m) {
let line = getStdIn().readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
bonus[line[0]] = line[1]
}
let NEG: Int64 = -1000000000000000000
var dp = Array<Int64>(n + 2, { _ => NEG })
var ndp = Array<Int64>(n + 2, { _ => NEG })
dp[0] = 0
for (i in 0..n) {
var best = NEG
for (c in 0..=i) {
if (dp[c] > best) {
best = dp[c]
}
}
ndp[0] = best
for (c in 0..=i) {
ndp[c + 1] = dp[c] + x[i] + bonus[c + 1]
}
let tmp = dp
dp = ndp
ndp = tmp
}
var ans = NEG
for (c in 0..=n) {
if (dp[c] > ans) {
ans = dp[c]
}
}
println(ans)
}