当前位置:   article > 正文

莫队详解

莫队详解
莫队实际很简(du)单(liu)
依照某位dalao的说法,就是两只小手(two-pointers)瞎跳

一.莫队(静态莫队)

我们以Luogu P3901 数列找不同为例讲一下静态莫队
这道题是个绿题,因为数据比较弱,但真是一道良心的莫队练手题
莫队是由前国家队队长莫涛发明的
莫队算法的精髓就是通过合理地对询问排序,然后以较优的顺序暴力回答每个询问。处理完一个询问后,可以使用它的信息得到下一个询问区间的答案。(两个小手瞎跳)
考虑这个问题:对于上面这道题,我们知道区间[1,5]每个数的数量,如何求出[2,6]每个数的数量
算法1:暴力扫一遍(废话)
算法2:珂以在区间[1,5]的基础上,去掉位置1(即将左端点右移一位),加上位置6(即将右端点右移一位),得到区间[2,6]的答案。
如果按这样写,一种很简单的构造数据就能把时间复杂度把算法2也送上天:先询问[1,2],再询问[99999,100000],多重复几次就gg
但莫队算法是算法2的改进版
要进行合理的排序,使得每两个区间的距离最小
但如何进行合理的排序?
莫队提供了这样一个排序方案:将原序列以$ \sqrt n$为一块进行分块(分块的大小也珂以调整),排序第一关键字是询问的左端点所在块的编号,第二关键字是询问的右端点本身的位置,都是升序。然后我们用上面提到的“移动当前区间左右端点”的方法,按顺序求每个询问区间的答案,移动每一个询问区间左右端点可以求出下一个区间的答案。
这就是一般的莫队排序
  1. inline bool cmp(register query a,register query b)
  2. {
  3. return a.bl==b.bl?a.r<b.r:a.bl<b.bl;
  4. }
但由于出题人过于毒瘤
又多出一种优化,叫做奇偶优化
按奇偶块排序。这也是比较通用的。如果区间左端点所在块不同,那么就直接按左端点从小到大排;如果相同,奇块按右端点从小到大排,偶块按右端点从大到小排。
  1. inline bool cmp(register query a,register query b)
  2. {
  3. return a.bl!=b.bl?a.l<b.l:((a.bl&1)?a.r<b.r:a.r>b.r);
  4. }
莫队核心代码qaq:
  1. sort(q+1,q+m+1,cmp); //讲询问按上述方法排序
  2. int l=1,r=0; //当前左端点和右端点初值(两只小手two-pointers)
  3. for(register int i=1;i<=m;++i) //对排序后的询问一个个转移
  4. {
  5. int ll=q[i].l,rr=q[i].r; //本次询问的区间
  6. //转移,++--这些东西比较容易写错,需要注意
  7. while(l<ll)
  8. del(l++);
  9. while(l>ll)
  10. add(--l);
  11. while(r<rr)
  12. add(++r);
  13. while(r>rr)
  14. del(r--);
  15. ans[q[i].id]=sth; //询问是排过序的,存到答案数组里需要返回原顺序
  16. }
这样就可以求出答案了!
——可是,这样做的复杂度是什么?
大约是O(nn)
Luogu P3901 AC代码:
  1. #pragma GCC optimize("O3")
  2. #include <bits/stdc++.h>
  3. #define N 100005
  4. using namespace std;
  5. inline int read()
  6. {
  7. register int x=0,f=1;char ch=getchar();
  8. while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  9. while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
  10. return x*f;
  11. }
  12. int v[N],blocksize=0;
  13. struct query{
  14. int l,r,id,bl;
  15. }q[N];
  16. int sum[N];
  17. bool ans[N];
  18. int cnt=0;
  19. inline void add(register int x)
  20. {
  21. if(++sum[v[x]]==1)
  22. ++cnt;
  23. }
  24. inline void del(register int x)
  25. {
  26. if(--sum[v[x]]==0)
  27. --cnt;
  28. }
  29. inline bool cmp(register query a,register query b)
  30. {
  31. return a.bl!=b.bl?a.l<b.l:((a.bl&1)?a.r<b.r:a.r>b.r);
  32. }
  33. int main()
  34. {
  35. memset(sum,0,sizeof(sum));
  36. int n=read(),m=read();
  37. blocksize=sqrt(n);
  38. for(register int i=1;i<=n;++i)
  39. v[i]=read();
  40. for(register int i=1;i<=m;++i)
  41. {
  42. int l=read(),r=read();
  43. q[i]=(query){l,r,i,(l-1)/blocksize+1};
  44. }
  45. sort(q+1,q+m+1,cmp);
  46. int l=1,r=0;
  47. for(register int i=1;i<=m;++i)
  48. {
  49. int ll=q[i].l,rr=q[i].r;
  50. while(l<ll)
  51. del(l++);
  52. while(l>ll)
  53. add(--l);
  54. while(r<rr)
  55. add(++r);
  56. while(r>rr)
  57. del(r--);
  58. ans[q[i].id]=(cnt==rr-ll+1)?1:0;
  59. }
  60. for(register int i=1;i<=m;++i)
  61. if(ans[i])
  62. puts("Yes");
  63. else
  64. puts("No");
  65. return 0;
  66. }

