• 【暖*墟】#数据结构# 莫队算法的学习与练习


    普通莫队的简介

    莫队是一种基于分块思想的离线算法,用于解决区间问题,适用范围如下:

        1. 只有询问没有修改。     2. 允许离线。

        3. 在已知询问 [l,r] 答案的情况下可以 O(1) 得到 [l,r−1],[l,r+1],[l−1,r],[l+1,r] 的答案。

    满足以上三个条件就可以在O(n√m+mlogm)的时间复杂度下得到每个询问的解。

    普通莫队的算法思想

    莫队的精髓就在于:通过对询问进行排序,并把询问的结果作为下一个询问求解的基础

    “在已知询问 [l,r] 答案的情况下可以 O(1) 得到 [l,r−1],[l,r+1],[l−1,r],[l+1,r]的答案”

    • 即:把询问的结果作为下一个询问求解的基础,便于按顺序求值。

    例:【p1494】[国家集训队]小Z的袜子

    • N只袜子从1到N编号,然后从编号L到R中任选两只。
    • 求抽到两只颜色相同的袜子的概率。询问多个(L,R)。
    • 然而数据中有L=R的情况,请特判这种情况,输出0/1。

    用cnt[i]表示当前处理的区间内颜色为i的袜子出现的次数,用len表示当前处理的区间的长度,

    以已知区间[l,r]的答案求解区间[l,r+1]为例,用x表示新增的那只袜子的颜色。

    • 分别处理分子和分母:

    分母 为任选两只袜子的组合总数,原先是len∗(len−1)/2,

       现在是len∗(len+1)/2,增加了len。

    分子 为两只袜子颜色相同的组合总数,比原来增加了cnt[x],

        即:新增的这只袜子和原区间内的同色袜子的组合(出现次数)。

    //将一只颜色为x的袜子加入区间:(fz代表分子,fm代表分母)
    
    void add(int x){ fm+=len,len++,fz+=cnt[x],cnt[x]++; }
    
    
    //将一只颜色为x的袜子移出区间:(fz代表分子,fm代表分母)
    
    void del(int x){ len--,fm-=len,cnt[x]--,fz-=cnt[x]; }

    莫队的具体实现方案

    用 l、r 分别记录当前区间的两个端点,用下方代码来更新答案:

    while(l>q[i].l) add(col[--l]);
    
    while(r<q[i].r) add(col[++r]);
    
    while(l<q[i].l) del(col[l++]);
    
    while(r>q[i].r) del(col[r--]);
    
    //↑↑q[i].l/r即正在处理的询问区间的两个端点,col[p]即第p只袜子的颜色

    而莫队的精髓就在于通过对询问进行‘排序’,使得暴力求解的复杂度得到保证。

    为了让l、r两个指针走过的总距离尽量小,就要运用分块的思想了。

    • 把整个区间[1,n]分成若干块,以询问的‘左端点所在块’为第一关键字,
    • 以询问的‘右端点’为第二关键字,对询问进行排序,那么:

    对于同一块的询问,l指针每次最多移动块的大小,r指针最多移动n次。

    对于不同块的询问,l每次换块时最多移动两倍块的大小,r每次换块时最多移动n次。

    总结:(用B表示块的大小)l指针每次移动O(B),r指针每块移动O(n)。

            所以l的移动次数最多为询问数×块的大小,即O(mB)。

            r的移动次数最多为块的个数×总区间大小,即O(n^2/B)。

                 因此,总移动次数为O(mB+n^2/B)。

    初始的当前区间(任意空区间均可): l=1,r=0 。

    整个莫队算法可以概括为:

    1.记录询问; 2.以n/√m为块的大小,按规则对询问进行排序(分块只用于排序);

    3.双指针暴力处理每个询问; 4.总的复杂度为 O(n√m+mlogm)。

    普通莫队的各种例题

    例1:【p1494】小Z的袜子

    • N只袜子从1到N编号,然后从编号L到R中任选两只。
    • 求抽到两只颜色相同的袜子的概率。询问多个(L,R)。
    • 然而数据中有L=R的情况,请特判这种情况,输出0/1。
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<vector>
    #include<cmath>
    #include<map>
    using namespace std;
    typedef long long ll;
    
    // 【p1494】 小Z的袜子 
    // N只袜子从1到N编号,然后从编号L到R中任选两只。
    // 求抽到两只颜色相同的袜子的概率。询问多个(L,R)。
    // 然而数据中有L=R的情况,请特判这种情况,输出0/1。
    
    void reads(int &x){ //读入优化(正负整数)
        int fx=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fx; //正负号
    }
    
    const int N=50019;
    
    int n,m,pos[N],col[N]; ll cnt[N],ans;
    
    ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);}
    
    ll sqr(ll x){return x*x;}
    
    struct node{int l,r,id;ll fz,fm;}q[N];
    
    bool cmp(node a,node b)
     { if(pos[a.l]==pos[b.l]) return a.r<b.r; return a.l<b.l; }
    
    bool cmp_id(node a,node b){return a.id<b.id;}
    
    void update(int p,int add)
     { ans-=sqr(cnt[col[p]]),cnt[col[p]]+=add,ans+=sqr(cnt[col[p]]); }
    
    void solve(){
        for(int i=1,l=1,r=0;i<=m;i++){
            for(;r<q[i].r;r++) update(r+1,1);
            for(;r>q[i].r;r--) update(r,-1);
            for(;l<q[i].l;l++) update(l,-1);
            for(;l>q[i].l;l--) update(l-1,1);
            if(q[i].l==q[i].r){ q[i].fm=0,q[i].fz=1; continue; }
            q[i].fm=ans-(q[i].r-q[i].l+1);
            q[i].fz=(ll)(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
            ll gcds_=gcd(q[i].fm,q[i].fz);
            q[i].fm/=gcds_,q[i].fz/=gcds_; //分数约分
        }
    }
    
    int main(){
        reads(n),reads(m); for(int i=1;i<=n;i++) reads(col[i]);
        int block=(int)sqrt(n); for(int i=1;i<=n;i++) pos[i]=(i-1)/block+1;
        for(int i=1;i<=m;i++) reads(q[i].l),reads(q[i].r),q[i].id=i;
        sort(q+1,q+m+1,cmp); solve(); //离线,进行莫队算法
        sort(q+1,q+m+1,cmp_id); for(int i=1;i<=m;i++)
            printf("%lld/%lld
    ",q[i].fm,q[i].fz);
    }

    例2:【p2709】小B的询问

    • 查询区间(l,r)中的∑(cnt[i]^2),cnt[i]表示i在区间内出现的次数。
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<vector>
    #include<cmath>
    #include<map>
    using namespace std;
    typedef long long ll;
    
    // 【p2709】小B的询问
    // 查询区间(l,r)中的∑(cnt[i]^2),cnt[i]表示i在区间内出现的次数。
    
    void reads(ll &x){ //读入优化(正负整数)
        ll fx=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fx; //正负号
    }
    
    const ll N=50019;
    
    ll n,m,k,pos[N],col[N]; ll cnt[N],ans=0;
    struct node{ll l,r,id;ll sum;}q[N];
    
    bool cmp(node a,node b)
     { if(pos[a.l]==pos[b.l]) return a.r<b.r; return a.l<b.l; }
    
    bool cmp_id(node a,node b){return a.id<b.id;}
    
    void update(ll p,ll add) //↓↓相邻数的平方差
     { cnt[col[p]]+=add,ans+=add*(2*cnt[col[p]]-add); }
    
    void solve(){
        for(ll i=1,l=1,r=0;i<=m;i++){
            for(;r<q[i].r;r++) update(r+1,1);
            for(;r>q[i].r;r--) update(r,-1);
            for(;l<q[i].l;l++) update(l,-1);
            for(;l>q[i].l;l--) update(l-1,1);
            q[i].sum=ans; //记录此区间的sum值
        }
    }
    
    int main(){
        reads(n),reads(m),reads(k); for(ll i=1;i<=n;i++) reads(col[i]);
        ll block=(ll)sqrt(n); for(ll i=1;i<=n;i++) pos[i]=(i-1)/block+1;
        for(ll i=1;i<=m;i++) reads(q[i].l),reads(q[i].r),q[i].id=i;
        sort(q+1,q+m+1,cmp); solve(); //离线,进行莫队算法
        sort(q+1,q+m+1,cmp_id); for(ll i=1;i<=m;i++) cout<<q[i].sum<<endl;
    }

     例3:【p3245】大数

    • 一个数字串,m个询问,每次询问串[l,r]中 “ %P=0的子串个数 ” 。
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<vector>
    #include<cmath>
    #include<map>
    using namespace std;
    typedef long long ll;
    
    // 【p3245】大数
    // 一个数字串,m个询问,每次询问串[l,r]中‘%P=0的子串个数’ 。
    
    /*【莫队】存数字串的后缀:rs[i]表示i到n形成的数%p​的值。
    那么数字[l,r]=num(l,r)≡ rs[l]−rs[r+1] / 10^(r−l+1) (mod p)​。
    当10^(r−l+1)%p≠0时:num(l,r)∗10^(r−l+1)≡rs[l]−rs[r+1](mod p)。
    所以如果rs[l]=rs[r+1],num(l,r)就是p的倍数,条件是gcd(10,p)=1,p不能是2或5。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fx=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fx; //正负号
    }
    
    const int N=500019;
    
    int p,n,m,pos[N],a[N],rs[N],mi[N]; ll cnt[N],sum=0,cntt=0,l,r;struct number{ int z,id; }b[N];
    struct node{int l,r,id;ll ans;}q[N];
    
    bool cmp(node a,node b) //【分块优化排序】同块中按r排序,不同块时按l排序
     { if(pos[a.l]==pos[b.l]) return a.r<b.r; return a.l<b.l; }
    
    bool cmp2(number a,number b){return a.z<b.z;}
    
    bool cmp_id(node a,node b){return a.id<b.id;}
    
    inline void addl(int x){cntt+=(a[x]%p==0),sum+=cntt;}
    inline void addr(int x){sum+=(r-l+1)*(a[x]%p==0),cntt+=(a[x]%p==0);}
    inline void dell(int x){sum-=cntt,cntt-=(a[x]%p==0);}
    inline void delr(int x){sum-=(r-l+1)*(a[x]%p==0),cntt-=(a[x]%p==0);}
    
    inline void add(int x){sum+=cnt[rs[x]],cnt[rs[x]]++;}
    inline void del(int x){cnt[rs[x]]--,sum-=cnt[rs[x]];}
    
    void get_25(){
        l=1; for(int i=1;i<=m;i++){
            while(l<q[i].l) dell(l),l++;
            while(l>q[i].l) l--,addl(l);
            while(r>q[i].r) delr(r),r--;
            while(r<q[i].r) r++,addr(r); q[i].ans=sum;
        }
    }
    
    void solve(){
        l=1; for(int i=1;i<=m;i++){
            while(l<q[i].l) del(l),l++;
            while(l>q[i].l) l--,add(l);
            while(r>q[i].r+1) del(r),r--;
            while(r<q[i].r+1) r++,add(r);q[i].ans=sum;
        }
    }
    
    char ss[100019];
    
    int main(){
        reads(p),scanf("%s",ss+1); n=strlen(ss+1);
        int block=(int)sqrt(n); for(int i=1;i<=n;i++) 
            a[i]=ss[i]-'0',pos[i]=(i-1)/block+1;
    
        mi[1]=1; for(int i=2;i<=n;i++) mi[i]=mi[i-1]*10%p;
        if(p!=2&&p!=5){ for(int i=n;i>=1;i--){
                b[i].z=(b[i+1].z+a[i]*mi[n-i+1]%p)%p,b[i].id=i;
            } b[n+1].id=n+1; b[n+1].z=0; //z记录后缀%p得到的值(未离散化)
            sort(b+1,b+n+2,cmp2); //排序,这样就可以将rs数组离散化
            for(int i=1;i<=n+1;i++) rs[b[i].id]=rs[b[i-1].id]+(b[i].z!=b[i-1].z);
        } //↑↑计算后缀数%p之后得到的值(rs[]),离散化rs数组
        
        reads(m); for(int i=1;i<=m;i++) reads(q[i].l),reads(q[i].r),q[i].id=i;
        sort(q+1,q+m+1,cmp); if(p!=2&&p!=5) solve(); //离线,进行莫队算法
        else get_25(); //特殊情况:重开一个莫队处理2和5的情况
        sort(q+1,q+m+1,cmp_id); for(int i=1;i<=m;i++) printf("%lld
    ",q[i].ans);
    }

     例4:【p4396】作业

    • 长度为n的数列和m个询问,统计区间[l,r]中 满足a<=x<=b的x 的 个数 和 种类数 。
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<vector>
    #include<cmath>
    #include<map>
    using namespace std;
    typedef long long ll;
    
    // 【p4396】作业 
    // 长度为n的数列和m个询问,统计区间[l,r]中‘满足a<=x<=b的x’的 个数 和 种类数 。
    
    //莫队+分块的做法肯定有问题...但我感觉数据就是这样设计的(输入的max值<n)
    //因为出题人想设计的是[莫队+分块]做法,但没有说清楚↑这一点
    //所以 if(pos[l]!=0&&pos[r]==0) for(int i=pos[l]+1;i<=num;i++) 
    //        ans1[id]+=skuai[i]... 这种全盘皆加的处理方法也是能过的
    
    const int N=100010;
    
    int n,m,L,R,block,num,pos[N],lll[N],rrr[N],cnt[N];
    
    int skuai[N],vary[N],col[N],ans1[N],ans2[N]; //skuai[]即每个块的大小
    
    struct node{ int l,r,x,y,id; }a[N];
    
    bool cmp(node aa,node bb) //分块优化莫队
     { if(pos[aa.l]==pos[bb.l]) return aa.r<bb.r; return aa.l<bb.l; }
    
    void build(){ //预处理出每个块的边界
        block=sqrt(n); num=n/block; if(n%block) num++; //分块其实维护的是具体数字
        for(int i=1;i<=num;i++) lll[i]=(i-1)*block+1,rrr[i]=i*block;
        rrr[num]=n; for(int i=1;i<=n;i++) pos[i]=(i-1)/block+1; }
    
    void add(int col_){ cnt[col_]++; skuai[pos[col_]]++;
        if(cnt[col_]==1) vary[pos[col_]]++; } //此块中数的种类++
    
    void del(int col_){ cnt[col_]--; skuai[pos[col_]]--;
        if(cnt[col_]==0) vary[pos[col_]]--; } //减完了,此块种类--
    
    void find(int l,int r,int id){ //分块查找具体数字[l,r]
        if(pos[l]==pos[r]) //同一块中,直接+每个数的出现次数
         { for(int i=l;i<=r;i++) if(cnt[i]) ans1[id]+=cnt[i],ans2[id]++; return; }
        if(pos[l]!=0) for(int i=l;i<=rrr[pos[l]];i++) //零散块暴力维护
            if(cnt[i]) ans1[id]+=cnt[i],ans2[id]++;
        if(pos[r]!=0) for(int i=lll[pos[r]];i<=r;i++) //零散块暴力维护 
            if(cnt[i]) ans1[id]+=cnt[i],ans2[id]++;
        if(pos[l]!=0&&pos[r]!=0) //维护整块
            for(int i=pos[l]+1;i<pos[r];i++) ans1[id]+=skuai[i],ans2[id]+=vary[i];
        if(pos[l]!=0&&pos[r]==0) //r>n,直接++每个块的大小(所有数)
        // [这里的意思就是输入的每个数<n,这题有解释不清楚的地方......]
            for(int i=pos[l]+1;i<=num;i++) ans1[id]+=skuai[i],ans2[id]+=vary[i]; 
        //每块中维护的种类数都是按顺序的,比如[1,2,3,4][5,6,7,8]...所以vary不会重复
    }
    
    int main(){
        scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&col[i]);
        for(int i=1;i<=m;i++) scanf("%d%d%d%d",&a[i].l,&a[i].r,&a[i].x,&a[i].y),a[i].id=i;
        build(); sort(a+1,a+1+m,cmp); L=1,R=0; //分块并将询问排序
        for(int i=1;i<=m;i++){
            while(R<a[i].r) add(col[++R]); while(R>a[i].r) del(col[R--]);
            while(L<a[i].l) del(col[L++]); while(L>a[i].l) add(col[--L]);
            find(a[i].x,a[i].y,a[i].id); //在数值(x,y)的块中
            //寻找,当前区间中出现的数x<=i<=y(用cnt[i]记录次数)
        } for(int i=1;i<=m;i++) printf("%d %d
    ",ans1[i],ans2[i]);
    }

     例5:【p4462】异或序列

    • 求区间中,子段异或和=k的有多少组。
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<vector>
    #include<cmath>
    #include<map>
    using namespace std;
    typedef long long ll;
    
    //【p4462】异或序列 //求区间中,子段异或和=k的有多少组
    
    // 把每个位置的数值记成异或前缀和的形式,问题变成:
    // 区间[l−1,r]中,有多少对数异或为k。用cnt[]记录异或值,莫队。
    
    const int N=100019; struct node{ int l,r,id; }q[N];
    
    int n,m,k,block,sum=0,pos[N],cnt[N],a[N],ans[N];
    
    bool cmp(node aa,node bb) //分块优化莫队
     { if(pos[aa.l]==pos[bb.l]) return aa.r<bb.r; return aa.l<bb.l; }
    
    void add(int x){ sum+=cnt[a[x]^k],cnt[a[x]]++; }
    
    void del(int x){ cnt[a[x]]--,sum-=cnt[a[x]^k]; }
    
    int main(){
        scanf("%d%d%d",&n,&m,&k); block=(int)sqrt(n); // a[] 变成储存前缀的异或序列↓↓
        for(int i=1;i<=n;i++) scanf("%d",&a[i]),pos[i]=(i-1)/block+1,a[i]=a[i-1]^a[i];
        for(int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].l--,q[i].id=i; //注意l--
        sort(q+1,q+m+1,cmp); for(int i=1,L=1,R=0;i<=m;i++){ //【莫队】 
            while(R<q[i].r) add(++R); while(R>q[i].r) del(R--);
            while(L<q[i].l) del(L++); while(L>q[i].l) add(--L); ans[q[i].id]=sum;
        } for(int i=1;i<=m;i++) printf("%d
    ",ans[i]);
    }

                                             ——时间划过风的轨迹,那个少年,还在等你

  • 相关阅读:
    Ynoi 杂题选做
    CSP-S2021 浙江 游记
    P6272 [湖北省队互测2014]没有人的算术
    P5206 [WC2019]数树
    P5405 [CTS2019]氪金手游
    LOJ6679 Unknow
    LOJ3040 「JOISC 2019 Day4」合并
    P6122 [NEERC2016]Mole Tunnels
    三维凸包
    三,四元环计数
  • 原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/10428197.html
Copyright © 2020-2023  润新知