二次离线莫队
二次离线是个啥?
二次离线莫队
核心
普通莫队在指针移动时,如果单次增加/删除操作的时间复杂度太高,会导致总复杂度退化。
二次离线莫队的核心思想是:将莫队指针移动过程中产生的“贡献更新”拆分为两部分,第一部分用预处理直接 \(O(1)\) 计算,第二部分暂不计算,作为新的询问挂在对应的端点上(第一次离线);等莫队扫描完成后,统一按顺序处理这些挂载的询问(第二次离线)。
具体做法
- 第一次离线:不是很正常的跑一遍莫队。
- 利用前缀和思想,将移动产生的新贡献拆分为:\(\text{f}(x, [1, R]) - \text{f}(x, [1, L-1])\)。
- 第一部分 \(\text{f}(x, [1, R])\) 利用预处理的前缀和数组 \(O(1)\) 实时累加。
- 第二部分 \(\text{f}(x, [1, L-1])\) 作为区间查询打包存入 vector,挂载在位置 \(L-1\) 上。
- 第二次离线:重新扫描
- 维护支持数据插入的结构(如频次数组)。
- 遇到挂载在当前位置的询问时,扫描其指定的区间并统计贡献。
- 合并答案:将莫队过程中的前缀贡献与第二次离线计算出的增量贡献做前缀和,得到最终答案。
P4887 【模板】莫队二次离线 题解
题意简述
给定一个长度为 \(n\) 的序列 \(a\)(\(a_i < 2^{14}\)),有 \(m\) 次询问。每次查询区间 \([l, r]\) 内满足 \(i < j\) 且 \(a_i \oplus a_j\) 的二进制表示中有 \(k\) 个 \(1\) 的数对 \((i, j)\) 的数量。
做法
1. 预处理
- 若 \(k > 14\),直接特判输出
0。 - 生成所有满足
__builtin_popcount(x) == k且 \(x < 2^{14}\) 的数,存入Xor数组中。 - 预处理前缀数组
pre1和pre2,用于在莫队移动时 \(O(1)\) 获取与当前前缀产生的贡献。
2. 莫队移动(第一次离线)
按照普通莫队排序(带奇偶块优化)。在移动指针时:
- 移动 \(r\) 到 \(qr\) 时:
- 第一部分:直接利用前缀和
pre1[qr] - pre1[r]更新当前莫队答案sum1[i]。 - 第二部分:将查询区间 \([r+1, qr]\) 挂载到位置 \(l-1\) 下,系数设为 \(-1\)。
- 第一部分:直接利用前缀和
- 移动 \(l\) 到 \(ql\) 时同理,利用
pre2处理前缀部分,剩余区间挂载到 \(r\) 下。
3. 计算答案(第二次离线)
清空计数数组后,从 \(1\) 到 \(n\) 遍历数组:
- 将当前 \(a[i]\) 与
Xor数组中的所有数做异或更新频次表(做 \(n\) 次,每次 \(O(\binom{14}{k})\))。 - 遍历挂载在位置 \(i\) 上的所有区间,逐个累加区间内元素的频次(总共扫描 \(O(n\sqrt{m})\) 个元素,单次 \(O(1)\))。
代码实现
戳我喵~
vector<int> Xor;
int a[N], cnt[N];
int n, m, k;
int pos[N], block;
struct Query {
int l, r, id;
bool operator <(const Query &x) const {
return pos[l] == pos[x.l] ? (pos[l] & 1 ? r < x.r : r > x.r) : pos[l] < pos[x.l];
}
} q[N];
struct node {
int l, r, id, op;
};
vector<node> g[N];
int sum1[N], sum2[N];
int ans[N];
int lst[N], now[N];
int pre1[N], pre2[N];
signed main() {
n = re, m = re, k = re;
for (int i = 1; i <= n; i++) a[i] = re;
// 特判:14位二进制最多只有14个1
if (k > 14) {
while (m--) puts("0");
return 0;
}
// 1. 预处理二进制下有 k 个 1 的异或值
for (int i = 0; i < 16384; i++) {
if (__builtin_popcount(i) == k) Xor.push_back(i);
}
// 2. 预处理前缀贡献
for (int i = 1; i <= n; i++) {
lst[i] = cnt[a[i]];
for (auto x : Xor) cnt[a[i] ^ x]++;
now[i] = cnt[a[i]];
pre1[i] = pre1[i - 1] + lst[i];
pre2[i] = pre2[i - 1] + now[i];
}
for (int i = 1; i <= m; i++) q[i] = {re, re, i};
block = max(1ll, (int)(n / sqrt(m)));
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / block + 1;
sort(q + 1, q + 1 + m);
// 3. 莫队移动(第一次离线)
int l = 1, r = 0;
for (int i = 1; i <= m; i++) {
int ql = q[i].l, qr = q[i].r, qid = q[i].id;
if (l > ql) {
sum1[i] -= (pre2[l - 1] - pre2[ql - 1]);
g[r].push_back({ql, l - 1, qid, 1});
l = ql;
}
if (r < qr) {
sum1[i] += (pre1[qr] - pre1[r]);
g[l - 1].push_back({r + 1, qr, qid, -1});
r = qr;
}
if (l < ql) {
sum1[i] += (pre2[ql - 1] - pre2[l - 1]);
g[r].push_back({l, ql - 1, qid, -1});
l = ql;
}
if (r > qr) {
sum1[i] -= (pre1[r] - pre1[qr]);
g[l - 1].push_back({qr + 1, r, qid, 1});
r = qr;
}
}
// 4. 第二次离线扫描计算
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= n; i++) {
for (auto x : Xor) cnt[a[i] ^ x]++;
for (auto x : g[i]) {
int Ans = 0;
for (int j = x.l; j <= x.r; j++) Ans += cnt[a[j]];
sum2[x.id] += x.op * Ans;
}
}
// 5. 还原答案
int Ans = 0;
for (int i = 1; i <= m; i++) {
Ans += sum1[i] + sum2[q[i].id];
ans[q[i].id] = Ans;
}
for (int i = 1; i <= m; i++) wr(ans[i]), endl;
return 0;
}
时间复杂度:\(O(n \cdot \binom{14}{k} + n\sqrt{m})\)

浙公网安备 33010602011771号