例题:

1.Luogu P3901 数列找不同
讲解比上面暴力
2.Luogu CF375D Tree and Queries
树链剖分(dfs序)后跑莫队
3.Luogu SP3267 DQUERY - D-query
莫队入门题
4.Luogu P1972 [SDOI2009]HH的项链
上面那道题略加卡常
5.Luogu CF86D Powerful array
莫队与小学数学
6.Luogu P1533 可怜的狗狗
莫队+平衡树苟过
7.Luogu P5072 [Ynoi2015]盼君勿忘
题面好评,莫队模板,只是在算贡献时稍微毒瘤
7.Luogu P5071 [Ynoi2015]此时此刻的光辉
题面好评,莫队模板和pollard's Rho的结合

二.动态莫队(单点修改)

写完了上面这道题,可以发现:普通的莫队算法没有支持修改。那么如何改造该算法使它支持修改呢?
莫队俗称优雅的暴力
我们以Luogu P1903 [国家集训队]数颜色 / 维护队列讲解一下动态莫队
那么我们改造莫队算法的思路也只有一个:改造询问排序的方式,然后继续暴力。
首先我们需要把查询操作和修改操作分别记录下来。
在记录查询操作的时候,需要增加一个变量来记录离本次查询最近的修改的位置
然后套上莫队的板子,与普通莫队不一样的是,你需要用一个变量记录当前已经进行了几次修改
每次回答询问时,先从上一个询问的时间“穿越”到当前询问的时间:如果当前询问的时间更靠后,则顺序执行所有修改,直到达到当前询问时间;如果当前询问的时间更靠前,则“时光倒流”,还原所有多余的修改。进行推移时间的操作时,如果涉及到当前区间内的位置的修改,要对答案进行相应的维护。
排序有三个关键字:
1.左端点所在块数 2.右端点所在块数 3.在这次修改之前查询的次数
  1. inline bool cmp(register query a,register query b)
  2. {
  3. return a.bll!=b.bll?a.bll<b.bll:(a.blr!=b.blr?a.blr<b.blr:a.pre<b.pre);
  4. }
