zoukankan      html  css  js  c++  java
  • 莫队入门

    本文转自大米饼,略加修改

    ·排序巧妙优化复杂度,带来NOIP前的最后一丝宁静。几个活蹦乱跳的指针的跳跃次数,决定着莫队算法的优劣……

    ·目前的题型概括为三种:普通莫队,树形莫队以及带修莫队。

    若谈及入门,那么BZOJ2038的美妙袜子一题堪称顶尖。

    【例题一】袜子

    ·述大意:

         进行区间询问[l,r],输出该区间内随机抽两次抽到相同颜色袜子的概率。

    ·分析:

         首先考虑对于一个长度为n区间内的答案如何求解。题目要求Ans使用最简分数表示:那么分母就是n*n(表示两两袜子之间的随机组合),分子是一个累加和,累加的内容是该区间内每种颜色i出现次数sum[i]的平方。

         将莫队算法抬上议程。莫队算法的思路是,离线情况下对所有的询问进行一个美妙的SORT(),然后两个指针l,r(本题是两个,其他的题可能会更多)不断以看似暴力的方式在区间内跳来跳去,最终输出答案。

         掌握一个思想基础:两个询问之间的状态跳转。如图,当前完成的询问的区间为[a,b],下一个询问的区间为[p,q],现在保存[a,b]区间内的每个颜色出现次数的sum[]数组已经准备好,[a,b]区间询问的答案Ans1已经准备好,怎样用这些条件求出[p,q]区间询问的Ans2?

    image

    考虑指针向左或向右移动一个单位,我们要付出多大的代价才能维护sum[]和Ans(即使得sum[],Ans保存的是当前[l,r]的正确信息)。我们美妙地对图中l,r的向右移动一格进行分析:

                                        image

    如图啦。l指针向右移动一个单位,所造成的后果就是:我们损失了一个绿色方块。那么怎样维护?美妙地,sum[绿色]减去1。那Ans如何维护?先看分母,分母从n2变成(n-1)2,分子中的其他颜色对应的部分是不会变的,绿色却从sum[绿色]2变成(sum[绿色]-1)2 ,为了方便计算我们可以直接向给Ans减去以前该颜色的答案贡献(即sum[绿色]2)再加上现在的答案贡献(即(sum[绿色]-1)2 )。同理,观赏下面的r指针移动,将是差不多的。

                                          image

    ·如图r指针的移动带来的后果是,我们多了一个橙色方块。所以操作和上文相似,只不过是sum[橙色]++。

    ·回归正题地,我们美妙的发现,知道一个区间的信息,要求出旁边区间的信息(旁边区间指的是当前区间的一个指针通过加一减一得到的区间),竟只需要O(1)的时间。

    ·就算是这样,到这里为止的话莫队算法依旧无法焕发其光彩,原因是:如果我们以读入的顺序来枚举每个询问,每个询问到下一个询问时都用上述方法维护信息,那么在你脑海中会浮现出l,r跳来跳去的疯狂景象,疯狂之处在于最坏情况下时间复杂度为:O(n2)————如果要这样玩,那不如写一个暴力程序。

    ·“莫队算法巧妙地将询问离线排序,使得其复杂度无比美妙……”在一般做题时我们时常遇到使用排序来优化枚举时间消耗的例子。莫队的优化基于分块思想:对于两个询问,若在其l在同块,那么将其r作为排序关键字,若l不在同块,就将l作为关键字排序(这就是双关键字)。大米饼使用Be[i]数组表示i所属的块是谁。排序如:

    image

    ·值得强调的是,我们是在对询问进行操作。

    ·时间复杂度分析(分类讨论思想):

    首先,枚举m个答案,就一个m了。设分块大小为unit。
    
    分类讨论:
    
    ①l的移动:若下一个询问与当前询问的l所在的块不同,那么只需要经过最多2*unit步可以使得l成功到达目标.复杂度为:O(m*unit)
    
    ②r的移动:r只有在Be[l]相同时才会有序(其余时候还是疯狂地乱跳,你知道,一提到乱跳,那么每一次最坏就要跳n次!),
    Be[l]什么时候相同?在同一块里面l就Be[]相同。对于每一个块,排序执行了第二关键字:r。所以这里面的r是单调递增的,所以枚举完一个块,r最多移动n次。

    总共有n
    /unit个块:复杂度为:O(n*n/unit) 总结:O(n*unit+n*n/unit)(n,m同级,就统一使用n) 根据基本不等式得:当n为sqrt(n)时,得到莫队算法的真正复杂度: O(n*sqrt(n))

    ·代码上来了(莫队喜欢while):

     1 #include<cmath>
     2 #include<cstdio>
     3 #include<cstring>
     4 #include<iostream>
     5 #include<algorithm>
     6 #define RG register int
     7 #define rep(i,a,b)    for(RG i=a;i<=b;i++)
     8 #define per(i,a,b)    for(RG i=a;i>=b;i--)
     9 #define inf (1<<30)
    10 #define maxn 50005
    11 #define ll long long
    12 #define f(g) (g*g)
    13 using namespace std;
    14 int n,m,len;
    15 int be[maxn],col[maxn];
    16 ll sum[maxn],ans;
    17 struct Dat{
    18     int l,r,id;
    19     ll A,B;
    20 }dat[maxn];
    21  
    22 inline int read()
    23 {
    24     int x=0,f=1;char c=getchar();
    25     while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    26     while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    27     return x*f;
    28 }
    29  
    30 inline bool cmp1(const Dat &a,const Dat &b){return be[a.l]==be[b.l]?a.r<b.r:a.l<b.l;}
    31  
    32 inline bool cmp2(const Dat &a,const Dat &b){return a.id<b.id;}
    33  
    34 inline void add(int p,int val){ans-=f(sum[col[p]]),sum[col[p]]+=val,ans+=f(sum[col[p]]);}
    35  
    36 ll GCD(ll a,ll b){return (!(a%b)?b:GCD(b,a%b));}
    37  
    38 void Mo()
    39 {
    40     int l=1,r=0;
    41     rep(i,1,m)
    42     {
    43         while(l<dat[i].l) add(l++,-1);
    44         while(l>dat[i].l) add(--l,1);
    45         while(r<dat[i].r) add(++r,1);
    46         while(r>dat[i].r) add(r--,-1);
    47         
    48         if(dat[i].l==dat[i].r){dat[i].A=0,dat[i].B=1;continue;}
    49         ll L=dat[i].r-dat[i].l+1ll;
    50         dat[i].A=ans-L,dat[i].B=L*(L-1ll); //bug l/L
    51         ll gcd=GCD(dat[i].A,dat[i].B);
    52         dat[i].A/=gcd,dat[i].B/=gcd;
    53     }
    54 }
    55  
    56 int main()
    57 {
    58     n=read(),m=read(),len=sqrt(n);
    59     rep(i,1,n) col[i]=read(),be[i]=i/len+1;
    60     rep(i,1,m) dat[i].l=read(),dat[i].r=read(),dat[i].id=i;
    61     sort(dat+1,dat+1+m,cmp1);
    62     Mo();
    63     sort(dat+1,dat+1+m,cmp2);
    64     rep(i,1,m)
    65         printf("%lld/%lld
    ",dat[i].A,dat[i].B);
    66     return 0;
    67 }
    View Code

    【例题二】数颜色

    ·述大意:

           多个区间询问,询问[l,r]中颜色的种类数。可以单点修改颜色。

    ·分析:

    莫队可以修改?那不是爆炸了吗。

    这类爆炸的问题被称为带修莫队(可持久化莫队)。

    按照美妙类比思想,可以引入一个“修改时间”,表示当前询问是发生在前Time个修改操作后的。也就是说,在进行莫队算法时,看看当前的询问和时间指针(第三个指针,别忘了l,r)是否相符,然后进行时光倒流或者时光推移操作来保证答案正确性。

    ·Sort的构造。仅靠原来的sort关键字会使得枚举每个询问都可能因为时间指针移动的缘故要移动n次,总共就n2次,那还不如写暴力。

    ·为了防止这样的事情发生,再加入第三关键字Tim:

    image

    ·如何理解时间复杂度?

    首先,R和Tim的关系就像L和R的关系一样:只有在前者处于同块时,后者才会得到排序的恩赐,否则sort会去满足前者,使得后者开始乱跳。

    依旧像上文那样:枚举m个答案,就一个m了。设分块大小为unit。

    分类讨论:

    ①对于l指针,依旧是O(unit*n)

    ②对于r指针,依旧是O(n*n/unit)

    ③对于T指针(即Time):

        类比r时间复杂度的计算。我们要寻找有多少个单调段(一个单调段下来最多移动n次)。上文提到,当且仅当两个询问l在同块,r也在同块时,才会对可怜的Tim进行排序。局势明朗。对于每一个l的块,里面r最坏情况下占据了所有的块,所以最坏情况下:有n/unit个l的块,每个l的块中会有n/unit个r的块,此时,在一个r块里,就会出现有序的Tim。所以Tim的单调段个数为:(n/unit)*(n/unit)。每个单调段最多移动n次。

    所以:O((n/unit)2*n)

    三个指针汇总:O(unit*n+n2/unit+(n/unit)2*n)

    image

    (实际跑出来还是根号n快一点)

    代码:

     1 #include<cmath>
     2 #include<cstdio>
     3 #include<cstring>
     4 #include<iostream>
     5 #include<algorithm>
     6 #define RG register int
     7 #define rep(i,a,b)    for(RG i=a;i<=b;i++)
     8 #define per(i,a,b)    for(RG i=a;i>=b;i--)
     9 #define inf (1<<30)
    10 #define maxn 1000005
    11 #define f(g) (g*g)
    12 using namespace std;
    13 int n,m,na,nb,unit,ans,L,R;
    14 int col[maxn],sum[maxn],last[maxn],be[maxn],ANS[maxn];
    15 struct A{
    16     int l,r,tim,id;
    17 }dat[maxn];
    18 struct B{
    19     int pos,suc,pre;
    20 }upd[maxn];
    21 inline int read()
    22 {
    23     int x=0,f=1;char c=getchar();
    24     while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    25     while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    26     return x*f;
    27 }
    28 
    29 inline int cmp(const A &a,const A &b)
    30 {return be[a.l]==be[b.l]?(be[a.r]==be[b.r]?a.tim<b.tim:a.r<b.r):a.l<b.l;}
    31 
    32 inline void add(int c,int val)
    33 {
    34     sum[c]+=val;
    35     if(val>0&&sum[c]==1)     ans++;
    36     if(val<0&&!sum[c])        ans--;
    37 }
    38 
    39 inline void update(int pos,int c)
    40 {if(L<=pos&&pos<=R)    add(c,1),add(col[pos],-1);col[pos]=c;}
    41 
    42 void Mo()
    43 {
    44     L=1,R=0;RG T=0;
    45     rep(i,1,na)
    46     {
    47         while(T<dat[i].tim)    update(upd[T+1].pos,upd[T+1].suc),++T;
    48         while(T>dat[i].tim)    update(upd[T].pos,upd[T].pre),--T;
    49         
    50         while(L<dat[i].l)    add(col[L++],-1);
    51         while(L>dat[i].l)    add(col[--L],1);
    52         while(R<dat[i].r)    add(col[++R],1);
    53         while(R>dat[i].r)    add(col[R--],-1);
    54         
    55         ANS[dat[i].id]=ans;
    56     }
    57 }
    58 
    59 int main()
    60 {
    61     n=read(),m=read();
    62     unit=sqrt(n;)
    63     rep(i,1,n) col[i]=last[i]=read(),be[i]=i/unit+1;
    64     char opt[10];RG a,b;
    65     rep(i,1,m)
    66     {
    67         scanf("%s",opt),a=read(),b=read();
    68         if(opt[0]=='Q')
    69             dat[++na]=(A){a,b,nb,na};
    70         else
    71             upd[++nb]=(B){a,b,last[a]},last[a]=b;
    72     }
    73     sort(dat+1,dat+1+na,cmp);
    74     Mo();
    75     rep(i,1,na)
    76         printf("%d
    ",ANS[i]);
    77     return 0;
    78 }
    View Code

    【例题三】达到顶尖

    ·述大意:

          一棵树,可以单点修改一个节点的权值,许多询问和修改,询问(u,v)表示u到v的路径上,求出最小的没有出现的自然数。

    ·分析:

    带修莫队+树形莫队。要爆炸了。

    上文解决了爆炸的带修莫队,如何处理树形莫队?

    ·树形莫队引入的第一个难点是:如何分块。注意,分块的目的是为了快速访问与查找(例如上文在分析l指针时间复杂度的时候,发现每次最多移动

    unit*2次,这就是因为即使是跨越了块,这两个块的相邻关系使得时间复杂度不会改变)。

    ·尝试在树上构造相邻的块,使得:块内元素的互相访问的移动次数控制在一个范围内(也就是unit)。做法是用栈维护当前节点作为父节点访问它的子节点,当从栈顶到父节点的距离大于unit时,弹出这部分元素分为一块。

    如图:

    image

    (另外,对于剩余分块的节点,也就是根节点附近由于个数小于unit而形成的一坨点,最后再分一块或加在最后一块中)

    ·强调这样做的好处:使得每一个块内的点到达另一个点最多移动unit次。

    那么对于sort()就和第二题一样了。

    ·接下来还有一个区间移动(即指针u,v,T的移动)没有处理。很明显,这道题的树上路径的维护又是一个美妙的东西。与上几道题不同的是,u,v指针是在树上移动。如果当前路径(u,v)已处理好,下一个询问是到达(u1,v1).那么我们可以将u一步一步的移动到u1,一路上我们欢声笑语,走一个点就记录上面的自然数使用vis[u]标记这个节点来没来过,使用抑或就可以轻松求出访问状态,v到v1也可以这样做。

    image

    另外,维护当前已收集的自然数,可以用离散化+数据结构。(但是这道题好像有BUG,不需要离散化)。给出的代码用的方法是用分块维护,但事后想想,发现树状数组可能更美妙。

    ·这一切都得到解决,就在代码要到来时,你偷看了代码,发现里面有一个函数叫做LCA!什么,哪里要用到倍增求公共祖先?一张图如下:

    image

    ·这样的问题在什么点出现?u1,v1的最近公共祖先。所以,我们上文维护自然数的数据结构(u,v)改成:表示u到v路径上除开他们的LCA的其他点的信息,每次u,v归位后,我们单独为LCA计算一次,这样既避免了怪异情况影响答案,有保证了LCA对答案的贡献。

    ·网上对这种路径问题还有一种本质相同出发点不同的妙解,它也能帮助理解为什么会有怪异情况:求出该树的欧拉序(类似于dfs序,但每个点有头有尾),那么对于(u,v)路径,就是在序列中仅出现一次的数字。这样做同样也要处理公共祖先卡机的怪异情况,画图看看吧。

    ·终于出场的大米饼代码:

     1 #include<stdio.h>
     2 #include<algorithm>
     3 #include<math.h>
     4 #define go(i,a,b) for(int i=a;i<=b;i++)
     5 #define ro(i,a,b) for(int i=a;i>=b;i--)
     6 #define fo(i,a,x) for(int i=a[x],v=e[i].v;i;i=e[i].next,v=e[i].v)
     7 using namespace std;const int N=50009;
     8 struct E{int v,next;}e[N*3];
     9 int k=1,head[N],unit,Be[N],m,st[N],top,fa[N][18],deep[N];
    10 int n,Q,a[N],t[N],op,x,y,p,tim,u=1,v=1,T,ans[N],vis[N];
    11 void ADD(int u,int v){e[k]=(E){v,head[u]};head[u]=k++;}
    12 void dfs(int u){
    13     
    14     go(i,1,19)if((1<<i)>deep[u])break;
    15     else fa[u][i]=fa[fa[u][i-1]][i-1];
    16         
    17     int bottom=top;
    18     fo(i,head,u)if(v!=fa[u][0])
    19     {
    20         fa[v][0]=u;deep[v]=deep[u]+1;dfs(v);
    21         if(top-bottom>=unit){m++;while(top!=bottom)Be[st[top--]]=m;}
    22     }
    23     st[++top]=u;
    24 }
    25 int LCA(int x,int y)
    26 {
    27     if(deep[x]<deep[y])swap(x,y);int Dis=deep[x]-deep[y];
    28     go(i,0,16)if((1<<i)&Dis)x=fa[x][i];
    29     if(x==y)return x;
    30     ro(i,16,0)if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
    31     return x==y?x:fa[x][0];
    32 }
    33 struct Change{int u,New,Old;}cq[N];
    34 struct Query{int u,v,tim,id;bool operator <(const Query &a) const{
    35     return Be[u]==Be[a.u]?(Be[v]==Be[a.v]?tim<a.tim:Be[v]<Be[a.v]):Be[u]<Be[a.u];
    36 }}q[N];
    37 struct Datalock{
    38     struct _blo{int l,r;}b[350];
    39     int n,Be[N],m,unit,num[N],sum[350];
    40     void init()
    41     {
    42         unit=sqrt(n);m=(n-1)/unit+1;
    43         go(i,1,n)Be[i]=(i-1)/unit+1;
    44         go(i,1,m)b[i].l=(i-1)*unit+1,b[i].r=i*unit;
    45         b[m].r=n;
    46     }
    47     void Add(int v){if(v<=n)sum[Be[v]]+=(++num[v])==1;}
    48     void Del(int v){if(v<=n)sum[Be[v]]-=(--num[v])==0;}
    49     int mex()
    50     {
    51         go(i,1,m)if(sum[i]!=b[i].r-b[i].l+1)
    52         go(j,b[i].l,b[i].r)if(!num[j])return j;
    53         return -1;
    54     }
    55 }Data;
    56 void revise(int u,int d){if(vis[u])Data.Del(a[u]),Data.Add(d);a[u]=d;}
    57 void Run(int u){if(vis[u])Data.Del(a[u]),vis[u]=0;else Data.Add(a[u]),vis[u]=1;}
    58 void move(int x,int y)
    59 { 
    60     if(deep[x]<deep[y])swap(x,y);
    61     while(deep[x]>deep[y])Run(x),x=fa[x][0];
    62     while(x!=y)Run(x),Run(y),x=fa[x][0],y=fa[y][0];
    63 }
    64 void Mo()
    65 {
    66     go(i,1,p)
    67     {
    68         while(T<q[i].tim)T++,revise(cq[T].u,cq[T].New);
    69         while(T>q[i].tim)revise(cq[T].u,cq[T].Old),T--;
    70         
    71         if(u!=q[i].u)move(u,q[i].u),u=q[i].u;
    72         if(v!=q[i].v)move(v,q[i].v),v=q[i].v;
    73         int anc=LCA(u,v);Run(anc);ans[q[i].id]=Data.mex()-1;Run(anc);
    74     }
    75 }
    76 int main(){scanf("%d%d",&n,&Q);unit=pow(n,0.45);
    77     go(i,1,n)scanf("%d",&a[i]),t[i]=++a[i];
    78     go(i,2,n){int uu,vv;scanf("%d%d",&uu,&vv);ADD(uu,vv);ADD(vv,uu);}
    79     dfs(1);while(top)Be[st[top--]]=m;
    80     go(i,1,Q)
    81     {
    82         scanf("%d%d%d",&op,&x,&y);
    83         if( op)p++,q[p]=(Query){x,y,tim,p};
    84         if(!op)tim++,cq[tim]=(Change){x,y+1,t[x]},t[x]=y+1;
    85     } 
    86     Data.n=n+1;Data.init();sort(q+1,q+1+p);Mo();
    87     go(i,1,p)printf("%d
    ",ans[i]);
    88 }//Paul_Guderian
    View Code

    [小小总结]

    莫队算法适用条件是比较苛刻的吗?是的。

    ①题目必须离线

    ②能够以极少的时间推出旁边区间(一般是O(1))

    ③没有修改或者修改不太苛刻

    ④基于分块,分块不行,它也好不了哪里去(何况现在还有可持久化数据结构维护的分块)

    但莫队的思想美妙,代码优美,你值得拥有。莫队的排序思想也为众多离线处理的题目提供了完整的思路。

  • 相关阅读:
    2018CodeM复赛
    poj3683
    bzoj3991
    bzoj2809
    bzoj1001
    bzoj1412
    计蒜之道2018复赛
    HDU2255
    bzoj1010
    bzoj2006
  • 原文地址:https://www.cnblogs.com/ibilllee/p/8744161.html
Copyright © 2011-2022 走看看