莫队思想浅谈 莫队,基于分块思想。
所以说,在学习莫队时可以先了解一下分块的优化原理,这对于莫队的理解会有帮助;
我们将分层次讲解,难度不断增加,并附有例题。。。(由于博主太烂懒,所以莫队的模板概念知识只会在这里叙述)
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优化和排序优化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #pragma GCC optimize(3) #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];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 );}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++; while (l>q[i].l) syst(a[l-1 ],1 ),l--; 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 ('\n' ); }
这个代码至少过了这道题,但是仍然很悬,所以尽可能地优化常数
附上树状数组代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #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 ('\n' ); 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块时,移动需要nunit,当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));
那么就可以看代码了。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #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]; 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;}int main () { scan(n),scan(m);unit=pow (n,0.666666 ); 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\n" ,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的路径上有多少个不同的整数。
输出格式 对于每个询问,输出结果。
这是模板测试,只需要分类讨论,直接套莫队;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #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; } 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 ]; } 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]]++; vis[x] ^= 1 ; } int main () { scanf ("%d%d" ,&n,&m);unit=sqrt (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){ q[i].l=first[L]; q[i].r=first[R]; q[i].lca=0 ; }else { 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); ans[q[i].id]=now; if (lca) syst(lca); } for (int i=1 ;i<=m;i++) printf ("%d\n" ,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 );
这里用到了容斥原理,将区间容斥,那么我们就可以引用莫队,维护两个变量,双指针移动,分块排序(博主太唠叨了)
这里附上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #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; } 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 ); while (r<q[i].r) add(++r,1 ); while (r>q[i].r) del(r--,1 ); ans[q[i].id]+=ol*q[i].d; } for (int i=1 ;i<=m;i++) printf ("%d\n" ,ans[i]); }