第十四分块(前体)——二次离线莫队

update on 2020.4.20: 修改了评论大佬指出的一个细节错误(orz),以及调整了一下文章。


题目传送门

正题

二次离线莫队是一个新的科技,可以处理这样的问题:一个序列,m个询问,每次询问l~r间有多少对(x,y)(l<=x<y<=r)满足某条件

而在这道题中,某条件也就是x^y的二进制表示下有k个1

莫队嘛,肯定先要按莫队的套路来,先进行第一次离线,然后问题就出现了,排完序求解的时候,发现左右端点进行一次移动,很难在 O ( 1 ) O(1) O(1) 或是 O ( l o g n ) O(logn) O(logn) 的时间内更新答案,于是就需要用二次离线了。

(为了方便讲解,我们设输入的那个序列为 a [ 1 ] a[1] a[1] ~ a [ n ] a[n] a[n]

以右端点 r i g h t right right 向右移一位为例,这一次移动的贡献是a[right+1] ^ a[left]~a[right] 所能得到的二进制下有k个1的数的个数,发现这个是可以差分的,也就是 a [ r i g h t + 1 ] a[right+1] a[right+1] a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 贡献减去 a [ r i g h t + 1 ] a[right+1] a[right+1] a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left1]的贡献。

于是我们将莫队的 n n n \sqrt n nn 次查询贡献变成了 2 n n 2n\sqrt n 2nn 次查询贡献。

好像更加难做了……

但是,查询变成一个个对前缀的查询后,就可以使用扫描线算法来计算了。这就是算法的核心,因为扫描线是离线算法,所以,称之为二次离线

扫描线具体做法

现在的询问变成了:问 1 1 1 ~ x x x 这个前缀对 y y y 的贡献。于是我们可以开 n n n 个 vector,记录每个前缀对 对应的所有 y y y 的贡献。具体就是每有一个询问,就在第 x x x 个 vector 处 pushback 一个 y y y

根据异或的一个性质( a a a ^ b = c , b b=c,b b=c,b ^ c = a c=a c=a),我们可以开个桶, t o n g [ i ] tong[i] tong[i] 表示当前前缀中有多少个数异或 y y y 得到的数在二进制下有 k k k 1 1 1

然后从前缀 1 1 1 ~ i i i 转移到 1 1 1 ~ i + 1 i+1 i+1 时,只需要将所有 t o n g [ a [ i + 1 ] tong[a[i+1] tong[a[i+1] ^ p ] p] p]加一即可。其中 p p p 是指二进制下有 k k k 1 1 1 的那些数。

发现 p p p 的个数最多只有 C 14 7 = 3432 C_{14}^7 =3432 C147=3432 个,所以扫描线部分的时间复杂度只有 O ( n ∗ 3432 ) O(n*3432) O(n3432),完全可以接受。


看起来所有问题都已经解决了呢,但是

千辛万苦造出来以后发现毒瘤lxl卡了空间
——引自Scarlet

上面的做法有 2 n n 2n \sqrt n 2nn 个询问,但是如果全部存下来的话,算上一些杂七杂八的空间花费,空间复杂度瞬间爆炸。

但是我为了用实践来验证一下真理 (其实只是脑残) ,我去试了一下,然后发现Scarlet大佬所言无半句假话。

证据:MLE记录

于是我们还需要进行对空间的优化……


Here we go!

不妨先把莫队的所有移动的贡献列出来:

  1. 右端点右移:需要 a [ r i g h t + 1 ] a[right+1] a[right+1] a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right]和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left1] 的贡献
  2. 右端点左移:需要 a [ r i g h t ] a[right] a[right] a [ 1 ] a[1] a[1] ~ a [ r i g h t − 1 ] a[right-1] a[right1] 和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left1] 的贡献
  3. 左端点右移:需要 a [ l e f t ] a[left] a[left] a [ 1 ] a[1] a[1] ~ a [ l e f t ] a[left] a[left] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献
  4. 左端点左移:需要 a [ l e f t − 1 ] a[left-1] a[left1] a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left1] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献