完整代码,代码中有详细注释
  1. #pragma GCC optimize("O3")
  2. #include <bits/stdc++.h>
  3. #define N 50005
  4. using namespace std;
  5. inline int read()
  6. {
  7. register int x=0,f=1;register char ch=getchar();
  8. while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  9. while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
  10. return x*f;
  11. }
  12. inline void write(register int x)
  13. {
  14. if(!x)putchar('0');if(x<0)x=-x,putchar('-');
  15. static int sta[25];int tot=0;
  16. while(x)sta[tot++]=x%10,x/=10;
  17. while(tot)putchar(sta[--tot]+48);
  18. }
  19. struct change{ //记录修改操作的结构体,place为修改的位置,pre是修改之前的值,suf是修改之后的值
  20. int place,pre,suf;
  21. }cg[N];
  22. struct query{ //记录查询操作的结构体,l,r为查询左右端点,pre表示之前有过几次修改,id表示这是第几次查询,bll,blr表示左右端点所在的块
  23. int l,r,pre,id,bll,blr;
  24. }q[N];
  25. int a[N],blocksize=0,p[1000001],ans[N];
  26. inline bool cmp(register query a,register query b) //按上述三个关键字排序
  27. {
  28. return a.bll!=b.bll?a.bll<b.bll:(a.blr!=b.blr?a.blr<b.blr:a.pre<b.pre);
  29. }
  30. int main()
  31. {
  32. int n=read(),m=read(),tota=0,totb=0;
  33. for(register int i=1;i<=n;++i)
  34. a[i]=read();
  35. for(register int i=1;i<=m;++i)
  36. {
  37. char ch=getchar();
  38. while(ch!='R'&&ch!='Q')
  39. ch=getchar();
  40. if(ch=='R') //修改
  41. {
  42. cg[++tota].place=read(),cg[tota].suf=read();
  43. cg[tota].pre=a[cg[tota].place]; //为了方便先在原数组上修改
  44. a[cg[tota].place]=cg[tota].suf;
  45. }
  46. else
  47. {
  48. int l=read(),r=read();
  49. q[++totb]=(query){l,r,tota,totb,0};
  50. }
  51. }
  52. blocksize=ceil(exp((log(n)+log(tota))/3)); //奇妙的块的大小
  53. for(register int i=1;i<=totb;++i)
  54. q[i].bll=(q[i].l-1)/blocksize+1,q[i].blr=(q[i].r-1)/blocksize+1;
  55. for(register int i=tota;i>=1;--i) //还原数组
  56. a[cg[i].place]=cg[i].pre;
  57. sort(q+1,q+totb+1,cmp); //排序
  58. int l=1,r=0,num=0,ti=0;
  59. for(register int i=1;i<=m;++i)
  60. {
  61. int ll=q[i].l,rr=q[i].r,t=q[i].pre;
  62. //正常莫队操作
  63. while(ll<l)
  64. num+=!p[a[--l]]++;
  65. while(ll>l)
  66. num-=!--p[a[l++]];
  67. while(rr>r)
  68. num+=!p[a[++r]]++;
  69. while(rr<r)
  70. num-=!--p[a[r--]];
  71. while(t<ti) //当本次查询时修改的次数小于已经修改的次数,时光倒流 (还原修改)
  72. {
  73. int pla=cg[ti].place;
  74. if(l<=pla&&pla<=r)
  75. num-=!--p[a[pla]];
  76. a[pla]=cg[ti--].pre;
  77. if(l<=pla&&pla<=r)
  78. num+=!p[a[pla]]++;
  79. }
  80. while(t>ti) //当本次查询时修改的次数大于已经修改的次数,穿越 (把该修改的修改)
  81. {
  82. int pla=cg[++ti].place;
  83. if(l<=pla&&pla<=r)
  84. num-=!--p[a[pla]];
  85. a[pla]=cg[ti].suf;
  86. if(l<=pla&&pla<=r)
  87. num+=!p[a[pla]]++;
  88. }
  89. ans[q[i].id]=num;
  90. }
  91. for(register int i=1;i<=totb;++i)
  92. {
  93. write(ans[i]);
  94. printf("\n");
  95. }
  96. return 0;
  97. }

三、树上莫队

