[R59D] 数组查询
- 难度 普及+/提高
- 时限 1s
- 空限 512m
- 前缀和位运算
数据规模:,,,所有查询的 之和不超过 。
思路
把相邻元素对 看作一条边,边权为 ,则全序列权值 就是所有 条边权之和。
删除若干下标后,只有被删下标附近的边会发生变化。单个被删位置 会让原边 与 消失,同时剩余序列中 与 变成相邻,新增一条边 。
若一次查询删除多个下标,连续的被删下标要合并成一个删除块 (否则相邻块之间会重复计算消失的边):
- 消失的原边是 的所有边;
- 若 且 ,剩余序列中 与 变成相邻,新增边权 ;
- 若 (块贴着序列开头)或 (块贴着末尾),则没有新增边。
预处理边权前缀和 ,每个删除块消失的边权和即可 求得。每次查询先令答案为 ,再对每个删除块减去消失边、加上新增边即可。
被删下标严格递增,线性扫描一遍即可把连续下标合并成块,单次查询复杂度 。
复杂度
时间 ,空间 。
仓颉实现
代码中 a 数组按下标从 0 存储,而删除块 中的 是题面里的 1 基下标;答案最大约 ,用 Int64 存储。
import std.env.*
import std.convert.*
main(): Int64 {
let reader = getStdIn()
let line1 = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let n = line1[0]
let q = line1[1]
let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
// pre[j] 表示原序列中边 1..j 的异或和(边 i 指 a_i 与 a_{i+1} 的异或),pre[0] = 0
let pre = Array<Int64>(n, { _ => 0 })
var i: Int64 = 1
while (i <= n - 1) {
pre[i] = pre[i - 1] + (a[i - 1] ^ a[i])
i += 1
}
let total = pre[n - 1]
let sb = StringBuilder()
for (_ in 0..q) {
let qline = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
let k = qline[0]
var ans = total
var idx: Int64 = 1
while (idx <= k) {
// 合并连续的被删下标,得到一个删除块 [L, R]
let L = qline[idx]
var R = L
while (idx + 1 <= k && qline[idx + 1] == R + 1) {
idx += 1
R = qline[idx]
}
// 去掉块内涉及的原边
var lo = L - 1
if (lo < 1) { lo = 1 }
var hi = R
if (hi > n - 1) { hi = n - 1 }
ans -= pre[hi] - pre[lo - 1]
// 块不在两端时,剩余序列中 L-1 与 R+1 变成相邻,补上这条新边
if (L > 1 && R < n) {
ans += a[L - 2] ^ a[R]
}
idx += 1
}
sb.append(ans)
sb.append("\n")
}
print(sb.toString())
return 0
}
要点:
- 新增边只在 且 时存在,块贴着数组边界时不要加。
- 全删时所有块覆盖整条序列,答案自然减为 ,无需特判。