数据结构专题-学习笔记:莫队二次离线
1. 前言
莫队二次离线,是一种莫队,由 lxl 发明,专门用来处理莫队中转移不是 \(O(1)\),但是可以前缀和拆分的问题。
在学习之前,请先确保对莫队有一定的了解度,包括但不限于普通莫队及其优化。
如果您不知道普通莫队是什么,可以看一看我的 这篇博文。
2. 模板
模板题:P4887 【模板】莫队二次离线(第十四分块(前体))
首先考虑莫队 废话
一种思路就是直接利用普通莫队,每一次移动指针的时候 \(O(C_{14}^k)\) 的复杂度来转移,但是这显然会超时。
那么考虑如何优化复杂度。
接下来设 \(f(x,[l,r])\) 表示 \(x\) 对区间 \([l,r]\) 的贡献,\(g([l_1,r_1],[l_2,r_2])\) 表示 \([l_1,r_1]\) 中的每一个数对 \([l_2,r_2]\) 的贡献,也就是 \(\sum_{i=l_1}^{r_1}f(a_i,[l_2,r_2])\)。
那么在转移的时候,我们对式子做一个变形:
也就是可以差分。
那么如果记 \(h([l,r])\) 表示区间 \([l,r]\) 的答案,那么仿照上面做一遍差分:
于是我们发现此时的结果分为两部分:形如 \(h([1,k])\) 的和形如 \(g([l_1,r_1],[l_2,r_2])\) 的。
那么形如 \(h([1,k])\) 的可以采用一遍预处理与一遍莫队求出来,这样就解决了第一部分。
但是 \(g([l_1,r_1],[l_2,r_2])\) 要怎么办啊?
看一下前面给出的式子:\(g([l_1,r_1],[l_2,r_2])=\sum_{i=l_1}^{r_1}f(a_i,[l_2,r_2])\)。
这与莫队指针移动的连续性恰好吻合。
然后如果您学过扫描线,您还可以发现所有形如 \(g([l_1,r_1],[l_2,r_2])\) 可以利用扫描线解决。
那么于是我们先用一次莫队算出形如 \(h([1,k])\) 的答案以及处理出所有形如 \(g([l_1,r_1],[l_2,r_2])\) 的询问,然后一遍扫描线解决答案。
然后最后的答案统计在 \(ans\) 数组内。
于是您连样例都过不去。
为什么?上面的过程有问题吗?
其实上面的过程没问题,关键的一点就是在执行上述过程的时候实际上 \(ans\) 记录的是答案的变化量,最后还要做一遍前缀和才能通过。
友情提醒:考虑到代码里面使用了先进的 STL:tuple,因此请采用 C++17 及以上编译。
当然可以写个结构体
代码:
/*
========= Plozia =========
Author:Plozia
Problem:P4887 【模板】莫队二次离线(第十四分块(前体))
Date:本代码书写于 2020/12/16
========= Plozia =========
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN = 1e5 + 10;
int n, m, k, a[MAXN], t[MAXN], p[MAXN], block, ys[MAXN];
LL ans[MAXN];
struct node
{
int l, r, id;
LL ans;
}q[MAXN];
vector <int> b;
vector < tuple <int, int, int, int> > v[MAXN];
int read()
{
int sum = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum;
}
bool cmp(const node &fir, const node &sec)
{
if (ys[fir.l] ^ ys[sec.l]) return ys[fir.l] < ys[sec.l];
return fir.r < sec.r;
}
int Count(int x)
{
int sum = 0;
for (; x; x >>= 1)
if (x & 1) sum++;
return sum;
}//统计位数
int main()
{
n = read(); m = read(); k = read(); block = ceil(n / sqrt(m));
if (k > 14)
{
for (int i = 1; i <= m; ++i) printf("0\n");
return 0;
}//坑
for (int i = 1; i <= n; ++i) {a[i] = read(); ys[i] = (i - 1) / block + 1;}
for (int i = 1; i <= m; ++i) {q[i].l = read(); q[i].r = read(); q[i].id = i;}
sort(q + 1, q + m + 1, cmp);
for (int i = 0; i < 16384; ++i)
if (Count(i) == k) b.push_back(i);
for (int i = 1; i <= n; ++i)
{
p[i] = t[a[i]];
for (int j = 0; j < b.size(); ++j) t[a[i] ^ b[j]]++;
}//预处理
memset(t, 0, sizeof(t));
int l = 1, r = 0;
for (int i = 1; i <= m; ++i)
{
if (l > q[i].l) v[r].emplace_back(q[i].l, l - 1, i, 1);
while (l > q[i].l) q[i].ans -= p[--l];
if (r < q[i].r) v[l - 1].emplace_back(r + 1, q[i].r, i, -1);
while (r < q[i].r) q[i].ans += p[++r];
if (l < q[i].l) v[r].emplace_back(l, q[i].l - 1, i, -1);
while (l < q[i].l) q[i].ans += p[l++];
if (r > q[i].r) v[l - 1].emplace_back(q[i].r + 1, r, i, 1);
while (r > q[i].r) q[i].ans -= p[r--];
}//莫队,处理 h([1,k]) 答案以及 g([l1,r1],[l2,r2]) 的询问
for (int i = 1; i <= n; ++i)
{
for (int j = 0; j < b.size(); ++j) ++t[a[i] ^ b[j]];
for (int j = 0; j < v[i].size(); ++j)
{
tuple x = v[i][j];
for (int zzh = get<0>(x); zzh <= get<1>(x); ++zzh)
{
if (zzh <= i && k == 0) q[get<2>(x)].ans += get<3>(x) * (t[a[zzh]] - 1);
else q[get<2>(x)].ans += get<3>(x) * t[a[zzh]];
}
}
}//扫描线
for (int i = 1; i <= m; ++i) q[i].ans += q[i-1].ans;//注意答案要差分
for (int i = 1; i <= m; ++i) ans[q[i].id] = q[i].ans;//重新还原答案
for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]);
return 0;
}
3. 总结
莫队二次离线的式子:\(h([l,r])=h([1,r])-h([1,l-1])-g([1,l-1],[l,r])\)。