luoguP4887 第十四分块(前体) 莫队的二次离线法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lvzelong2014/article/details/83448152

luoguP4887 第十四分块(前体)

题目传送门

分析

掌握核心莫队科技。
如果说题目中询问的是某个区间中的两两元素子区间这种没有办法在 O ( 1 ) O(1) 统计的答案,有一种黑科技叫做莫队二次离线。
考虑莫队的一次插入与删除。
[ l , r ] > [ l , r + 1 ] [l,r]->[l,r+1]
这个过程中,需要变更的信息是 [ l , r ] a [ r + 1 ] [l,r]\oplus a[r+1]
考虑开桶,也需要 C 14 7 = 3432 C_{14}^7=3432 的复杂度来完成更新。
这个时候发现,变更的信息是可差分的。
D e l t a ( l , r ) = A n s ( [ 1 , r ] a [ r + 1 ] ) A n s ( [ 1 , l 1 ] a [ r + 1 ] ) Delta(l,r)=Ans([1,r]\oplus a[r+1])-Ans([1,l-1]\oplus a[r+1])
不难发现,如果说能够预处理出 [ 1 , x ] [1,x] 区间内的贡献的桶,我们可以在 O ( 1 ) O(1) 的复杂度内处理 A n s ( [ 1 , x ] a k ) Ans([1,x]\oplus a_k) ,而从 [ 1 , x ] > [ 1 , x + 1 ] [1,x]->[1,x+1] ,仅仅需要把 a [ x + 1 ] v i a[x+1]\oplus v_i 塞到桶里面,其中 v i v_i 是所有二进制下恰有 k k 1 1 的数。
所以考虑把莫队的所有 O ( n m ) O(n\sqrt m) 个操作暴力记录。开一个桶扫描线即可做到 O ( 3432 n + n m ) O(3432n+n\sqrt m) ,这就是所谓二次离线
然而出题人卡了空间。
这个时候就要考虑莫队的性质。
对于一个固定的询问 [ s t , e d ] [st,ed] ,莫队仅仅会做两种操作:

  1. 固定 l l ,将 r r 移到 e d ed
  2. 固定 r r ,将 l l 移到 s t st

假设当前的操作是 [ l , r ] > [ l , e d ] [l,r]->[l,ed]
答案就是 x = r + 1 e d D e l t a ( l , x ) = x = r + 1 e d A n s ( [ 1 , x 1 ] a [ x ] ) A n s ( [ 1 , l 1 ] a [ x ] ) \sum_{x=r+1}^{ed}Delta(l,x)=\sum_{x=r+1}^{ed}Ans([1,x-1]\oplus a[x])-Ans([1,l-1]\oplus a[x])
显然 A n s ( [ 1 , x ] a [ x + 1 ] ) Ans([1,x]\oplus a[x+1]) 仅仅和 x x 有关,可以预处理+前缀和。
A n s ( [ 1 , l 1 ] a [ x ] ) Ans([1,l-1]\oplus a[x]) l l 是固定不变的,扫描线的时候可以一起处理区间 [ 1 , l 1 ] [1,l-1] x [ r + 1 , e d ] x\in[r+1,ed] 的贡献。所以开一个邻接表或者 v e c t o r vector 记录下要算的区间即可。具体地,在 l 1 l-1 位置插入 [ r + 1 , e d ] [r+1,ed] 区间,表示要计算 [ r + 1 , e d ] [r+1,ed] 中每个数的贡献,并且标记这个贡献对应的位置。
而其他情况是类似的,讨论一下即可。
这样的话就可以在 O ( m ) O(m) 的空间内完成这道题。

代码

