扩大
缩小

【题解】洛谷P4688 [Ynoi2016] 掉进兔子洞

原题链接

一些题外话

这是我做的第一道 Ynoi,做完这题后我深深感受到了 lxl 题目的毒瘤。

做题时曾受神犇 rui_er 题解的启发。

题意

给定一个长为 $n$ 的序列 $a$

有 $m$ 个询问,每次询问三个区间,把三个区间中同时出现的数一个一个删掉,问最后三个区间剩下的数的个数和,询问独立。

注意这里删掉指的是一个一个删,不是把等于这个值的数直接删完,比如三个区间是 $[1,2,2,3,3,3,3]$,$[1,2,2,3,3,3,3]$ 与 $[1,1,2,3,3]$,就一起扔掉了 $1$ 个 $1$,$1$ 个 $2$,$2$ 个 $3$。

数据范围:$1\leq n , m \leq 10^5$,$1 \leq a_i\leq 10^9$,$1\leq l_1,r_1,l_2,r_2,l_3,r_3\leq n$,$l_1\leq r_1$,$l_2\leq r_2$,$l_3\leq r_3$。

时间限制:$3.0$ 秒

空间限制:$512\text{MiB}$

题解

首先 $a_i \le 10^9$,但是我们只在意 $a_i$ 的种类,因此先将其离散化。

显然我们不必同时求出这 $3$ 个区间内的所有数字。我们可以将 $3 \times m$ 个区间顺序打乱,以较快的速度将这堆区间一并求出。

看到这里,应该都知道这时用莫队来解决问题就可以了。至于每个区间的答案,我们可以用 bitset 存储,

我们可以先将三个区间的长度和相加,那么对于每个询问,答案就是三个区间的长度和,减去 $3$ 倍的三个可重集的交集

如果每个子区间内所有元素互不相等,那这很好办,我们把数字变成下标,放入一个 bitset 内,求交集时直接做与运算,时间复杂度是 $\mathcal{O}(\frac{nm}{ω})$

但是实际上元素会重复,我们需要想出一种新的办法,将每个数字映射到 bitset 内,使得每个数字都可能被记录到。

发现相同的元素不收到插入顺序的影响。因此有一种映射的办法,举例如下:

  • 先将元素排序,例如 $[1,2,2,3,3,3,3]$。
  • 记录每个不同元素的初始位置:$1; \ 2; \ 4$。
  • 现在我们记录数字 $1$,此时在 bitset 中就在第 $1$ 个位置标记。
  • 之后记录数字 $2$,此时在 bitset 中就在第 $2$ 个位置标记。
  • 接着记录数字 $3$,此时在 bitset 中就在第 $4$ 个位置标记(注意第 $3$ 个位置是 $2$ 的地盘,不能碰)。
  • 接下来,我们再记一个 $3$。注意到第 $4$ 个位置被占据了,且 bitset 中之前存了 $1$ 次 $3$,那么这次将 $3$ 存入位置 $4+1=5$。
  • 最后如果再插入一个 $3$,因为之前 $3$ 出现了 $2$ 次,所以这次存入位置 $4+2=6$。

流程大概就是这样,莫队算法过程中删除数字的步骤也同理。

我们得到了每个区间的 bitset,就可以拿来取交集了。这个步骤复杂度是有保证的,为 $\frac{n}{ω}$,$ω$ 为 $32$ 或 $64$。

但是还剩了一个问题——记录所有询问的 bitset  需要空间为 $\frac{n \times m}{8}$ 字节,存不下!

这时我们就需要考虑重复利用 bitset 了。观察到不同的询问不会互相干扰,因此我们可以将 $m$ 个询问分成 $S$ 个小组,每小组询问分别跑一次莫队。这里 $S=3$ 或 $4$ 就可以了。

这样就解决了空间不够的问题。假设不考虑分组而浪费的时间,时间复杂度为 $\mathcal{O}(\frac{nm}{ω}+m \sqrt{n})$,$ω$ 为 $32$ 或 $64$。

实现代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 1e9, N = 100010;

struct S{
    int l, r, id, stp;
}s[N];
int n, M, a[N], bb[N], cnt, fir[N], sz[N], siz, SZ, blk[N], ans[N], len[N], vis[N];
bitset <100010> b[20010], now, res;

void add(int x){
    x = a[x];
    now[fir[x] + sz[x]] = 1;
    sz[x]++;
}
void del(int x){
    x = a[x];
    now[fir[x] + sz[x] - 1] = 0;
    sz[x]--;
}

bool cmp(S x, S y){
    if(blk[x.l] == blk[y.l]) return x.r > y.r;
    return blk[x.l] < blk[y.l];
}

void solve(int m){
    for(int i = 1; i <= m; i++){
        scanf("%d%d%d%d%d%d", &s[i * 3 - 2].l, &s[i * 3 - 2].r, &s[i * 3 - 1].l, &s[i * 3 - 1].r, &s[i * 3].l, &s[i * 3].r);
        s[i * 3 - 2].id = i, s[i * 3 - 2].stp = 0;
        s[i * 3 - 1].id = i, s[i * 3 - 1].stp = 1;
        s[i * 3].id = i, s[i * 3].stp = 2;
    }
    now.reset(), siz = (int)sqrt(m * 3);
    for(int i = 1; i <= m * 3; i++) len[i] = vis[i] = 0, blk[i] = (i - 1) / siz + 1;
    for(int i = 1; i <= M; i++) sz[i] = 0;
    
    sort(s + 1, s + m * 3 + 1, cmp);
    
    for(int i = 1, L = 1, R = 0; i <= 3 * m; i++){
        while(L > s[i].l) L--, add(L);
        while(R < s[i].r) R++, add(R);
        while(L < s[i].l) del(L), L++;
        while(R > s[i].r) del(R), R--;
        if(!vis[s[i].id]) b[s[i].id] = now;  // 这里不分开记 3 个区间的 bitset,而是记住初始编号,之后在同一个 bitset 内与运算,节省空间
        else b[s[i].id] &= now;
        vis[s[i].id] = 1;
        len[s[i].id] += s[i].r - s[i].l + 1;
    }
    
    for(int i = 1; i <= m; i++)
        printf("%d\n", len[i] - 3 * b[i].count());
}

int main(){

    int m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]), bb[i] = a[i];
    sort(bb + 1, bb + n + 1);
    for(int i = 1; i <= n; i++)
        if(bb[i] != bb[i - 1])
            fir[++cnt] = i;
    M = unique(bb + 1, bb + n + 1) - bb - 1;
    for(int i = 1; i <= n; i++)
        a[i] = lower_bound(bb + 1, bb + M + 1, a[i]) - bb;
    while(m >= 1)
        solve(m > 20000 ? 20000 : m), m -= 20000;  // 这里最多分 5 组处理

    return 0;
}
posted @ 2021-12-11 00:03  HoshizoraZ  阅读(59)  评论(0编辑  收藏  举报