离线算法。
算法核心思想是优化暴力。
一般形式是若干区间询问。
若能用较少的时间((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} imes 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) ot= 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 imes C_{14}^7) 是可以过的。
复杂度 (O(n sqrt{n} + n imes 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
", ans[i]);
return 0;
}