[R47E]集合


题目

nn 只兔子在数轴上,第 ii 只坐标为 aia_i(已保证非降序),每秒最多移动 11 单位。最多使用 kk 次魔法,每次可让一只兔子瞬移最多 xx 单位,每只兔子至多被魔法选中一次。要求选一个整数坐标作为集合点,使所有兔子在同一位置集合的最少秒数。保证所有 aia_i 与 xx 均为偶数。

对于 100%100\% 的数据:1≤n≤1051\le n\le 10^5,0≤k≤n0\le k\le n,0≤x≤1090\le x\le 10^9,0≤a1≤⋯≤an≤1090\le a_1\le\cdots\le a_n\le 10^9。

思路

二分答案 TT(最少秒数),问题转化为判定:能否在 TT 秒内、使用不超过 kk 次魔法,让所有兔子到达某个整数坐标 pp。

设集合点为 pp。一只兔子到 pp 的距离是 ∣ai−p∣|a_i-p|:

  • 不用魔法时,能走的距离就是 TT,因此需要 ∣ai−p∣≤T|a_i-p|\le T;
  • 用一次魔法时,走 TT 加上瞬移 xx,因此需要 ∣ai−p∣≤T+x|a_i-p|\le T+x。

由于每只兔子至多用一次魔法、共至多 kk 次,所以合法 pp 必须满足:

  1. 覆盖:所有兔子都在 pp 的 T+xT+x 范围内,即 max⁡a−min⁡a≤2(T+x)\max a-\min a\le 2(T+x),且 p∈[max⁡a−(T+x), min⁡a+(T+x)]p\in[\max a-(T+x),\ \min a+(T+x)];
  2. 窗口:不需要魔法的兔子(即落在 [p−T,p+T][p-T,p+T] 内的)至少有 n−kn-k 只,这样最多剩下 kk 只用魔法补足。

剩下就是快速判断「是否存在合法 pp 同时满足上述两条」。由于 aa 已排序,保留下来不使用魔法的兔子必然是一段连续子数组 a[l..j]a[l..j](其余从左、右两端删掉)。枚举左侧删掉 ll 只(0≤l≤k0\le l\le k),则右侧至多删 k−lk-l 只;要让保留的窗口尽量窄,jj 取满足「保留数量 ≥n−k\ge n-k、且右侧删掉数量 ≤k−l\le k-l」的最小值:

j=max⁡(l+(n−k)−1, (n−1)−(k−l)).j=\max(l+(n-k)-1,\ (n-1)-(k-l)).

只需检查这段最窄窗口能否塞进宽度 2T2T 的聚集窗口,即 a[j]−a[l]≤2Ta[j]-a[l]\le 2T;同时聚集点 pp 必须落在窗口中心可行区间 [a[j]−T, a[l]+T][a[j]-T,\ a[l]+T] 与覆盖可行区间 [max⁡a−(T+x), min⁡a+(T+x)][\max a-(T+x),\ \min a+(T+x)] 的交集内,二者相交即可。

特判 n−k≤0n-k\le 0(魔法次数够给所有兔子用):此时聚集窗口内的兔子数条件恒成立,只需覆盖可行即可。

每个 check 是 O(k)O(k)(即 O(n)O(n)),外层二分 O(log⁡V)O(\log V),总复杂度 O(nlog⁡V)O(n\log V),V=109V=10^9。

代码

查看仓颉实现
import std.convert.*
import std.console.*
import std.sort.*

main() {
    let reader = Console.stdIn
    let line = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
    let n = line[0]
    let k = line[1]
    let x = line[2]
    let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
    sort(a)
    let minA = a[0]
    let maxA = a[n - 1]
    let need = n - k // 至少保留的兔子数
    var lo = Int64(0)
    var hi = Int64(2000000005) // > 10^9
    while (lo < hi) {
        let mid = (lo + hi) / Int64(2)
        if (check(mid, n, k, x, a, minA, maxA, need)) {
            hi = mid
        } else {
            lo = mid + Int64(1)
        }
    }
    println(lo)
}

func check(T: Int64, n: Int64, k: Int64, x: Int64, a: Array<Int64>, minA: Int64, maxA: Int64, need: Int64): Bool {
    // 覆盖可行性:所有兔子必须能被走路 T + 魔法 x 覆盖,range <= 2(T+x)
    if (maxA - minA > Int64(2) * (T + x)) {
        return false
    }
    let covLo = maxA - (T + x)
    let covHi = minA + (T + x)
    // 特判:need <= 0 表示魔法次数足够给所有兔子用,只要覆盖可行即可,
    // 且聚集点 p 取覆盖区间内任意一点都满足(窗口内兔子数 >= 0 恒成立)。
    if (need <= Int64(0)) {
        return covLo <= covHi
    }
    // 枚举左侧删除 l
    var l = Int64(0)
    while (l <= k) {
        if (l > n - Int64(1)) {
            break
        }
        // 保留从下标 l 起,至少 need 个 => j >= l+need-1
        let jmin = l + need - Int64(1)
        if (jmin > n - Int64(1)) {
            l++
            continue
        }
        // 右侧删除 = n-1-j <= k-l => j >= n-1-(k-l)
        var jlo = jmin
        let bound = n - Int64(1) - (k - l)
        if (bound > jlo) {
            jlo = bound
        }
        // jlo < n
        if (jlo <= n - Int64(1)) {
            // 窗口宽度
            if (a[jlo] - a[l] <= Int64(2) * T) {
                // 中心可行区间 [a[jlo]-T, a[l]+T] 与 [covLo,covHi] 相交
                let pLo = a[jlo] - T
                let pHi = a[l] + T
                if (pHi >= covLo && pLo <= covHi) {
                    return true
                }
            }
        }
        l++
    }
    return false
}