离线算法。
算法核心思想是优化暴力。
一般形式是若干区间询问。
若能用较少的时间(\(O(1) - O(\log n)\))从 \([l, r]\) 的答案推演至 \([l, r + 1], [l - 1, r]\) 的答案,那么这个问题就可以用莫队优化。
普通莫队
设扩展一次的时间为 \(O(a)\)
将 \([l, r]\) 的答案快速推演至 \([l, r + 1], [l, r - 1], [l + 1, r], [l - 1, r]\)。
我们考虑把询问排序,对于相邻的询问,\([l_i, r_i]\) 移动到 \([l_{i+1}, r_{r+1}]\),复杂度是 \(O(a(|r_i - r_{i-1}| + |l_{i+1}-l_i|))\)
排序方式?考虑分块,把序列分块,设 \(i\) 所属块为 \(pos_i\),设块长度为 \(S\) ,询问排序关键字,先按照左端点块数 \(pos_l\) 排序,若左端点所属块相等,右端点按从小到大排序。
复杂度:
- 右指针:\(\frac{n^2}{S}\) 次
- 左指针:
- 块内:每次移动最多不会超过 \(S\) 次,总共 \(mS\) 次
- 块间:即这一块最后询问到下一块第一个询问,下一个询问左端点一定是大于这个询问左端点的。所以总复杂度不超过 \(n\)。
平衡算法 \(\frac{n^2}{S} = mS\)。\(S = \frac{n}{\sqrt{m}}\) 最优。总复杂度 \(O(n \sqrt{m} \times a)\)
模板题:
带修莫队
其实就是多了一个时间维。
\([l, r, t]\) 代表区间为 \([l, r]\),第 \(t\) 次修改之后的答案。
排序:第一关键字为 \(pos_l\),第二关键字为 \(pos_r\),第三关键字为 \(t\)。
设 \(pos_l , pos_r\) 相同的一段是一个块。
设每一块长度为 \(S\),一共有 \(\frac{n^2}{S}\) 块。
-
\(l\) 的移动不超过 \(mS + n\)
-
\(r\) 的移动不超过 \(mS + \frac{n^2}{S}\)
-
\(t\):\(\frac{n^2}{S^2}t\)
\(n, m\) 同阶时,\(S = \sqrt[3]{nt}\) 取到最优解,复杂度 \(O(\sqrt[3]{n^4t})\)
模板题:
回滚莫队
若只能插入,不能删除,可以用回滚莫队。
排序和普通莫队一样
设 \(R_i\) 为 \(i\) 这个块最右边点的编号。
- 暴力求块内的所有区间
- 同一块左端点,他们所有的跨块询问,这么做:首先强行变成区间 \([R_{pos_l}, R_{pos_l}]\) 剩下的询问必然在 \(r > R_{pos_l}\)。转移一个询问让右端点跟着走,然后左端点暴力扩展,暴力撤销。
复杂度类似普通莫队,左端点每次暴力扩展暴力撤销都在同一个块里,所以复杂度还是跟普通莫队相同的。
模板题:
树上莫队
一般形式是对树上一条简单路径的某些询问。
考虑把树转化成链,跑欧拉序:
- dfs 一遍,进入的时候把该节点 push 进序列,退出时再 push 一次。
设 \(L_x, R_x\) 分别为进入 / 退出 \(x\) 点对应的序列位置。
这样的序列,我们发现对于一条 \(u, v\)(设 \(L_u < L_v\)) 的路上简单路径上的所有点编号:
- 如果 \(lca(u, v) = u\),这些编号恰好是 \([L_u, L_v]\) 内出现次数为一的编号。
- 否则, 这些编号恰好是 \([R_u, L_v]\) 内出现次数为一的编号 + 一个 \(lca(u, v)\)。
证明可以画画图,感性理解:
- 如果 \(lca(u, v) = u\),那么 \(u\) 是 \(v\) 的祖先,所以从 \(u\) 一路 dfs 到 \(v\) 的第一次,这之中可能会经过其他子树,但肯定会回溯,所以经过其他子树的就肯定出现的两次。
- 如果 \(lca(u, v) \not= u\),那么他们不是祖先关系,那么跑 dfs 时,会先进到他们的 LCA,然后到 u,回溯会 LCA,再跑到 v,其中经过别的子树也都出现两次被搞掉了。
统计是否出现两次一般都是开另外一个数组,看这个编号出现过没有,如果出现过你就加上,已经在了你就删掉。
模板:
二次离线莫队
如果对于 \([l, r]\) 快速扩展一步到 \([l, r + 1], [l, r - 1], [l + 1, r], [l - 1, r]\) 的操作,不好做,可以把所有扩展的询问离线下来,都查一遍,然后重做,每次用之前弄好的询问,复杂度可以降。
模板:
每次扩展的询问形式是 \([l, r]\) 中有多少数可以和 \(r+1,r,l,l-1\) 匹配。
这个东西差分成 \(sum_{[1, r]} - sum_{[1, l - 1]}\) 这样可以扫描线扫一遍。
至于扫描线,暴力 \(n \times C_{14}^7\) 是可以过的。
复杂度 \(O(n \sqrt{n} + n \times C_{14}^7)\)
如果不离线 \(\sqrt{n}\) 和 \(C_{14}^7\) 会叠加起来。
然后空间就炸了。。
考虑其一,剩下是对称的,考虑 \([l, r]\) 跑到 \([l, R]\),其中 \(r < R\)。
那么差分的左侧都是 \([1, l - 1]\) 中所有跟 \([r, R + 1]\) 里对称的个数,这样打包到一个结构体了。
差分的右侧时 \([1, x]\) 里和 \(x + 1\) 匹配的形式,这个东西区间和匹配的数相关,预处理一下就好了。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
const int N = 100005, L = 14;
typedef long long LL;
using namespace std;
int n, m, K, t, a[N], cnt[1 << L], d[1 << L], f[N], tot, pos[N];
LL ans[N], res[N];
// R[i] 表示 [1, i] 有多少和 i + 1 匹配的。
int inline get(int x) {
int s = 0;
while (x) x -= x & -x, s++;
return s;
}
struct Q{
int l, r, id;
bool operator < (const Q &b) const {
if (pos[l] != pos[b.l]) return l < b.l;
return (pos[l] & 1) ? r < b.r : r > b.r;
}
} q[N];
struct E{
int l, r, opt, id;
};
vector<E> e[N];
int main() {
scanf("%d%d%d", &n, &m, &K); t = sqrt(n);
for (int i = 1; i <= n; i++) scanf("%d", a + i), pos[i] = (i - 1) / t + 1;
for (int i = 0; i < (1 << L); i++)
if (get(i) == K) d[++tot] = i;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= tot; j++) cnt[a[i] ^ d[j]]++;
f[i] = cnt[a[i + 1]];
}
for (int i = 0; i < (1 << L); i++) cnt[i] = 0;
for (int i = 1; i <= m; i++)
scanf("%d%d", &q[i].l, &q[i].r), q[i].id = i;
sort(q + 1, q + 1 + m);
int l = 1, r = 0;
for (int i = 1; i <= m; i++) {
if (l < q[i].l) e[r].push_back((E) { l, q[i].l - 1, -1, i });
while (l < q[i].l) res[i] += f[l - 1] + !K, l++;
if (l > q[i].l) e[r].push_back((E) { q[i].l, l - 1, 1, i });
while (l > q[i].l) res[i] -= f[l - 2] + !K, l--;
if (r < q[i].r) e[l - 1].push_back((E) { r + 1, q[i].r, -1, i });
while (r < q[i].r) res[i] += f[r++];
if (r > q[i].r) e[l - 1].push_back((E) { q[i].r + 1, r, 1, i });
while (r > q[i].r) res[i] -= f[--r];
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= tot; j++) cnt[a[i] ^ d[j]]++;
for (int j = 0; j < e[i].size(); j++) {
E u = e[i][j];
for (int k = u.l; k <= u.r; k++)
res[u.id] += cnt[a[k]] * u.opt;
}
}
for (int i = 1; i <= m; i++) res[i] += res[i - 1], ans[q[i].id] = res[i];
for (int i = 1; i <= m; i++) printf("%lld\n", ans[i]);
return 0;
}