[R76D] 检修顺序
数据规模:,。
思路
固定顺序的可行性判据。 对给定的一段工作顺序,想让时刻严格递增,贪心地取每个时刻尽量小最优:,。该顺序可行当且仅当每个 。把 展开得 ,于是可行当且仅当对任意 都有 ,即
记 、,判据就是「每个位置 的前缀最大值不超过 」。
交换位置 后的三段结构。 交换后顺序是 ,可以看成四段:前缀 、单独的第 项、单独的第 项、后缀 。三段之间的条件都是
而每段内部仍是上面的前缀最大值判据。
前缀段。 内部可行,当且仅当 满足 对所有 成立。前缀越长越难满足,所以只需记录第一个违反的位置 :前缀可行当且仅当 (不存在违反时 )。
前缀段塞满后,第 项的最早时刻为
时前缀为空,视 。
中间两项。 把第 项放到位置 :
再把第 项放到位置 :
后缀段。 位置 及其后的项都在原位,第 项的时刻下界是 ,要求它不超过 ,两边同减 得:对一切 ,
后半部分与 无关,就是「后缀 自身可行」。后缀自身可行等价于从右往左贪心的最晚安排 处处不小于 ;展开得 ,故可行当且仅当对一切 有 。后缀越短越容易可行,于是只需记录最后一个违反的位置 :后缀可行当且仅当 。
前半部分则是 。
四组条件都只依赖 的邻域,预处理 的前缀最大值、 的后缀最小值、、 后 判断每个 即可。
复杂度
时间 ,空间 。
仓颉实现
查看仓颉实现
import std.env.*
import std.convert.*
const NEG: Int64 = -(1 << 60)
const INF: Int64 = 1 << 60
main() {
let reader = getStdIn()
let n = Int64.parse(reader.readln().getOrThrow())
let l = Array<Int64>(n + 2, { _ => 0 })
let r = Array<Int64>(n + 2, { _ => 0 })
var i: Int64 = 1
while (i <= n) {
let p = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ s: String => Int64.parse(s) })
l[i] = p[0]
r[i] = p[1]
i += 1
}
// pm[i] = max_{q<=i}(l_q - q),firstBad 为最早使前缀不可行的位置
let pm = Array<Int64>(n + 2, { _ => 0 })
pm[0] = NEG
var firstBad: Int64 = n + 1
i = 1
while (i <= n) {
let a = l[i] - i
let v = if (pm[i - 1] > a) { pm[i - 1] } else { a }
pm[i] = v
if (firstBad == n + 1 && v > r[i] - i) {
firstBad = i
}
i += 1
}
// sm[i] = min_{q>=i}(r_q - q),lastBad 为最晚使后缀不可行的起始位置
let sm = Array<Int64>(n + 3, { _ => 0 })
sm[n + 1] = INF
var lastBad: Int64 = 0
i = n
while (i >= 1) {
let b = r[i] - i
sm[i] = if (sm[i + 1] < b) { sm[i + 1] } else { b }
if (l[i] - i > sm[i]) {
if (lastBad < i) {
lastBad = i
}
}
i -= 1
}
var cnt: Int64 = 0
var k: Int64 = 1
while (k < n) {
var ok = k <= firstBad
if (ok) {
let base = (k - 1) + pm[k - 1]
let u = if (l[k + 1] > base + 1) { l[k + 1] } else { base + 1 }
ok = u <= r[k + 1]
if (ok) {
let v = if (l[k] > u + 1) { l[k] } else { u + 1 }
ok = v <= r[k]
if (ok && k + 2 <= n) {
ok = k + 2 > lastBad && v - (k + 1) <= sm[k + 2]
}
}
}
if (ok) {
cnt += 1
}
k += 1
}
println(cnt)
}