打打打打打字机|

realFish

园龄:3年1个月粉丝:3关注:0

二次离线莫队学习笔记

二次离线莫队

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

Acwing2535 二次离线莫队为例。
给定一个长度为 n 的序列 a1,a2,,an 以及一个整数 k
现在要进行 m 次询问,每次询问给定一个区间 [l,r],请你求出共有多少个数对 (i,j) 满足 li<jraiaj 的结果在二进制表示下恰好有 k1 表示按位异或操作。
1n,m1051k140ai<214

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

首先,设 tar 为一个二进制下有 k1 的数。我们定义 xy=tarxy 匹配。另外,若 xy=tar,则 y=tarx。由于 k14ai<214,故所有的 tar 均可以预处理得出,且总数不到3500个。其个数设为 S
f(i) 表示 1~i 中与 wi+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]wR+1 匹配的数的个数。利用前缀和的思想,就是 [1,R]wR+1 匹配个数 - [1,L1]wR+1 匹配个数。前者 = f(R),且随 R 向右移动 i 位,分别为 f(R+i)。后者分别为 [1,L1]wR+i 匹配个数。
    于是得到,R 右移至 rres += i=Rr1f(i) - [1,L1][r+1,R] 匹配个数。将后项记作 match([1,L1],[R+1,r])
    此时,前项可随指针右移快速求出,但后项仍无法快速求出。没关系,先看其他情况。
  2. R 向左移动到 r
    推法与上相似。res += (i=rR1f(i))+match([1,L1],[r+1,R])
  3. L 向左移动到 l
    L 移动到 L1 时,res += [L,R]wL1 匹配个数。即 [1,R]wL1 匹配个数 - [1,L1]wL1 匹配个数。因为 wL1 与自己匹配当且仅当 k=0,故后项可化为 f(L2)+!k。于是得到,L 左移至 lres += (i=l1L2(f(i)+!k))+match([1,R],[l,L1])
  4. L 向右移动到 l
    与上相似。res += i=L1l2(f(i)+!k)match([1,R],[L,l1])

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

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

本题要旨在于巧妙的前缀和转化,以及 fg 数组的实现。

本文作者:realFish

本文链接:https://www.cnblogs.com/fish07/p/16097405.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   realFish  阅读(57)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起