但是自己不可能对自己产生贡献,于是我们将上面这个可以稍作改动(荧光部分):

  1. 右端点右移:需要 a [ r i g h t + 1 ] a[right+1] a[right+1] a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left1] 的贡献
  2. 右端点左移:需要 a [ r i g h t ] a[right] a[right] a [ 1 ] a[1] a[1] ~ a [ r i g h t − 1 ] a[right-1] a[right1] 和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left1] 的贡献
  3. 左端点右移:需要 a [ l e f t ] a[left] a[left] a [ 1 ] a[1] a[1] ~ a [ a[ a[ l e f t − 1 left-1 left1 ] ] ] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献
  4. 左端点左移:需要 a [ l e f t − 1 ] a[left-1] a[left1] a [ 1 ] a[1] a[1] ~ a [ a[ a[ l e f t − 2 left-2 left2 ] ] ] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献

仔细观察,发现需要计算的贡献有两大类:

  1. a [ x ] a[x] a[x] a [ 1 ] a[1] a[1] ~ a [ x − 1 ] a[x-1] a[x1] 的贡献
  2. a [ i ] a[i] a[i] a [ 1 ] a[1] a[1] ~ a [ y ] a[y] a[y] 的贡献
对于第一类贡献

第一类贡献只需要一个大小为 n n n 的数组即可存下,并且发现可以用前缀和优化。

对于第二类贡献

莫队每一次是的移动所产生的询问是连续的,比如将 [ l , r ] [l,r] [l,r] 变成 [ l , r + x ] [l,r+x] [l,r+x] 的过程中,产生的两类贡献询问是这个样子的:

询问 a [ r + 1 ] a[r+1] a[r+1] a [ 1 ] a[1] a[1] ~ a [ l − 1 ] a[l-1] a[l1] 的贡献;
询问 a [ r + 2 ] a[r+2] a[r+2] a [ 1 ] a[1] a[1] ~ a [ l − 1 ] a[l-1] a[l1] 的贡献;
询问 a [ r + 3 ] a[r+3] a[r+3] a [ 1 ] a[1] a[1] ~ a [ l − 1 ] a[l-1] a[l1] 的贡献;
……
询问 a [ r + x ] a[r+x] a[r+x] a [ 1 ] a[1] a[1] ~ a [ l − 1 ] a[l-1] a[l1] 的贡献;

也就是说,我们的询问可以合并为:询问 a [ r + 1 ] a[r+1] a[r+1] ~ a [ r + x ] a[r+x] a[r+x] a [ 1 ] a[1] a[1] ~ a [ l − 1 ] a[l-1] a[l1] 的贡献。

这好办,只需要在做扫描线的时候将 t o n g [ a [ r + 1 ] ] tong[a[r+1]] tong[a[r+1]] ~ t o n g [ a [ r + x ] ] tong[a[r+x]] tong[a[r+x]]累加起来即可。

也就是说,对于莫队每次端点的移动(例如 r r r 移动到 r + x r+x r+x)所产生的二类贡献只需要一个变量就可以存下来了。

总结上文

于是,空间复杂度从 O ( n n ∗ O(n \sqrt n * O(nn 玄学常数 ) ) )降到了 O ( m ) O(m) O(m)

perfect~

最后一步,上代码!

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
#define maxn 100010
#define ll long long

int n,m,k;
int a[maxn];
struct node{int l,r,past;ll answer;};
node ask[maxn];
struct vec{
    vector <node>ve;
    int t;
    vec():t(-1){}
    ll get(){return ve[++t].answer;}
}s[maxn];//上面提到的n个vector
int belong[maxn];
void work1()
{
    int sqrtn=sqrt(n),tot=0,tt=1;
    for(int i=1;i<=n;i++)
    {
        tot++;
        if(tot>sqrtn)tot=1,tt++;
        belong[i]=tt;
    }
}
bool cmp1(node x,node y){return belong[x.l]==belong[y.l]?x.r<y.r:belong[x.l]<belong[y.l];}
ll me[maxn];//a[1]~a[x-1] 对a[x]的贡献 