树上莫队,顾名思义就是把莫队搬到树上。
复杂度同序列上的莫队(不带修:O(nn),带修:O(n53)
我们根据Luogu SP10707 COT2 - Count on a tree II来讲树上莫队
题目意思很明确:给定一个n个节点的树,每个节点表示一个整数,问u到v的路径上有多少个不同的整数。
像这种不带修改数颜色的题首先想到的肯定是树套树莫队,那么如何把在序列上的莫队搬到树上呢?

欧拉序

我们考虑用什么东西可以把树上的问题转化到序列上,dfs序是可以的,但是这道题不行(无法搞lca的贡献)
有一种神奇的东西,叫做欧拉序。
它的核心思想是:当访问到点i时,加入序列,然后访问i的子树,当访问完时,再把i加入序列
煮个栗子,下面这棵树的欧拉序为
1 2 3 4 4 5 5 6 6 3 7 7 2 1

1101696-20180625111030297-903825718.png

有了这个有什么用呢?
我们考虑我们要解决的问题:求x到y的路径上有多少个不同的整数
这里我们设st[i]表示访问到i时加入欧拉序的时间,ed[i]表示回溯经过i时加入欧拉序的时间
不妨设st[x]<st[y](也就是先访问x,再访问y)
分情况讨论
  • 若lca(x,y)=x,这时x,y在一条链上,那么st[x]到st[y]这段区间中,有的点出现了两次,有的点没有出现过,这些点都是对答案没有贡献的,我们只需要统计出现过1次的点就好
    比如当询问为2,6时,(st[2],st[6])=2 3 4 4 5 5 6 4,5这两个点都出现了两次,因此不统计进入答案
  • 若lca(x,y)≠x,此时x,y位于不同的子树内,我们只需要按照上面的方法统计ed[x]到st[y]这段区间内的点。
    比如当询问为4,7时,(ed[4],st[7])=4 5 5 6 6 3 7。大家发现了什么?没错!我们没有统计lca,因此我们需要特判lca

算欧拉序之后可以顺带重链剖分,这样lca就直接树剖来求了qaq

完整代码
  1. #pragma GCC optimize("O3")
  2. #include <bits/stdc++.h>
  3. #define N 40005
  4. #define M 100005
  5. #define getchar nc
  6. using namespace std;
  7. inline char nc(){
  8. static char buf[100000],*p1=buf,*p2=buf;
  9. return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
  10. }
  11. inline int read()
  12. {
  13. register int x=0,f=1;register char ch=getchar();
  14. while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  15. while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
  16. return x*f;
  17. }
  18. inline void write(register int x)
  19. {
  20. if(!x)putchar('0');if(x<0)x=-x,putchar('-');
  21. static int sta[20];register int tot=0;
  22. while(x)sta[tot++]=x%10,x/=10;
  23. while(tot)putchar(sta[--tot]+48);
  24. }
  25. inline void Swap(register int &a,register int &b)
  26. {
  27. a^=b^=a^=b;
  28. }
  29. struct edge{
  30. int to,next;
  31. }e[N<<1];
  32. int head[N],cnt=0;
  33. inline void add(register int u,register int v)
  34. {
  35. e[++cnt]=(edge){v,head[u]};
  36. head[u]=cnt;
  37. }
  38. int n,m;
  39. int a[N],date[N];
  40. int dep[N],top[N],siz[N],son[N],fa[N],st[N],ed[N],pot[N<<1],tot=0;
  41. inline void dfs1(register int x,register int f)
  42. {
  43. fa[x]=f,siz[x]=1,st[x]=++tot;
  44. pot[tot]=x;
  45. for(register int i=head[x];i;i=e[i].next)
  46. if(e[i].to!=f)
  47. {
  48. dep[e[i].to]=dep[x]+1;
  49. dfs1(e[i].to,x);
  50. siz[x]+=siz[e[i].to];
  51. if(siz[e[i].to]>siz[son[x]])
  52. son[x]=e[i].to;
  53. }
  54. ed[x]=++tot;
  55. pot[tot]=x;
  56. }
  57. inline void dfs2(register int x,register int topf)
  58. {
  59. top[x]=topf;
  60. if(son[x])
  61. dfs2(son[x],topf);
  62. for(register int i=head[x];i;i=e[i].next)
  63. if(e[i].to!=fa[x]&&e[i].to!=son[x])
  64. dfs2(e[i].to,e[i].to);
  65. }
  66. inline int Getlca(register int x,register int y)
  67. {
  68. while(top[x]!=top[y])
  69. {
  70. if(dep[top[x]]<dep[top[y]])
  71. Swap(x,y);
  72. x=fa[top[x]];
  73. }
  74. return dep[x]<dep[y]?x:y;
  75. }
  76. struct query{
  77. int l,r,id,lca;
  78. }q[M];
  79. int bel[N<<1],block;
  80. inline bool cmp(register query a,register query b)
  81. {
  82. return bel[a.l]!=bel[b.l]?a.l<b.l:((bel[a.l]&1)?a.r<b.r:a.r>b.r);
  83. }
  84. int ans[M],p[N],vis[N],res=0;
  85. inline void add(register int x)
  86. {
  87. res+=!p[x]++;
  88. }
  89. inline void del(register int x)
  90. {
  91. res-=!--p[x];
  92. }
  93. inline void Add(register int x)
  94. {
  95. vis[x]?del(a[x]):add(a[x]);
  96. vis[x]^=1;
  97. }
  98. int main()
  99. {
  100. n=read(),m=read();
  101. block=sqrt(n);
  102. for(register int i=1;i<=n;++i)
  103. a[i]=date[i]=read();
  104. for(register int i=1;i<=(n<<1);++i)
  105. bel[i]=i/block+1;
  106. sort(date+1,date+1+n);
  107. int num=unique(date+1,date+1+n)-date-1;
  108. for(register int i=1;i<=n;++i)
  109. a[i]=lower_bound(date+1,date+1+num,a[i])-date;
  110. for(register int i=1;i<n;++i)
  111. {
  112. int u=read(),v=read();
  113. add(u,v),add(v,u);
  114. }
  115. dep[1]=1;
  116. dfs1(1,0);
  117. dfs2(1,1);
  118. for(register int i=1;i<=m;++i)
  119. {
  120. int x=read(),y=read();
  121. if(st[x]>st[y])
  122. Swap(x,y);
  123. int lcaa=Getlca(x,y);
  124. q[i].id=i;
  125. if(lcaa==x)
  126. q[i].l=st[x],q[i].r=st[y];
  127. else
  128. q[i].l=ed[x],q[i].r=st[y],q[i].lca=lcaa;
  129. }
  130. sort(q+1,q+1+m,cmp);
  131. int l=1,r=0;
  132. for(register int i=1;i<=m;++i)
  133. {
  134. int ll=q[i].l,rr=q[i].r;
  135. while(l<ll)
  136. Add(pot[l++]);
  137. while(l>ll)
  138. Add(pot[--l]);
  139. while(r<rr)
  140. Add(pot[++r]);
  141. while(r>rr)
  142. Add(pot[r--]);
  143. if(q[i].lca)
  144. Add(q[i].lca);
  145. ans[q[i].id]=res;
  146. if(q[i].lca)
  147. Add(q[i].lca);
  148. }
  149. for(register int i=1;i<=m;++i)
  150. write(ans[i]),puts("");
  151. return 0;
  152. }

四、回滚莫队

1.我们以块编号为第一关键字排序,右端点位置为第二关键字排序
2.询问时依次枚举区间,我们保留右端点的移量(右边单增),左端点则每次在这一个块中来回移动
3.下一个块时,清空统计答案重做
所以对于每一个块:左端点每次操作O(n),右端点总共移n,均摊O(n),因此时间复杂度保证了O(nn)
完整代码Luogu AT1219 歴史の研究
  1. #include <bits/stdc++.h>
  2. #define N 100005
  3. #define ll long long
  4. using namespace std;
  5. inline ll read()
  6. {
  7. register ll x=0,f=1;register char ch=getchar();
  8. while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  9. while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
  10. return x*f;
  11. }
  12. inline void write(register ll x)
  13. {
  14. if(!x)putchar('0');if(x<0)x=-x,putchar('-');
  15. static int sta[36];int tot=0;
  16. while(x)sta[tot++]=x%10,x/=10;
  17. while(tot)putchar(sta[--tot]+48);
  18. }
  19. inline ll Max(register ll a,register ll b)
  20. {
  21. return a>b?a:b;
  22. }
  23. struct query{
  24. int l,r,id,bll,blr;
  25. }q[N];
  26. inline bool cmp(register query a,register query b)
  27. {
  28. return a.bll==b.bll?a.r<b.r:a.bll<b.bll;
  29. }
  30. int n,m,blocksize;
  31. int a[N],v[N];
  32. ll sum[N],num[N],ans[N];
  33. ll pre,now;
  34. inline void add(register int x)
  35. {
  36. sum[x]+=v[x];
  37. now=Max(now,sum[x]);
  38. }
  39. inline void del(register int x)
  40. {
  41. sum[x]-=v[x];
  42. }
  43. int main()
  44. {
  45. n=read(),m=read();
  46. blocksize=sqrt(n);
  47. for(register int i=1;i<=n;++i)
  48. v[i]=a[i]=read();
  49. sort(v+1,v+1+n);
  50. int tot=unique(v+1,v+1+n)-v-1;
  51. for(register int i=1;i<=n;++i)
  52. a[i]=lower_bound(v+1,v+1+tot,a[i])-v;
  53. for(register int i=1;i<=m;++i)
  54. {
  55. int l=read(),r=read();
  56. q[i]=(query){l,r,i,(l-1)/blocksize+1,(r-1)/blocksize+1};
  57. }
  58. sort(q+1,q+1+m,cmp);
  59. int l=1,r=0,pos=0;
  60. for(register int i=1;i<=m;++i)
  61. {
  62. int ql=q[i].l,qr=q[i].r;
  63. if(q[i].bll!=q[i-1].bll)
  64. {
  65. memset(sum,0,sizeof(sum));
  66. pre=now=0;
  67. l=pos=q[i].bll*blocksize+1;
  68. r=l-1;
  69. }
  70. if(q[i].bll==q[i].blr)
  71. {
  72. ll cur=0;
  73. for(register int j=ql;j<=qr;++j)
  74. {
  75. num[a[j]]+=v[a[j]];
  76. cur=Max(cur,num[a[j]]);
  77. }
  78. for(register int j=ql;j<=qr;++j)
  79. num[a[j]]-=v[a[j]];
  80. ans[q[i].id]=cur;
  81. continue;
  82. }
  83. while(r<qr)
  84. add(a[++r]);
  85. pre=now;
  86. while(l>ql)
  87. add(a[--l]);
  88. ans[q[i].id]=now;
  89. while(l<pos)
  90. del(a[l++]);
  91. now=pre;
  92. }
  93. for(register int i=1;i<=m;++i)
  94. write(ans[i]),puts("");
  95. return 0;
  96. }

转载于:https://www.cnblogs.com/yzhang-rp-inf/p/9991671.html

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/434744
推荐阅读
相关标签
  

闽ICP备14008679号