[R18D]联想


对于 100%100\% 的数据,1n,q1051\leq n,q\leq 10^51ai,xi1091\leq a_i,x_i\leq 10^9

思路

题目要求按顺序完成 nn 道题。对于第 ii 道题,可以花 aia_i 直接做,也可以从某道已完成的前置题 j<ij<i 联想:若 ajaix|a_j-a_i|\leq x,则花费 ajai|a_j-a_i|

因为按顺序做题,处理到第 ii 道题时所有 j<ij<i 均已完成,所以联想的来源不受「如何完成」限制,只受下标限制。于是对固定的 ii 与联想能力 xx

  • 若存在 j<ij<i 使 ajaix|a_j-a_i|\leq x,则联想代价为这些 ajai|a_j-a_i| 的最小值;
  • 否则只能直接做,代价为 aia_i

mindi=minj<iajai\text{mind}_i=\min_{j<i}|a_j-a_i| 为第 ii 道题到所有前置题的最小代价差(i=1i=1 时无前置题,只能直接做)。注意联想不一定更优:当 mindi>ai\text{mind}_i>a_i 时直接做反而更省。因此第 ii 道题的贡献为

costi={min(ai,mindi),mindixai,mindi>x\text{cost}_i=\begin{cases}\min(a_i,\text{mind}_i), & \text{mind}_i\leq x\\ a_i, & \text{mind}_i>x\end{cases}

11 道题恒为直接做,取 mind1=a1\text{mind}_1=a_1 即可让上式自然成立。

计算 mindi\text{mind}_i

朴素做法是边插入边维护一个有序集合,每次查询 aia_i 的前驱后继。但本题数据范围 n105n\leq 10^5,需要 O(nlogn)O(n\log n)

采用经典的 排序 + 双向链表倒序删除 技巧:

  1. 把所有 aia_i 按值升序排序,得到值序位置 posi\text{pos}_i;在值序上构建双向链表(prev/next 数组)。
  2. 原下标 ii 从大到小遍历。处理 ii 时,链表中尚未被删除的元素恰好是 {a0,a1,,ai}\{a_0,a_1,\dots,a_i\},其中 posi\text{pos}_i 在链表里的前驱和后继(不含自身)就落在 {a0,,ai1}\{a_0,\dots,a_{i-1}\} 中,且正是值域上离 aia_i 最近的两个元素,所以 mindi\text{mind}_i 即为二者与 aia_i 之差的较小者。
  3. 查询完后把 posi\text{pos}_i 从链表中删除(O(1)O(1) 改指针),继续处理 i1i-1

离线处理询问

savei=min(ai,mindi)\text{save}_i=\min(a_i,\text{mind}_i)。对所有 iimindi\text{mind}_i 升序排序,维护 save\text{save}aa 的前缀和。记 SA=aiS_A=\sum a_i

对每个询问 xx,二分找出 mindix\text{mind}_i\leq x 的元素集合(前 cnt\text{cnt} 个),则

ans=activesavei+inactiveai=pSave[cnt]+(SApA[cnt])\text{ans}=\sum_{\text{active}}\text{save}_i+\sum_{\text{inactive}}a_i =\text{pSave}[\text{cnt}]+(S_A-\text{pA}[\text{cnt}])

复杂度

  • 时间:排序 O(nlogn)O(n\log n),链表删除 O(n)O(n),每个询问二分 O(logn)O(\log n),共 O((n+q)logn)O((n+q)\log n)
  • 空间:O(n)O(n)

仓颉实现

import std.convert.*
import std.env.*
import std.sort.*

main(): Int64 {
    let reader = getStdIn()
    let firstLine = reader.readln().getOrThrow().split(" ", removeEmpty: true)
    let n = Int64.parse(firstLine[0])
    let q = Int64.parse(firstLine[1])
    let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ s: String => Int64.parse(s) })
    // 把原下标按 a 值升序排序,构建值序双向链表
    let order = Array<Int64>(n, { k: Int64 => k })
    sort(order, key: { k: Int64 => a[k] })
    let vs = Array<Int64>(n, { k: Int64 => a[order[k]] })
    let pos = Array<Int64>(n, { _ => 0 })
    for (k in 0..n) {
        pos[order[k]] = k
    }
    let prev = Array<Int64>(n, { _ => 0 })
    let next = Array<Int64>(n, { _ => 0 })
    for (k in 0..n) {
        prev[k] = k - 1
        next[k] = k + 1
    }
    next[n - 1] = -1
    prev[0] = -1
    // mind[i] = min_{j<i} |a_j - a_i|;mind[0] = a[0] 表示恒直接做
    let mind = Array<Int64>(n, { _ => 0 })
    var ii = n - 1
    while (ii >= 0) {
        let p = pos[ii]
        let ai = a[ii]
        if (ii == 0) {
            mind[ii] = ai
        } else {
            // 当前存活集合 = {a_0..a_i},前驱后继必在 {a_0..a_{i-1}} 中
            var best = Int64(2000000005)
            let pr = prev[p]
            if (pr >= 0) {
                let d = ai - vs[pr]
                if (d < best) {
                    best = d
                }
            }
            let nx = next[p]
            if (nx >= 0) {
                let d = vs[nx] - ai
                if (d < best) {
                    best = d
                }
            }
            mind[ii] = best
        }
        // 从链表删除位置 p
        let pr = prev[p]
        let nx = next[p]
        if (pr >= 0) {
            next[pr] = nx
        }
        if (nx >= 0) {
            prev[nx] = pr
        }
        ii -= 1
    }
    // 离线回答询问:save[i] = min(a[i], mind[i])
    var SA = Int64(0)
    for (k in 0..n) {
        SA += a[k]
    }
    let save = Array<Int64>(n, { k: Int64 => if (a[k] < mind[k]) { a[k] } else { mind[k] } })
    let idx = Array<Int64>(n, { k: Int64 => k })
    sort(idx, key: { k: Int64 => mind[k] })
    let sm = Array<Int64>(n, { _ => 0 })
    let psave = Array<Int64>(n, { _ => 0 })
    let pa = Array<Int64>(n, { _ => 0 })
    for (k in 0..n) {
        let oi = idx[k]
        sm[k] = mind[oi]
        psave[k] = save[oi]
        pa[k] = a[oi]
    }
    let pSave = Array<Int64>(n + 1, { _ => 0 })
    let pA = Array<Int64>(n + 1, { _ => 0 })
    for (k in 0..n) {
        pSave[k + 1] = pSave[k] + psave[k]
        pA[k + 1] = pA[k] + pa[k]
    }
    var out = StringBuilder()
    var qi = Int64(0)
    while (qi < q) {
        let x = Int64.parse(reader.readln().getOrThrow())
        // 二分找 mind <= x 的元素个数(上界)
        var lo = Int64(0)
        var hi = n
        while (lo < hi) {
            let mid = (lo + hi) >> 1
            if (sm[mid] <= x) {
                lo = mid + 1
            } else {
                hi = mid
            }
        }
        let cnt = lo
        let ans = pSave[cnt] + (SA - pA[cnt])
        out.append(ans)
        out.append('\n')
        qi += 1
    }
    print(out.toString())
    return 0
}