//from和to表示从from转移到to,x表示询问的那个前缀,from+del1和to+del2是询问的那个区间
void move(int &from,int to,int x,int del1,int del2)
{
    s[x].ve.push_back((node){from+del1,to+del2,0,0});
    from=to;
}
int kk[maxn],kt=0;
void work2(int x,int y,int z)//求出二进制下有k个1的数,将他们存在kk中
{
    if(x==14)
    {
        if(z==0)kk[++kt]=y;
        return;
    }
    else
    {
        if(z>0)work2(x+1,y|(1<<x),z-1);
        work2(x+1,y,z);
    }
}
ll tong[maxn];
void SMX()//扫描线
{
    work2(0,0,k);
    for(int i=1;i<=n;i++)
    {
        me[i]=tong[a[i]];//得出一类贡献
        for(int j=1;j<=kt;j++)
        tong[a[i]^kk[j]]++;//更新
        for(int j=0;j<s[i].ve.size();j++)//计算二类贡献
        {
            ll tot=0;
            if(s[i].ve[j].l<s[i].ve[j].r)//注意l是可能大于r的
                for(int pp=s[i].ve[j].l;pp<=s[i].ve[j].r;pp++)
                    tot+=(ll)tong[a[pp]]-(pp<=i?k==0:0);
                    //这里要特判一下当k=0的时候自己是会对自己产生贡献的,要减掉
                    //因为这个我WA掉4个点调了半天555……
            else
                for(int pp=s[i].ve[j].l;pp>=s[i].ve[j].r;pp--)
                    tot+=(ll)tong[a[pp]]-(pp<=i?k==0:0);
            s[i].ve[j].answer=tot;
        }
    }
}
ll sum[maxn];
bool cmp2(node x,node y){return x.past<y.past;};

int main()
{
    scanf("%d %d %d",&n,&m,&k);
    for(int i=1;i<=n;i++)
    scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    scanf("%d %d",&ask[i].l,&ask[i].r),ask[i].past=i;
    work1();//预处理一下分块,后面莫队排序用
    sort(ask+1,ask+m+1,cmp1);//莫队排序
    int left=1,right=0;//先跑一次莫队,把需要所有对贡献的询问存下来
    for(int i=1;i<=m;i++)
    {
//move是在记录二类贡献,建议这里move的参数要理解透,不然看下面第二次跑莫队计算答案时会很难受
        if(right<ask[i].r)move(right,ask[i].r,left-1,1 ,0);
        if(right>ask[i].r)move(right,ask[i].r,left-1,0 ,1);
        if(left<ask[i].l) move(left ,ask[i].l,right ,0,-1);
        if(left>ask[i].l) move(left ,ask[i].l,right ,-1,0);
    }
    SMX();
    left=1;right=0;
    ll ans=0;
    for(int i=1;i<=n;i++)
    sum[i]=sum[i-1]+me[i];
    for(int i=1;i<=m;i++)//计算答案
    {
        if(right<ask[i].r)
        {
            ans+=sum[ask[i].r]-sum[right];
            ans-=s[left-1].get();
        }
        if(right>ask[i].r)
        {
            ans-=sum[right]-sum[ask[i].r];
            ans+=s[left-1].get();
        }
        right=ask[i].r;
        if(left<ask[i].l)
        {
            ans+=sum[ask[i].l-1]-sum[left-1];
            ans-=s[right].get();
        }
        if(left>ask[i].l)
        {
            ans-=sum[left-1]-sum[ask[i].l-1];
            ans+=s[right].get();
        }
        left=ask[i].l;
        if(ask[i].l==ask[i].r)ans=0;
        ask[i].answer=ans;
    }
    sort(ask+1,ask+m+1,cmp2);
    for(int i=1;i<=m;i++)
    printf("%lld\n",ask[i].answer);
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值