二次离线莫队学习笔记
二次离线莫队
对于普通莫队,时间复杂度为 乘以每次增加、删除操作的时间复杂度,一般为 。其不为 时,但满足增加或删除操作其中至少有一个为 时,我们也可以用回滚莫队 实现。然而,有时两项操作均不能达到 ,例如其复杂度为“区间长度”。在一些特定的题目中,我们可以使用二次离线莫队,时间复杂度还是 。
在用这种方法处理区间的增加和删除操作时,变化量无法快速求出。但是我们注意到众多区间的变化量有“相似”之处,于是将这些变化量离线下来,统一算出后,再用于更新莫队的区间操作。
以Acwing2535 二次离线莫队为例。
给定一个长度为 的序列 以及一个整数 。
现在要进行 次询问,每次询问给定一个区间 ,请你求出共有多少个数对 满足 且 的结果在二进制表示下恰好有 个 。 表示按位异或操作。
,,。
注意到,区间左右端点指针的每次移动的时间复杂度均为“当前区间长度”,显然无法通过此题。我们来看二次离线莫队的做法。
首先,设 为一个二进制下有 个 的数。我们定义 为 与 匹配。另外,若 ,则 。由于 ,,故所有的 均可以预处理得出,且总数不到3500个。其个数设为 。
设 表示 ~ 中与 匹配的数的个数。再利用一个迭代数组 表示迭代过程中与 匹配的数的个数。则可以 求出:
for (int i = 0; i < 1 << 14; i++) {
int x = i, cnt = 0;
while (x) {
cnt++; x ^= x & -x;
}
if (cnt == k) tar.push_back(i); //求tar
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < tar.size(); j++) g[w[i] ^ tar[j]]++;
f[i] = g[w[i + 1]];
}
再来看莫队中区间的增减操作:
- 将区间右指针 向右移动到
将 移动到 时, += 与 匹配的数的个数。利用前缀和的思想,就是 与 匹配个数 - 与 匹配个数。前者 = ,且随 向右移动 位,分别为 。后者分别为 与 匹配个数。
于是得到, 右移至 , += - 与 匹配个数。将后项记作 。
此时,前项可随指针右移快速求出,但后项仍无法快速求出。没关系,先看其他情况。 - 将 向左移动到
推法与上相似。 += 。 - 将 向左移动到
将 移动到 时, += 与 匹配个数。即 与 匹配个数 - 与 匹配个数。因为 与自己匹配当且仅当 ,故后项可化为 。于是得到, 左移至 , += 。 - 将 向右移动到
与上相似。 += 。
于是我们的任务只剩下快速求出所有 。如果在移动时在线模拟,那么复杂度很高。但是我们注意到所有 有相似之处,它们都形如 。那么我们是不是可以将它们全部离线下来,扫一边序列的同时,利用迭代数组 统一求出所有 呢?很显然,这样的做法避免了每次求 时重新从头扫描,时间复杂度肯定可以降低。那么我们来分析一下做法。
扫描一遍序列,并实时更新迭代数组 。来到第 个数时,找到所有 ,记录答案为 。总时间复杂度为 ,其中 表示所有 的 长度之和。我们考虑 的实际意义,就是:指针 和 的移动距离之和!在普通莫队中,这个值就是莫队的时间复杂度 。可以承受。
至此,二次离线莫队的算法已经分析完了。因为 较小,故时间复杂度为 。
Code
#include<cstdio>
#include<cstring>
#include<vector>
#include<cmath>
#include<algorithm>
#define ll long long
using namespace std;
const int N = 1e5 + 5;
int n, m, k, w[N], pos[N];
int g[N], f[N];
ll ans[N];
struct Query {
int id, l, r;
ll res;
bool operator <(const Query &oth) const {
return pos[l] != pos[oth.l] ? pos[l] < pos[oth.l] : r < oth.r;
}
} q[N];
vector<int> tar;
vector<Query> range[N]; //range存储match
//注意,虽然q和range都适用Query类型,但它们的实际意义不一样
int read() {
int x = 0; char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
return x;
}
int main() {
n = read(); m = read(); k = read();
for (int i = 1; i <= n; i++) w[i] = read();
for (int i = 0; i < 1 << 14; i++) {
int x = i, cnt = 0;
while (x) {
cnt++; x ^= x & -x;
}
if (cnt == k) tar.push_back(i);
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < tar.size(); j++) g[w[i] ^ tar[j]]++;
f[i] = g[w[i + 1]];
}
int len = max(1, (int)sqrt((double)n * n / m));
for (int i = 1; i <= n; i++) pos[i] = (i - 1) / len + 1;
for (int i = 1; i <= m; i++) q[i] = (Query){i, read(), read()};
sort(q + 1, q + m + 1);
for (int i = 1, L = 1, R = 0; i <= m; i++) {
int l = q[i].l, r = q[i].r;
if (R < r) range[L - 1].push_back((Query){i, R + 1, r, -1}); //-1和1的类型是为了方便代码,表示-和+
while (R < r) q[i].res += f[R], R++;
if (R > r) range[L - 1].push_back((Query){i, r + 1, R, 1});
while (R > r) q[i].res -= f[R - 1], R--;
if (L < l) range[R].push_back((Query){i, L, l - 1, -1});
while (L < l) q[i].res += f[L - 1] + !k, L++;
if (L > l) range[R].push_back((Query){i, l, L - 1, 1});
while (L > l) q[i].res -= f[L - 2] + !k, L--;
}
memset(g, 0, sizeof (g));
for (int i = 1; i <= n; i++) {
for (int j = 0; j < tar.size(); j++) g[w[i] ^ tar[j]]++;
for (int j = 0; j < range[i].size(); j++)
for (int p = range[i][j].l; p <= range[i][j].r; p++)
q[range[i][j].id].res += g[w[p]] * range[i][j].res;
}
for (int i = 1; i <= m; i++) {
q[i].res += q[i - 1].res; ans[q[i].id] = q[i].res;
}
for (int i = 1; i <= m; i++) printf("%lld\n", ans[i]);
return 0;
}
In the end
本题要旨在于巧妙的前缀和转化,以及 和 数组的实现。
本文作者:realFish
本文链接:https://www.cnblogs.com/fish07/p/16097405.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步