• 莫队——基于分块的优雅暴力


    莫队思想浅谈

    莫队,基于分块思想。

    所以说,在学习莫队时可以先了解一下分块的优化原理,这对于莫队的理解会有帮助;

    我们将分层次讲解,难度不断增加,并附有例题。。。(由于博主太懒,所以莫队的模板概念知识只会在这里叙述)

    1.莫队:

      基础的莫队是用来解决区间离线查询问题,利用分块原理和排序,将查询时的重叠部分集中以来优化的算法,大多的算法的复杂度为O(nsqrt(n)),实际更优;

      莫队代码一般很短,且有套路可言,所以应熟练掌握;

      表述完毕,开始讲解:

      P1972 [SDOI2009]HH的项链

    题目描述

    HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答……因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

    输入格式

    第一行:一个整数N,表示项链的长度。

    第二行:N 个整数,表示依次表示项链中贝壳的编号(编号为0 到1000000 之间的整数)。

    第三行:一个整数M,表示HH 询问的个数。

    接下来M 行:每行两个整数,L 和R(1 ≤ L ≤ R ≤ N),表示询问的区间。

    输出格式

    M 行,每行一个整数,依次表示询问对应的答案。

      莫队的一道例题,但是这道题很恶心,数据专门卡了莫队,不过不用担心,我们可以吸口氧,再吸口臭氧,然后再有一些优化就可以A掉(如果实在过不了,后面附上了树状数组代码),然后让我们来讲解;

    设第一个查询的区间为[ l1,r1 ] , 第二个查询的区间为[ l2,r2 ];

    既然已经说莫队时暴力结构,那么就能猜到它的统计方式是暴力统计;

    引入两个指针 l ,r,首先让 l 移动,让l = l1,移动时进行增加或减去操作,像这道题,是颜色个数的减少或增加,那么暴力统计就可以不说了吧;

    在移动到第二个区间时,用同样的操作移动;

    但是这个不就是纯暴力了吗,不知道你是否有这样的疑问;

    但是当我们将区间分块,再将区间按l,r所在块进行排序,那么每次移动的时间复杂度就变成了sqrt(n)(因为我们分成了sqrt(n)个块);

    这样就应该明白其原理,如果真的还是不懂了话,那么可以看看代码;

    Code(带有O2优化,O3优化和排序优化)

    // luogu-judger-enable-o2
    //luogu的O2,NOIP没有 
    #pragma GCC optimize(3)//O3优化,联赛时没有 
    #include<bits/stdc++.h>
    #define maxn 500007
    #define Ri register int//指针优化 
    using namespace std;
    int n,m,be[maxn],a[maxn],unit,col[maxn*10],ans,l=1,r,prin[maxn];
    struct query{int l,r,id;}q[maxn];//query的结构体,便于排序 
    inline bool cmp(query a,query b){
        return be[a.l]^be[b.l]?be[a.l]<be[b.l]:(be[a.l]&1)?a.r<b.r:a.r>b.r;
    }//排序小优化 
    inline void syst(int x,int d){col[x]+=d;if(d>0)ans+=(col[x]==1);if(d<0)ans-=(col[x]==0);}
    //d==1为增加,d==-1为减少 
    template<typename type_of_scan>
    inline void scan(type_of_scan &x){
        type_of_scan f=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
        x*=f;
    }
    template<typename Tops,typename... tops>
    inline void scan(Tops &x,tops&... X){
        scan(x),scan(X...);
    }
    template<typename type_of_print>
    inline void print(type_of_print x){
        if(x<0) putchar('-'),x=-x;
        if(x>9) print(x/10);
        putchar(x%10+'0');
    } 
    
    int main(){
        scan(n);unit=sqrt(n);
        for(Ri i=1;i<=n;i++)
            scan(a[i]),be[i]=i/unit+1;//分块 
        scan(m);
        for(Ri i=1;i<=m;i++)
            scan(q[i].l,q[i].r),q[i].id=i;
        sort(q+1,q+1+m,cmp);//排序 
        for(Ri i=1;i<=m;i++){
            while(l<q[i].l) syst(a[l],-1),l++;//减去l上这个数 
            while(l>q[i].l) syst(a[l-1],1),l--;//加上l-1上这个数 
            while(r<q[i].r) syst(a[r+1],1),r++; 
            while(r>q[i].r) syst(a[r],-1),r--;
            prin[q[i].id]=ans;//记录答案 
        }
        for(Ri i=1;i<=m;i++)
            print(prin[i]),putchar('
    ');
    }

    这个代码至少过了这道题,但是仍然很悬,所以尽可能地优化常数

    附上树状数组代码

    #include<bits/stdc++.h>
    #define maxn 500007
    using namespace std;
    int n,m,tree[maxn<<2],a[maxn],cent,col[maxn*10],pre[maxn*10];
    struct query{int l,r,id,ans;}q[maxn];
    inline bool cmp(query a,query b){return a.r==b.r?a.l<b.l:a.r<b.r;}
    inline bool cmp1(query a,query b){return a.id<b.id;}
    inline int lowbit(int x){return x&-x;}
    inline void add(int x,int d){for(;x<=n;x+=lowbit(x)) tree[x]+=d;}
    inline int ask(int x){int ans=0;while(x) ans+=tree[x],x-=lowbit(x);return ans;}
    
    template<typename type_of_scan>
    inline void scan(type_of_scan &x){
        type_of_scan f=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
        x*=f;
    }
    template<typename Tops,typename... tops>
    inline void scan(Tops &x,tops&... X){
        scan(x),scan(X...);
    }
    template<typename type_of_print>
    inline void print(type_of_print x){
        if(x<0) putchar('-'),x=-x;
        if(x>9) print(x/10);
        putchar(x%10+'0');
    }
    
    int main(){
        scan(n);
        for(int i=1;i<=n;i++)
            scan(a[i]);
        scan(m);
        for(int i=1;i<=m;i++)
            scan(q[i].l,q[i].r),q[i].id=i;
        sort(q+1,q+1+m,cmp);
        for(int i=1;i<=m;i++){
            while(cent<q[i].r){
                add(++cent,1);
                col[a[cent]]++;
                if(col[a[cent]]>1){
                    add(pre[a[cent]],-1);
                    pre[a[cent]]=cent;
                    col[a[cent]]--;
                }else pre[a[cent]]=cent;
            }
            q[i].ans=ask(q[i].r)-ask(q[i].l-1);
        }
        sort(q+1,q+1+m,cmp1);
        for(int i=1;i<=m;i++)
            print(q[i].ans),putchar('
    ');
        return 0;
    }

    这样应该可以去水题了,紫题,不用谢我;

    2.带修莫队:

    例题:P1903 [国家集训队]数颜色 / 维护队列

    题目描述

    墨墨购买了一套N支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:

    1、 Q L R代表询问你从第L支画笔到第R支画笔中共有几种不同颜色的画笔。

    2、 R P Col 把第P支画笔替换为颜色Col。

    为了满足墨墨的要求,你知道你需要干什么了吗?

    输入格式

    第1行两个整数N,M,分别代表初始画笔的数量以及墨墨会做的事情的个数。

    第2行N个整数,分别代表初始画笔排中第i支画笔的颜色。

    第3行到第2+M行,每行分别代表墨墨会做的一件事情,格式见题干部分。

    输出格式

    对于每一个Query的询问,你需要在对应的行中给出一个数字,代表第L支画笔到第R支画笔中共有几种不同颜色的画笔。

      带修莫队其实没什么,只要查询是离线就别虚,只要加入一个时间指针t即可。

      但是如果只是单纯的分块了话,可能时间会超,我们看一下分块块数unit,

      当同l块时,移动需要n*unit,当l块之间r携带移动时需要(n^2)/unit,当时间 t 移动时仍然有 l 和 r 块的移动,所以需要 (n^2*t)/(unit^2)

      时间复杂度时三个移动取最大值(因为省略常数),发现如果unit取sqrt(n)时,最坏时间复杂度为n^2,这不是我们所期望的,

      将t看似为n那么整理第三个为(n^3)/(unit^2),那么让两两相等,三种情况取最优,发现当unit=n^(2/3)时,时间复杂度最优,为O(n^(5/3));

      那么就可以看代码了。。。

      

    #include<bits/stdc++.h>
    #define maxn 50007
    #define Ri register int
    using namespace std;
    int n,m,col[maxn*100],s[maxn],unit,be[maxn],T,l=1,r;
    int cent,t,ans[maxn],Ans,now[maxn];
    struct query{
        int l,r,Time,id;
    }q[maxn];//查询数组 
    struct change{
        int exfo,New,Old;
    }c[maxn];//change数组 
    
    template<typename type_of_scan>
    inline void scan(type_of_scan &x){
        type_of_scan f=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
        x*=f;
    }
    bool cmp(query a,query b){
        if(be[a.l]!=be[b.l]) return a.l<b.l;
        else if(be[a.r]!=be[b.r]) return a.r<b.r;
        return a.Time<b.Time;
    }//不同的排序方式 
    
    inline void syst(int x,int d){col[x]+=d;if(d>0)Ans+=(col[x]==1);if(d<0)Ans-=(col[x]==0);}
    inline void spe(int x,int d){if(l<=x&&x<=r) syst(d,1),syst(s[x],-1);s[x]=d;}//t指针移动方式 
    
    int main(){
        scan(n),scan(m);unit=pow(n,0.666666);//2/3=0.6666666,6越多越精确 
        for(int i=1;i<=n;i++)
            scan(s[i]),now[i]=s[i],be[i]=i/unit+1;
        for(int i=1,x,y;i<=m;i++){
            char type;
            scanf(" %c %d%d",&type,&x,&y);
            if(type=='Q') q[++cent]=(query){x,y,t,cent};
            else if(type=='R') c[++t]=(change){x,y,now[x]},now[x]=y;
        }
        sort(q+1,q+1+cent,cmp);
        for(int i=1;i<=cent;i++){    
            while(T<q[i].Time) spe(c[T+1].exfo,c[T+1].New),T++;
            while(T>q[i].Time) spe(c[T].exfo,c[T].Old),T--;
            while(l<q[i].l) syst(s[l],-1),l++;
            while(l>q[i].l) syst(s[l-1],1),l--;
            while(r<q[i].r) syst(s[r+1],1),r++;
            while(r>q[i].r) syst(s[r],-1),r--;
            ans[q[i].id]=Ans;
        }
        for(int i=1;i<=cent;i++)
            printf("%d
    ",ans[i]);
    }

    3.树上莫队:

    要说树上操作,那么莫队可以操作的有两种类型,第一种是子树统计,第二种是路径统计,那么让我们详细来看;

    警告:前方有dfs序和euler序出现;

    1.子树统计

      这个如果只是一颗子树单纯的统计非常简单,只要一次dfs求出dfs序和子树size即可,你会发现子树上的dfs序时连续的,直接查询一段区间即可;

      

      有点丑,不要介意,它的dfs序是1 2 3 7 9 4 6 5 8

        2的子树序列即为 2 3 7 9;

      这个应该不需要再解释了吧。。。

      但是在两颗子树上统计再加上换根操作是不是很毒瘤,我们在后面会讲到;

    2.路径统计

      莫队只能维护序列,所以我们将子树转化为dfs序,将其从树中拿出,维护其序列,那么路径上怎么办呢

        我们再拿出这个图(我知道很丑!)

        

        不知道euler序?没关系,我解释就行了,euler序将一个数记录两遍,进的时候记录一遍,出的时候记录一遍;

        我们将序列列出来就很清晰了 : 1 2 3 7 7 9 9 3 4 4 2 6 5 5 8 8 6 1;

         那么每个数字都出现了两遍,我们设节点 i 在euler序中第一次出现的位置为first [ i ] ,第二次出现的位置为last [ i ] 。

        观察路径2->9,你会发现路径上first [ 2 ] -> fisrt [ 9 ]:2 3 7 7 9,其中路径上的点都只出现了一次,出现两次的都不在路径上,证明也很简单,这里就不再赘述。

        当然last [ 2 ] -> last [ 9 ]是一样的,只是顺序不一样。

        但是这个只是一种情况——当两个点中有一个点是另一个点子树的一部分,或者问题转换一下一个点是另一个点的LCA,如刚刚的2是9的LCA。

        但是当不在同一颗子树上会发生什么?

        例如3 -> 6:3 7 7 9 9 3 4 4 2 6 5 5 8 8 6,这是3和6出现两次时序列,我们已经发现没有1这个点,我们选取last [ 3 ] -> first [ 6 ]  : 3 4 4 2 6,其中路径上的点3 2 6都出现

        但是1却没有出现,我们可以发现1时LCA(3,6),所以euler序中在这种情况下是没有LCA的,那么我们在统计答案时将其加上,然后再减去。

        两种情况都已经讨论完毕,我们只要在存q数组时做个lca的标记即可。

        这里给出一道  例题  

    题目描述

    给定一个n个节点的树,每个节点表示一个整数,问u到v的路径上有多少个不同的整数。

    输入格式

    第一行有两个整数n和m(n=40000,m=100000)。

    第二行有n个整数。第i个整数表示第i个节点表示的整数。

    在接下来的n-1行中,每行包含两个整数u v,描述一条边(u,v)。

    在接下来的m行中,每一行包含两个整数u v,询问u到v的路径上有多少个不同的整数。

    输出格式

    对于每个询问,输出结果。

         

        这是模板测试,只需要分类讨论,直接套莫队;

    #include<bits/stdc++.h>
    #define maxn 40007
    #define N 100007
    using namespace std;
    int n,m,ls[maxn],euler[maxn<<1],ans[N],cent,cnt,dep[maxn<<1],num[maxn<<1],l=1,r,now;
    int first[maxn<<1],last[maxn<<1],b[maxn],fa[maxn<<1][30],be[maxn<<1],vis[maxn<<1];
    int head[maxn<<1],unit,col[maxn];
    struct query{
        int l,r,id,lca;
    }q[N];
    struct node{
        int next,to;
    }edge[N<<2];
    inline void add(int u,int v){
        edge[++cent]=(node){head[u],v};head[u]=cent;
        edge[++cent]=(node){head[v],u};head[v]=cent;
    }
    
    void dfs(int x){
        euler[++cnt]=x;first[x]=cnt;
        for(int i=head[x];i;i=edge[i].next){
            int y=edge[i].to;
            if(fa[x][0]==y) continue;
            dep[y]+=dep[x]+1;fa[y][0]=x;
            dfs(y);
        }
        euler[++cnt]=x;last[x]=cnt;
    }//在完成LCA初始化的同时完成euler序的记录 
    
    void init(){
        dep[1]=1,fa[1][0]=-1;dfs(1);
        for(int i=1;i<=25;i++){
            for(int k=1;k<=n;k++){
                if(fa[k][i-1]<0) fa[k][i]=-1;
                else fa[k][i]=fa[fa[k][i-1]][i-1];
            }
        }
    }
    
    int getlca(int x,int y){
        if(dep[x]<dep[y]) swap(x,y);
        for(int i=0,d=dep[x]-dep[y];d;d>>=1,i++)
            if(d&1) x=fa[x][i];
        if(x==y) return x;
        for(int i=25;i>=0;i--)
            if(fa[x][i]!=fa[y][i]){
                x=fa[x][i];
                y=fa[y][i];
            }
        return fa[x][0];
    }
    //这里是倍增求LCA,当然也可以2遍dfs+树剖 
    
    inline bool cmp(query a,query b){
        return be[a.l]^be[b.l]?be[a.l]<be[b.l]:(be[a.l]&1?a.r<b.r:a.r>b.r); 
    }//排序小优化
    
    inline void syst(int x){
        vis[x] ? now -= (!--num[ls[x]]):now += !num[ls[x]]++;
        //CASE 1 :前者减去,而!是为了判断是否应该减取(true=1,false=0) 
        //CASE 2 :后者加上,!同上面所说 
        vis[x] ^= 1;//异或操作可以方便的转换出现次数 
    }
    
    int main(){
        scanf("%d%d",&n,&m);unit=sqrt(2*n);//记住euler序是2*n 
        for(int i=1;i<=n;i++)
            scanf("%d",&ls[i]),b[i]=ls[i];
        sort(b+1,b+1+n);
        int tot=unique(b+1,b+1+n)-b-1;
        for(int i=1;i<=n;i++)
            ls[i]=lower_bound(b+1,b+1+tot,ls[i])-b;
        //由于数值太大,我们进行离散化 
        for(int i=1,a,b;i<=n-1;i++)
            scanf("%d%d",&a,&b),add(a,b);
        init();//初始化 
        for(int i=1;i<=cnt;i++) be[i]=i/unit+1;//分块 
        for(int i=1,L,R;i<=m;i++){
            scanf("%d%d",&L,&R);
            int lca=getlca(L,R);q[i].id=i;
            if(first[L]>first[R]) swap(L,R);//小的在前面哦 
            if(lca==L){//CASE 1 : 
                q[i].l=first[L];
                q[i].r=first[R];
                q[i].lca=0;//不需要考虑LCA 
            }else {//CASE 2 : 
                q[i].l=last[L];
                q[i].r=first[R];
                q[i].lca=lca;
            }
        }
        sort(q+1,q+1+m,cmp);
        for(int i=1;i<=m;i++){
            int lca=q[i].lca;
            while(l<q[i].l) syst(euler[l]),l++;
            while(l>q[i].l) syst(euler[l-1]),l--;
            while(r<q[i].r) syst(euler[r+1]),r++;
            while(r>q[i].r) syst(euler[r]),r--;
            if(lca) syst(lca);//判断是否需要考虑LCA 
            ans[q[i].id]=now;
            if(lca) syst(lca);
        }
        for(int i=1;i<=m;i++)
            printf("%d
    ",ans[i]);
        return 0;
    }

    如果您已经AC了这几道题,恭喜您已经大概掌握莫队的框架。(这几道题不是小意思嘛,博主太菜了)

    由于博主太菜,还没有学回滚莫队,所以这里先不再说(逃ε=ε=ε=┏(゜ロ゜;)┛)

    不过这里再留下一道模板题,糖果公园——带修树上莫队,两种算法相结合,不要虚,虽然是黑题

    这里不再附上代码,请原谅。。。


    拓展提升

    4.双指针莫队

      其实这才是莫队的本质,虽然带修莫队是三指针,但是别忘了其时间复杂度还是很难让人接受的(尤其是常数巨大的博主),所以我们还是用双指针的比较多。

      我们以上看到的莫队是一个区间的查询,一个指针维护l,一个指针维护r,然后再用分块排序;

      但是当我们遇见了两个区间怎么办?比如说这道题P5268 [SNOI2017]一个简单的询问,请读者不要被其吓住,这其实只有一个普通莫队的难度,只是需要从原来的思维跳出来

      真正理解莫队双指针的作用。

      设该函数为f( l1 , r1 , l2 , r2),那么可以拆成f( 1  , l1-1 , 1 , l2-1 ) + f ( 1 , r1, 1 , r2 ) - f (1 , l1-1 , 1 , r2) - f ( 1 , l2 - 1 , 1 , r2 );

      这里用到了容斥原理,将区间容斥,那么我们就可以引用莫队,维护两个变量,双指针移动,分块排序(博主太唠叨了)

      这里附上代码:

    #include<bits/stdc++.h>
    #define maxn 500007
    using namespace std;
    int n,m,a[maxn],be[maxn],unit,ans[maxn],ol,all;
    int t[maxn][2];
    struct node{
        int l,r,d,id;
    }q[maxn<<3];
    
    template<typename type_of_scan>
    inline void scan(type_of_scan &x){
        type_of_scan f=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
        x*=f;
    }
    
    bool cmp(node a,node b){
        return be[a.l]==be[b.l]?a.r<b.r:be[a.l]<be[b.l];
    }
    
    inline void add(int x,int type){
        ol+=t[a[x]][type^1];
        t[a[x]][type]++;
    } 
    inline void del(int x,int type){
        ol-=t[a[x]][type^1];
        t[a[x]][type]--;
    }//简单的操作 
    
    int main(){
        scan(n);unit=sqrt(n)+1;
        for(int i=1;i<=n;i++) scan(a[i]),be[i]=(i-1)/unit+1;
        scan(m);
        for(int i=1,l1,r1,l2,r2;i<=m;i++){
            scan(l1),scan(r1),scan(l2),scan(r2);
            q[++all].l=r1,q[all].r=r2,q[all].d=1,q[all].id=i;
            q[++all].l=l2-1,q[all].r=r1,q[all].d=-1,q[all].id=i;
            q[++all].l=l1-1,q[all].r=r2,q[all].d=-1,q[all].id=i;
            q[++all].l=l1-1,q[all].r=l2-1,q[all].d=1,q[all].id=i;
        }//拆成四个询问 ,id一样 
        sort(q+1,q+1+all,cmp);//排序是一样的 
        int l=0,r=0;
        for(int i=1;i<=all;i++){
            while(l<q[i].l) add(++l,0);
            while(l>q[i].l) del(l--,0);//两个指针要分开标记用1和0即可(可以异或) 
            while(r<q[i].r) add(++r,1);
            while(r>q[i].r) del(r--,1);
            ans[q[i].id]+=ol*q[i].d;// 4个询问最后会累加在一起 
        }
        for(int i=1;i<=m;i++) printf("%d
    ",ans[i]);
    }
  • 相关阅读:
    mybatis显示sql语句 log4j.properties配置文件
    Netty 4.0 demo
    Netty多线程处理机制
    crontab定时任务中文乱码问题
    linux grep、find 命令详解
    resin access.log format配置详解
    linux top命令详解
    正则表达式介绍及案例分享
    java定时任务
    rhApp遇到的项目问题
  • 原文地址:https://www.cnblogs.com/waterflower/p/11248733.html
Copyright © 2020-2023  润新知