[R16D] 通关
- 难度 普及/提高-
- 时限 1s
- 空限 512m
- 贪心倒推
数据规模:,,。
思路
面对第 关时能量为 (需 ),消耗 后剩余 ,过关奖励 ,于是过关后能量变为 。奖励函数 关于 单调递增,因此「初始能量越大越容易通关」具有单调性,既可二分答案,也可直接倒推。
采用倒推法一次性 求出最小初始能量。设 为「通过第 关所需的最小能量」,边界 ,目标是 。已知 ,要求出过第 关后剩余的 满足
对 分两种情况求最小值:
- 时 落在 段,方程化为 ,最小 ;
- 时必有 ,方程化为 ,最小 。
得到 后 。从 倒序推到 即可。
复杂度:时间 ,空间 。中间量最大可达 ,用 Int64 安全。
仓颉实现
import std.env.*
// 预分配大缓冲,分块读入全部字节,返回 (缓冲, 有效长度)。
// 大输入下逐行 split 较慢,这里一次性读入再扫描整数。
func readAll(reader: ConsoleReader): (Array<UInt8>, Int64) {
let cap: Int64 = 32 * 1024 * 1024
let buf = Array<UInt8>(cap, { _ => 0 })
var total: Int64 = 0
let chunk = Array<UInt8>(1 << 16, { _ => 0 })
while (true) {
let got = reader.read(chunk)
if (got <= 0) {
break
}
// copyTo(dest, srcOffset, destOffset, length)
chunk.copyTo(buf, Int64(0), total, got)
total = total + got
}
return (buf, total)
}
main(): Int64 {
let reader = getStdIn()
let (bytes, m) = readAll(reader)
// 单趟扫描解析整数;最坏 T(1+2n) <= 2e6,预分配 2.2e6 足够
let nums = Array<Int64>(2200000, { _ => 0 })
var cnt: Int64 = 0
var i: Int64 = 0
while (i < m) {
var c = bytes[i]
while (i < m && (c < 48 || c > 57)) {
i = i + 1
if (i < m) { c = bytes[i] }
}
if (i >= m) { break }
var v: Int64 = 0
while (i < m && c >= 48 && c <= 57) {
v = v * 10 + Int64(c - 48)
i = i + 1
if (i < m) { c = bytes[i] }
}
nums[cnt] = v
cnt = cnt + 1
}
var p: Int64 = 0
let t = nums[p]
p = p + 1
for (_ in 0..t) {
let n = nums[p]
p = p + 1
let baseA = p
let baseB = p + n
// 倒推:need 表示通过第 j+1..n 关所需的最小能量
var need: Int64 = 0
var j = n - 1
while (j >= 0) {
let bj = nums[baseB + j]
let twoB = bj * 2
// 设过第 j 关后剩余 y,则 y + min(y, bj) >= need
// need <= 2*bj 时 y = ceil(need/2);否则 y = need - bj
let y = if (need <= twoB) { (need + 1) / 2 } else { need - bj }
need = y + nums[baseA + j]
j = j - 1
}
println(need)
p = baseB + n
}
return 0
}
要点:
- 倒推消去模拟:奖励关于剩余能量单调,故「过完后续关卡所需能量」可以倒着传回来,每关用 直接解出最小的过关前剩余 ,再 ,省去二分的 因子。
- 分情况解方程: 在 处折点,按 是否超过 取两支闭式解,避免浮点,全程整数运算。
ceil(E/2)用整除(E + 1) / 2实现。 - 快读: 可达 、输入近 ,逐行
split在 1s 时限内偏紧。这里用reader.read分块读入到一个预分配大字节数组,再单趟扫描 ASCII 字节累加整数,把读入与解析压到一次线性遍历,实测最坏点约 s。