连锁反应
- 难度 提高
- 时限 1s
- 空限 256m
- 线性 DP
数据规模:,,,所有 两两不同。
思路
新信标只会摧毁一段后缀。 新信标在所有原有信标右侧,它的爆炸范围是「位置不小于某个值」的一段,因此被它摧毁的原有信标恰好是按位置排序后的最右边连续 个( 可以是 到 的任意值:把新信标放得足够远、半径取得足够小就能只摧毁前 个)。剩下的信标是位置最靠左的 个,之后蜗蜗从它们中间最靠右的那个开始,按位置从右到左激活,这个子过程与只考虑这 个信标时完全相同。于是若记
- :只考虑位置最靠左的 个信标时,最终没有被摧毁的信标个数,
那么摧毁总数是 ,对所有 取最小即 。
关键观察:第一次爆炸之后,问题变成同一个问题的更小规模。 只考虑最左边 个信标时,最右侧的第 个信标一定最先被激活、一定存活,它会摧毁左侧位置不小于 的所有信标。这一刻还没有别的信标爆炸过,所以被摧毁的正是一段连续下标区间 ,其中 是第一个位置满足 的下标(所有位置都在这个区间内的信标都还活着)。区间左边的 个信标一个都没受影响,并且在它们内部继续按位置从右到左激活——这正是「只考虑最左边 个信标」的子问题。由此得到
因为 ,按 从小到大递推即可, 步。
为什么不能简单认为新信标没用。 摧毁最右侧的信标本身也计一次,但它的爆炸半径可能很大;把它连同半径一起「提前引爆」,反而能让左侧更多信标活下来。例如位置半径依次为 时,不用新信标会被 那颗一次炸掉 个,而让新信标只摧毁 那颗后只剩 个被摧毁。所以必须对所有前缀取最大值,不能只算 。
边界。 落在第一个信标左边时取 (等价于 ); 对应把所有原有信标都摧毁,对应 ,因此答案一定不超过 , 时为 。
复杂度
位置范围只有 ,用位置桶代替排序,并在扫描时顺便记录「前缀桶里有几个信标」,于是 可以 查出:时间 ,空间 ,其中 是位置上界。若改用排序 + 二分,则为 时间、 空间。
仓颉实现
pre[p] 记录位置严格小于 p 的信标个数,它同时就是「 的最左下标」,a[i] - b[i] 为负时截到 (此时一个信标也不会被摧毁,对应 S[0])。
查看仓颉实现
import std.env.*
var gdata = Array<Byte>(0, { _ => 0 })
var gpos: Int64 = 0
func nextInt(): Int64 {
while (gpos < gdata.size && Int64(gdata[gpos]) <= 32) {
gpos += 1
}
var x: Int64 = 0
while (gpos < gdata.size && Int64(gdata[gpos]) > 32) {
x = x * 10 + Int64(gdata[gpos]) - 48
gpos += 1
}
return x
}
main() {
gdata = getStdIn().readToEnd().getOrThrow().toArray()
let n = nextInt()
let lim: Int64 = 1000001
let rad = Array<Int64>(lim, { _ => 0 })
let has = Array<Bool>(lim, { _ => false })
for (_ in 0..n) {
let x = nextInt()
let y = nextInt()
rad[x] = y
has[x] = true
}
let a = Array<Int64>(n, { _ => 0 })
let b = Array<Int64>(n, { _ => 0 })
let pre = Array<Int64>(lim, { _ => 0 })
var m: Int64 = 0
for (p in 0..lim) {
pre[p] = m
if (has[p]) {
a[m] = p
b[m] = rad[p]
m += 1
}
}
let s = Array<Int64>(n + 1, { _ => 0 })
var best: Int64 = 0
for (i in 0..n) {
let left = a[i] - b[i]
var lo: Int64 = 0
if (left >= 0) {
lo = pre[left]
}
s[i + 1] = s[lo] + 1
if (s[i + 1] > best) {
best = s[i + 1]
}
}
println(n - best)
}