#include<bits/stdc++.h>
const int N = 1e5 + 10;
int ri() {
    char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f  = -1;
    for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int v[N], a[N], pr[N], cnt[N], nx[N << 1], C, tp, n, m, k, B;
long long Ans[N], cur, A[N << 1], s1[N], s2[N];
struct Q {int l, r, id; void in(int i) {l = ri(), r = ri(); id = i;}}q[N], to[N << 1];
void Add(int u, Q a) {to[++C] = a; nx[C] = pr[u]; pr[u] = C;}
bool cmp(Q a, Q b) {
    int x = a.l / B, y = b.l / B;
    return x == y ? (x & 1 ? a.r > b.r : a.r < b.r) : x < y;
}
int main() {
    n = ri(); m = ri(); k = ri(); B = sqrt(n);
    for(int i  = 0, x, c;i < 16384; ++i) {
        for(x = i, c = 0; x; x -= x&-x) ++c;
        if(c == k) v[++tp] = i;
    }
    for(int i = 1;i <= n; ++i) a[i] = ri();
    for(int i = 1;i <= m; ++i) q[i].in(i);
    std::sort(q + 1, q + m + 1, cmp);
    for(int i = 1, l = q[1].r + 1, r = q[1].r;i <= m; ++i) {
        int st = q[i].l, ed = q[i].r, x = q[i].id;
        if(l < st) Add(r, (Q) {l, st - 1, x << 1});
        else if(l > st) Add(r, (Q) {st, l - 1, x << 1});
        l = st;
        if(r < ed) Add(l - 1, (Q) {r + 1, ed, x << 1 | 1});
        else if(r > ed) Add(l - 1, (Q) {ed + 1, r, x << 1 | 1});
        r = ed;
    }
    for(int i = 1;i <= n; ++i) {
        s1[i] = s1[i - 1] + cnt[a[i]];
        for(int j = 1;j <= tp; ++j) ++cnt[a[i] ^ v[j]];
        s2[i] = s2[i - 1] + cnt[a[i]];
        for(int j = pr[i]; j; j = nx[j])
            for(int k = to[j].l;k <= to[j].r; ++k)
                A[to[j].id] += cnt[a[k]];
    }
    for(int i = 1, l = q[1].r + 1, r = q[1].r;i <= m; ++i) {
        int st = q[i].l, ed = q[i].r, x = q[i].id;
        if(l < st) cur += s2[st - 1] - s2[l - 1] - A[x << 1];
        else if(l > st) cur += A[x << 1] - s2[l - 1] + s2[st - 1];
        l = st;
        if(r < ed) cur += s1[ed] - s1[r] - A[x << 1 | 1];
        else if(r > ed) cur += A[x << 1 | 1] - s1[r] + s1[ed];
        r = ed; Ans[x] = cur;
    }
    for(int i = 1;i <= m; ++i) printf("%lld\n", Ans[i]);
    return 0;
}

模型抽取

神仙发明的神仙解法,我在此班门弄斧地解说一波。
二次离线法的问题模型:

  1. 基于莫队算法。
  2. 莫队 [ l , r ] [l,r] 拓展时,会用到 [ l , r ] [l,r] 中若干数对扩展数的贡献,暂且称这个贡献为 A n s ( [ l , r ] &gt; x ) ( x = r + 1 , l 1 , r 1 , l + 1 ) Ans([l,r]-&gt;x)(x=r+1, l-1,r-1,l+1)
  3. 这个贡献可差分,即 A n s ( [ l , r ] &gt; x ) = A n s ( [ 1 , r ] &gt; x ) A n s ( [ 1 , l 1 ] &gt; x ) Ans([l,r]-&gt;x)=Ans([1,r]-&gt;x)-Ans([1,l-1]-&gt;x)

假设采用了某种数据结构(桶,堆,树状数组,块)维护了当前区间 [ l , r ] [l,r] 的信息,单点插入的复杂度为 O ( I ) O(I) ,单点查询的复杂度为 O ( Q ) O(Q)
如果按照普通莫队算法进行分析,复杂度就是 O ( n m ( I + Q ) ) O(n\sqrt m(I+Q))
而二次离线法可以将其优化到 O ( n I + n m Q ) O(nI+n\sqrt m Q)
做法如下:
考虑一个当前区间 [ l , r ] [l,r] ,莫队算法对每一次询问的每次操作等价于固定某一个端点,将另一个端点扩展至目标端点 e d ed ,并计算贡献。(下面均采用固定左端点,挪动右端点的情况,其余情况类似)
也就是
D e l t a ( [ l , r ] &gt; [ l , e d ] ) = x = r e d 1 A n s ( [ l , x ] &gt; x + 1 ) Delta([l,r]-&gt;[l,ed])=\sum_{x=r}^{ed-1} Ans([l,x]-&gt;x+1)

  1. 由差分的性质可以得到, D e l t a ( [ l , r ] &gt; [ l , e d ] ) = x = r e d 1 A n s ( [ 1 , x ] &gt; x + 1 ) A n s ( [ 1 , l 1 ] &gt; x + 1 ) Delta([l,r]-&gt;[l,ed])=\sum_{x=r}^{ed-1}Ans([1,x]-&gt;x+1)-Ans([1,l-1]-&gt;x+1)
  2. 因为 A n s ( [ 1 , x ] &gt; x + 1 ) Ans([1,x]-&gt;x+1) 仅仅与 x x 有关,可以预处理+前缀和求出。
  3. 因为 A n s ( [ 1 , l 1 ] &gt; x + 1 ) Ans([1,l-1]-&gt;x+1) 的左端点不变,相当于固定 [ 1 , l 1 ] [1,l-1] ,每次采用 [ 1 , l 1 ] [1,l-1] 的信息去计算 x [ r + 1 , e d ] x\in[r + 1,ed] 的所有 x x 的贡献,所以将区间 [ r + 1 , e d ] [r+1,ed] 标记至 l 1 l-1 的位置。
  4. 采用扫描线,用数据结构维护出 [ 1 , x ] [1,x] 的信息,再计算被标记到 x x 上所有区间内的数的贡献。 4.采用扫描线,用数据结构维护出 [ 1 , x ] [1,x] 的信息,再计算被标记到 x x 上所有区间内的数的贡献。

由于采用了扫描线,插入的时间复杂度被缩短到了 O ( n I ) O(nI) ,询问照常。考虑每一个莫队上的询问,最多产生两个要插入的区间,所以空间复杂度不变。
采用了这个模型解决这道题就很快了。每个数插入桶的时间是 O ( 3432 ) O(3432) ,询问 O ( 1 ) O(1) ,可以差分,于是就可以采用这个神仙做法。
分块、莫队其实就是各种时间与空间的平衡,果然是一门大学问,学到了学到了。

猜你喜欢

转载自blog.csdn.net/lvzelong2014/article/details/83448152