二次离线莫队

二次离线是个啥?

二次离线莫队

核心

普通莫队在指针移动时,如果单次增加/删除操作的时间复杂度太高,会导致总复杂度退化。

二次离线莫队的核心思想是:将莫队指针移动过程中产生的“贡献更新”拆分为两部分,第一部分用预处理直接 \(O(1)\) 计算,第二部分暂不计算,作为新的询问挂在对应的端点上(第一次离线);等莫队扫描完成后,统一按顺序处理这些挂载的询问(第二次离线)。

具体做法

  1. 第一次离线:不是很正常的跑一遍莫队。
    • 利用前缀和思想,将移动产生的新贡献拆分为:\(\text{f}(x, [1, R]) - \text{f}(x, [1, L-1])\)
    • 第一部分 \(\text{f}(x, [1, R])\) 利用预处理的前缀和数组 \(O(1)\) 实时累加。
    • 第二部分 \(\text{f}(x, [1, L-1])\) 作为区间查询打包存入 vector,挂载在位置 \(L-1\) 上。
  2. 第二次离线:重新扫描
    • 维护支持数据插入的结构(如频次数组)。
    • 遇到挂载在当前位置的询问时,扫描其指定的区间并统计贡献。
  3. 合并答案:将莫队过程中的前缀贡献与第二次离线计算出的增量贡献做前缀和,得到最终答案。

P4887 【模板】莫队二次离线 题解

题意简述

给定一个长度为 \(n\) 的序列 \(a\)\(a_i < 2^{14}\)),有 \(m\) 次询问。每次查询区间 \([l, r]\) 内满足 \(i < j\)\(a_i \oplus a_j\) 的二进制表示中有 \(k\)\(1\) 的数对 \((i, j)\) 的数量。

做法

1. 预处理

  • \(k > 14\),直接特判输出 0
  • 生成所有满足 __builtin_popcount(x) == k\(x < 2^{14}\) 的数,存入 Xor 数组中。
  • 预处理前缀数组 pre1pre2,用于在莫队移动时 \(O(1)\) 获取与当前前缀产生的贡献。

2. 莫队移动(第一次离线)

按照普通莫队排序(带奇偶块优化)。在移动指针时:

  • 移动 \(r\)\(qr\) 时:
    • 第一部分:直接利用前缀和 pre1[qr] - pre1[r] 更新当前莫队答案 sum1[i]
    • 第二部分:将查询区间 \([r+1, qr]\) 挂载到位置 \(l-1\) 下,系数设为 \(-1\)
  • 移动 \(l\)\(ql\) 时同理,利用 pre2 处理前缀部分,剩余区间挂载到 \(r\) 下。

3. 计算答案(第二次离线)

清空计数数组后,从 \(1\)\(n\) 遍历数组:

  1. 将当前 \(a[i]\)Xor 数组中的所有数做异或更新频次表(做 \(n\) 次,每次 \(O(\binom{14}{k})\))。
  2. 遍历挂载在位置 \(i\) 上的所有区间,逐个累加区间内元素的频次(总共扫描 \(O(n\sqrt{m})\) 个元素,单次 \(O(1)\))。

代码实现

戳我喵~
vector<int> Xor;
int a[N], cnt[N];
int n, m, k;
int pos[N], block;

struct Query {
    int l, r, id;
    bool operator <(const Query &x) const {
        return pos[l] == pos[x.l] ? (pos[l] & 1 ? r < x.r : r > x.r) : pos[l] < pos[x.l];
    }
} q[N];

struct node {
    int l, r, id, op;
};

vector<node> g[N];

int sum1[N], sum2[N];
int ans[N];
int lst[N], now[N];
int pre1[N], pre2[N];

signed main() {
    n = re, m = re, k = re;
    for (int i = 1; i <= n; i++) a[i] = re;
    
    // 特判:14位二进制最多只有14个1
    if (k > 14) {
        while (m--) puts("0");
        return 0;
    }

    // 1. 预处理二进制下有 k 个 1 的异或值
    for (int i = 0; i < 16384; i++) {
        if (__builtin_popcount(i) == k) Xor.push_back(i);
    }

    // 2. 预处理前缀贡献
    for (int i = 1; i <= n; i++) {
        lst[i] = cnt[a[i]];
        for (auto x : Xor) cnt[a[i] ^ x]++;
        now[i] = cnt[a[i]];
        pre1[i] = pre1[i - 1] + lst[i];
        pre2[i] = pre2[i - 1] + now[i];
    }

    for (int i = 1; i <= m; i++) q[i] = {re, re, i};
    
    block = max(1ll, (int)(n / sqrt(m)));
    for (int i = 1; i <= n; i++) pos[i] = (i - 1) / block + 1;
    sort(q + 1, q + 1 + m);

    // 3. 莫队移动(第一次离线)
    int l = 1, r = 0;
    for (int i = 1; i <= m; i++) {
        int ql = q[i].l, qr = q[i].r, qid = q[i].id;
        if (l > ql) {
            sum1[i] -= (pre2[l - 1] - pre2[ql - 1]);
            g[r].push_back({ql, l - 1, qid, 1});
            l = ql;
        }
        if (r < qr) {
            sum1[i] += (pre1[qr] - pre1[r]);
            g[l - 1].push_back({r + 1, qr, qid, -1});
            r = qr;
        }
        if (l < ql) {
            sum1[i] += (pre2[ql - 1] - pre2[l - 1]);
            g[r].push_back({l, ql - 1, qid, -1});
            l = ql;
        }
        if (r > qr) {
            sum1[i] -= (pre1[r] - pre1[qr]);
            g[l - 1].push_back({qr + 1, r, qid, 1});
            r = qr;
        }
    }

    // 4. 第二次离线扫描计算
    memset(cnt, 0, sizeof cnt);
    for (int i = 1; i <= n; i++) {
        for (auto x : Xor) cnt[a[i] ^ x]++;
        for (auto x : g[i]) {
            int Ans = 0;
            for (int j = x.l; j <= x.r; j++) Ans += cnt[a[j]];
            sum2[x.id] += x.op * Ans;
        }
    }

    // 5. 还原答案
    int Ans = 0;
    for (int i = 1; i <= m; i++) {
        Ans += sum1[i] + sum2[q[i].id];
        ans[q[i].id] = Ans;
    }

    for (int i = 1; i <= m; i++) wr(ans[i]), endl;

    return 0;
}

时间复杂度:\(O(n \cdot \binom{14}{k} + n\sqrt{m})\)

posted @ 2026-07-24 18:39  OiLight  阅读(3)  评论(0)    收藏  举报