二次离线莫队学习笔记


二次离线莫队

对于普通莫队,时间复杂度为 \(O(N\sqrt{M})\) 乘以每次增加、删除操作的时间复杂度,一般为 \(O(1)\)。其不为 \(O(1)\) 时,但满足增加或删除操作其中至少有一个为 \(O(1)\) 时,我们也可以用回滚莫队 \(O(N\sqrt{M})\) 实现。然而,有时两项操作均不能达到 \(O(1)\),例如其复杂度为“区间长度”。在一些特定的题目中,我们可以使用二次离线莫队,时间复杂度还是 \(O(N\sqrt{M})\)
在用这种方法处理区间的增加和删除操作时,变化量无法快速求出。但是我们注意到众多区间的变化量有“相似”之处,于是将这些变化量离线下来,统一算出后,再用于更新莫队的区间操作。

以Acwing2535 二次离线莫队为例。
给定一个长度为 \(n\) 的序列 \(a1,a2,…,an\) 以及一个整数 \(k\)
现在要进行 \(m\) 次询问,每次询问给定一个区间 \([l,r]\),请你求出共有多少个数对 \((i,j)\) 满足 \(l≤i\(ai⊕aj\) 的结果在二进制表示下恰好有 \(k\)\(1\)\(⊕\) 表示按位异或操作。
\(1≤n,m≤10^5\)\(1≤k≤14\)\(0≤a_i<2^{14}\)

注意到,区间左右端点指针的每次移动的时间复杂度均为“当前区间长度”,显然无法通过此题。我们来看二次离线莫队的做法。

首先,设 \(tar\) 为一个二进制下有 \(k\)\(1\) 的数。我们定义 \(x⊕y=tar\)\(x\)\(y\) 匹配。另外,若 \(x⊕y=tar\),则 \(y=tar⊕x\)。由于 \(k≤14\)\(a_i<2^{14}\),故所有的 \(tar\) 均可以预处理得出,且总数不到3500个。其个数设为 \(S\)
\(f(i)\) 表示 \(1\)~\(i\) 中与 \(w_{i+1}\) 匹配的数的个数。再利用一个迭代数组 \(g(x)\) 表示迭代过程中与 \(x\) 匹配的数的个数。则可以 \(O(NS)\) 求出:

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]];
}

再来看莫队中区间的增减操作:

  1. 将区间右指针 \(R\) 向右移动到 \(r\)
    \(R\) 移动到 \(R+1\) 时,\(res\) += \([L,R]\)\(w_{R+1}\) 匹配的数的个数。利用前缀和的思想,就是 \([1,R]\)\(w_{R+1}\) 匹配个数 - \([1,L-1]\)\(w_{R+1}\) 匹配个数。前者 = \(f(R)\),且随 \(R\) 向右移动 \(i\) 位,分别为 \(f(R+i)\)。后者分别为 \([1,L-1]\)\(w_{R+i}\) 匹配个数。
    于是得到,\(R\) 右移至 \(r\)\(res\) += $ \sum_{i=R}^{r-1} f(i)$ - \([1,L-1]\)\([r+1,R]\) 匹配个数。将后项记作 \(match([1,L-1],[R+1,r])\)
    此时,前项可随指针右移快速求出,但后项仍无法快速求出。没关系,先看其他情况。
  2. \(R\) 向左移动到 \(r\)
    推法与上相似。\(res\) += \((- \sum_{i=r}^{R-1} f(i))+match([1,L-1],[r+1,R])\)
  3. \(L\) 向左移动到 \(l\)
    \(L\) 移动到 \(L-1\) 时,\(res\) += \([L,R]\)\(w_{L-1}\) 匹配个数。即 \([1,R]\)\(w_{L-1}\) 匹配个数 - \([1,L-1]\)\(w_{L-1}\) 匹配个数。因为 \(w_{L-1}\) 与自己匹配当且仅当 \(k=0\),故后项可化为 \(f(L-2)+!k\)。于是得到,\(L\) 左移至 \(l\)\(res\) += \((- \sum_{i=l-1}^{L-2} (f(i)+!k)) + match([1,R],[l,L-1])\)
  4. \(L\) 向右移动到 \(l\)
    与上相似。\(res\) += \(\sum_{i=L-1}^{l-2} (f(i)+!k) - match([1,R],[L,l-1])\)

于是我们的任务只剩下快速求出所有 \(match\)。如果在移动时在线模拟,那么复杂度很高。但是我们注意到所有 \(match\) 有相似之处,它们都形如 \(match([1,x],[a,b])\)。那么我们是不是可以将它们全部离线下来,扫一边序列的同时,利用迭代数组 \(g\) 统一求出所有 \(match\) 呢?很显然,这样的做法避免了每次求 \(match\) 时重新从头扫描,时间复杂度肯定可以降低。那么我们来分析一下做法。
扫描一遍序列,并实时更新迭代数组 \(g\)。来到第 \(x\) 个数时,找到所有 \(match([1,x],[a,b])\),记录答案为 \(\sum_{i=a}^b g(i)\)。总时间复杂度为 \(O(NS+LEN)\),其中 \(LEN\) 表示所有 \(match\)\(b-a\) 长度之和。我们考虑 \(b-a\) 的实际意义,就是:指针 \(L\)\(R\) 的移动距离之和!在普通莫队中,这个值就是莫队的时间复杂度 \(O(N\sqrt{M})\)。可以承受。
至此,二次离线莫队的算法已经分析完了。因为 \(S\) 较小,故时间复杂度为 \(O(N\sqrt{M})\)

Code

#include
#include
#include
#include
#include
#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 tar;
vector 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

本题要旨在于巧妙的前缀和转化,以及 \(f\)\(g\) 数组的实现。