最近刚刚学习了莫队算法,感觉很神奇,很强大,于是我打算写一些小小的总结,希望能对看的人有一点点帮助。ps:noip前我是不会学新算法啦qwq...老师让我多刷刷题连连代码能力
对于莫队算法,我们可以简单的将其分为几类,普通莫队,带修莫队,树上莫队(应该没别的了吧qwq我只会这三个)。
如果我哪里说错了欢迎各位dalao指出!!!
概述:莫队算法是用于解决什么问题呢?
给出莫队应用范围:对于一个序列在已知f[l,r]的情况下可以O1或者Ologn的复杂度求出f[l,r+1],f[l,r-1],f[l+1,r],f[l-1,r],并且题目不强制在线,那么我们就可以用莫队算法给出一个满意的复杂度。
那莫队的整体思想是什么呢?
其实是将题目中所给的询问记录下来,然后通过某种排序,使他们询问顺序发生改变,然后通过暴力移动左右两个边界(l,r)来处理处每一个询问的值,而如何排序,就需要用到一点分块的思想,来将其最坏情况复杂度变为n根号n。
一、普通莫队
这里先给出例题和代码,以及思路,后面将会给出详细的证明。
举个简单的例子,我现在有若干个物品,每一个物品对应一个颜色,给出若干个询问,请你给出对于每一个询问区间,它里面有多少种颜色。
[SDOI2009]HH的项链
这是一道可以用莫队解决的题目中最简单的一道了吧,他们好像都是用树状数组搞得,我也没往那想,单纯为了练莫队。
先贴上代码
1 #include<bits/stdc++.h> 2 using namespace std; 3 int n,a[50005],m,pos[50005],f[50005],ans; 4 struct Ques{ 5 int l,r,ans,id; 6 bool operator < (const Ques &o)const{ 7 if(pos[l]!=pos[o.l])return pos[l]<pos[o.l]; 8 return r<o.r; 9 } 10 }q[50005]; 11 bool cmp(Ques x,Ques y){ 12 return x.id<y.id; 13 } 14 int main() 15 { 16 // freopen("1.txt","r",stdin); 17 scanf("%d",&n); 18 for(int i=1;i<=n;i++)scanf("%d",&a[i]); 19 scanf("%d",&m); 20 for(int i=1;i<=m;i++){ 21 scanf("%d%d",&q[i].l,&q[i].r); 22 q[i].id=i; 23 } 24 int len=sqrt(n)+1; 25 for(int i=1;i<=n;i++)pos[i]=(i-1)/len+1; 26 sort(q+1,q+m+1); 27 int l=1,r=0; 28 for(int i=1;i<=m;i++){ 29 while(r<q[i].r){ 30 r++; 31 f[a[r]]++; 32 if(f[a[r]]==1)ans++; 33 } 34 while(r>q[i].r){ 35 f[a[r]]--; 36 if(!f[a[r]])ans--; 37 r--; 38 } 39 while(l>q[i].l){ 40 l--; 41 f[a[l]]++; 42 if(f[a[l]]==1)ans++; 43 } 44 while(l<q[i].l){ 45 f[a[l]]--; 46 if(!f[a[l]])ans--; 47 l++; 48 } 49 q[i].ans=ans; 50 } 51 sort(q+1,q+m+1,cmp); 52 for(int i=1;i<=m;i++)printf("%d\n",q[i].ans); 53 return 0; 54 }
对于这道题,我们的思路是什么呢?先想一想最暴力的算法如何解决,对于每一次询问,我们从l枚举到r,并记录颜色数量,这样复杂度最坏情况下是nm的,显然不能令人满意,但如果我们可以将其将为n根号n,那么他就可以被完美的解决。
我们可以发现这道题是满足莫队的应用范围的,因为如果我知道l到r的颜色总数,那么我可以在O1的时间内求出[l,r+1]....等等,而这道题并没有强制在线。
那么莫队的具体实现是什么呢?
首先,我们对这个序列进行分块,设块的大小为n^w。然后我们以每次询问左端点所在的块为第一关键字,以右端点的下标为第二关键字,对所有的询问进行排序。然后从第一个询问开始,初始l=1,r=0,ans=0。(如果是用我这种写法,那么l必须附成1,因为如果不附成1,在l右移的时候会多处理了一项,也就是0那一项。)然后每次移动l和r并更新ans,每次移动的复杂度是k=abs(li-l)+abs(ri-r),那么我们就需要使所有的k加起来最小,当我们对其按块排序之后,我们可以对其复杂度进行以下的分析
1.l所处的块相同,r的移动复杂度,因为我们r是以下标排序的,所以我们最坏情况只需要从1到n扫一遍即可求出这一个块的所有答案,复杂度On,而块最多有n^(1-w),于是这个操作整体复杂度n^(2-w)(我们并不能以右端点所在块排序,举个简单例子,假设(1,7),(2,1),(3,7),我们假设123,17都是处于同一个块中,如果按照下标排序,一遍扫过去即可,而如果按所在块排序,最坏情况可能出现7——1——7的情况,复杂度会提高很多(不知道恰不恰当qwq,不过好多题以右端点所在块排序好像也是没什么问题的))
2.l所处的块相同,l的移动复杂度,l既然在一个块中,那么每次移动至多是n^w,询问m个,复杂度m*n^w,等价于n^(1+w)。
3.l在不同块之间移动时,l的移动复杂度,每一次至多是2*n^w,至多出现n^(1-w),所以整体复杂度时On的。
4.l在不同块之间移动式,r的移动复杂度,每一次移动至多是On的,最多有n^(1-w)个块,所以复杂度时n^(2-w)
综上所述,我们分块的复杂度是max(n^(2-w),n^(1+w)),于是当w取1/2也就是块的大小取到根号n的时候,莫队的复杂度达到最优。
参考博客:戳我(没看明白我写的可以看这里qwq,我在这里学的)
二、带修莫队
如果会了普通莫队,那么带修莫队其实也很简单了。
先放上题目和代码
1 #include<bits/stdc++.h>//再写一遍带修莫队 2 using namespace std; 3 int n,m,col[10005],pos[10005],cnt[1000005],b[10005],m1,m2; 4 struct Ques{ 5 int l,r,id,ans,tim; 6 bool operator < (const Ques &o)const{ 7 if(pos[l]!=pos[o.l])return l<o.l; 8 if(pos[r]!=pos[o.r])return r<o.r; 9 return tim<o.tim; 10 } 11 }q[10005]; 12 struct change{ 13 int id,to,from; 14 }c[1005]; 15 bool cmp(Ques x,Ques y){ 16 return x.id<y.id; 17 } 18 int main() 19 { 20 freopen("nt2011_color.in","r",stdin);freopen("nt2011_color.out","w",stdout); 21 // freopen("1.txt","r",stdin); 22 scanf("%d%d",&n,&m); 23 for(int i=1;i<=n;i++)scanf("%d",&col[i]),b[i]=col[i]; 24 for(int i=1;i<=m;i++){ 25 char s[5];int x,y; 26 scanf("%s%d%d",s,&x,&y); 27 if(s[0]=='Q'){ 28 q[++m1].id=i;q[m1].l=x;q[m1].r=y;q[m1].tim=m2; 29 } 30 else{ 31 c[++m2].id=x;c[m2].from=b[x];c[m2].to=y;b[x]=y;//这里x错打成了i还过了12个点... 32 } 33 } 34 int len=pow(n,0.666666); 35 for(int i=1;i<=n;i++)pos[i]=(i-1)/len+1; 36 sort(q+1,q+m1+1); 37 int now=0,l=1,r=0,ans=0; 38 for(int i=1;i<=m1;i++){ 39 while(now<q[i].tim){ 40 now++; 41 col[c[now].id]=c[now].to; 42 if(c[now].id>=l&&c[now].id<=r){ 43 cnt[c[now].from]--;if(!cnt[c[now].from])ans--; 44 cnt[c[now].to]++;if(cnt[c[now].to]==1)ans++; 45 } 46 } 47 while(now>q[i].tim){ 48 col[c[now].id]=c[now].from; 49 if(c[now].id>=l&&c[now].id<=r){ 50 cnt[c[now].from]++;if(cnt[c[now].from]==1)ans++; 51 cnt[c[now].to]--;if(!cnt[c[now].to])ans--; 52 } 53 now--; 54 } 55 while(l<q[i].l){ 56 cnt[col[l]]--;if(!cnt[col[l]])ans--; 57 l++; 58 } 59 while(l>q[i].l){ 60 l--; 61 cnt[col[l]]++;if(cnt[col[l]]==1)ans++; 62 } 63 while(r<q[i].r){ 64 r++; 65 cnt[col[r]]++;if(cnt[col[r]]==1)ans++; 66 } 67 while(r>q[i].r){ 68 cnt[col[r]]--;if(!cnt[col[r]])ans--; 69 r--; 70 } 71 q[i].ans=ans; 72 } 73 sort(q+1,q+m1+1,cmp); 74 for(int i=1;i<=m1;i++)printf("%d\n",q[i].ans); 75 return 0; 76 }
这道题也是一道模板题吧,其实就是上一道题加上了一个修改操作,莫队一般是处理不了修改操作的,忘记谁说的了
那我们如何处理呢?首先,我们引入一个“时间”参数,将其看做等同于l和r的一个变量,这个时间表示的是什么呢?他表示截止到他这个询问,前面一共修改了几次。这有什么用呢?当我们处理到一个询问,我们对他的时间和当前时间进行比较,如果当前时间小于当前询问时间,那我们依次执行这其中的修改操作,反之的话将修改操作"推"回去,也就是对于每一个修改,我记录他将那个位置的颜色从什么(from)变成了什么(to),“推”的过程其实就是再从to变回from。那这样复杂度不会很高么?
合理的分块啊!废话
这次我们分块,类比普通莫队,我们以左端点所在块为第一关键字,右端点所在块为第二关键字,第三关键字时间同普通莫队,按时间本身而不是所谓“块”排序。
这都是小事,整体思想是一样的,下面给出带修莫队复杂度分析,我尝试说一说啦qwq(我找不到我之前看的那个博客了),先假设块的大小为n^w
1、对于lr都处于同一个同一个块,tim的移动复杂度,因为是从小到大排序的,所以扫一遍最高复杂度是...是多少呢...跟询问有关啊,反正是线性的吧!而l有n^(1-w)种选择,r同理,所以至多有n^(2-2*w)次移动,那么整体复杂度为n^(3-2*w)。
2、对于lr都处于同一个块,l移动的复杂度,r的移动复杂度,都是n^w级别的,同样至多有,emmm,大概m次询问?所以l和r的整体复杂度都是n^(1+w)的。
3、对于l处于同一个快,r在发生块之间的变动时,tim的复杂度以及r的复杂度,l的复杂度,
l依旧在一个块中移动,复杂度n^w,总体n^(1+w),r在不同的块之间移动,复杂度至多为2*n^w
这种情况至多存在n^(2-2*w)次,整体复杂度n^(2-w),对于tim,他的移动也是On级别的,而这种情况至多出现n^(2-2*w)次,整体复杂度n^(3-2*w)。
4、对于l的块发生变动时,l,r,tim的复杂度分析,l至多为2*n^w,至多发生n^(1-w)次,整体复杂度是On的,r的移动至多是On的,最多发生n^(1-w)次,总体复杂度n^(2-w),对于tim,每次移动是On的,一共n^(1-w)次,总体复杂度n^(2-w)
emmmm应该没啦吧,我就想到这些了qwq,如果有落下的复杂度分析可以指出啊
那么整体复杂度是什么呢?max(3-2*w,1+w,2-w),我们发现,此刻w不能在等于1/2了!当w等于2/3的时候,算法复杂度达到最优,所以带修莫队块的大小设为2/3!!!切记
我就写过这一道模板题qwq
不过可以先看一下这道题: 糖果公园
三、树上莫队
例题就是上面那个糖果乐园啦,树上带修莫队,其实和树上莫队是一样的。
先贴上我的代码
1 #include<bits/stdc++.h> 2 #define v e[i].to 3 #define ll long long 4 using namespace std; 5 const int ma=100005; 6 int n,m,Q,fi[ma],tot,w[ma],dfn[ma<<1],cnt[ma],h[ma],c[ma],ans[ma],pos[ma<<1],o1,o2,st[ma],ed[ma],temp[ma]; 7 int siz[ma],top[ma],fa[ma],son[ma],dep[ma]; 8 struct edge{ 9 int to,next; 10 }e[ma<<1]; 11 void edge_add(int x,int y){ 12 e[++tot].to=y; 13 e[tot].next=fi[x]; 14 fi[x]=tot; 15 } 16 struct Ques{ 17 int l,r,tim,id,lc;ll ans; 18 bool operator < (const Ques&o)const{ 19 if(pos[l]!=pos[o.l])return l<o.l; 20 if(pos[r]!=pos[o.r])return r<o.r; 21 return tim<o.tim; 22 } 23 }q[ma]; 24 struct Change{ 25 int id,from,to; 26 }change[ma]; 27 void dfs1(int x){ 28 dfn[++tot]=x;st[x]=tot; 29 for(int i=fi[x];i;i=e[i].next){ 30 if(fa[x]==v)continue; 31 fa[v]=x;dep[v]=dep[x]+1; 32 dfs1(v);siz[x]+=siz[v]; 33 if(siz[v]>siz[son[x]])son[x]=v; 34 }dfn[++tot]=x;ed[x]=tot; 35 } 36 void dfs2(int x,int y){ 37 top[x]=y; 38 if(son[x])dfs2(son[x],y); 39 for(int i=fi[x];i;i=e[i].next){ 40 if(top[v])continue; 41 dfs2(v,v); 42 } 43 } 44 int get_lca(int x,int y){ 45 while(top[x]!=top[y]){ 46 if(dep[top[x]]<dep[top[y]])swap(x,y); 47 x=fa[top[x]]; 48 }return dep[x]<dep[y]?x:y; 49 } 50 bool cmp(Ques x,Ques y){ 51 return x.id<y.id; 52 } 53 int read(){ 54 char ch=getchar(); 55 int a=0; 56 while(!(ch<='9'&&ch>='0'))ch=getchar(); 57 while(ch<='9'&&ch>='0'){ 58 a=(a<<1)+(a<<3)+('0'^ch); 59 ch=getchar(); 60 }return a; 61 } 62 int main() 63 { 64 int __size__ = 128<<20; 65 char *__ptr__ = (char *)malloc(__size__)+__size__; 66 __asm__("movl %0, %%esp\n"::"r"(__ptr__)); 67 freopen("park.in","r",stdin);freopen("park.out","w",stdout); 68 n=read(),m=read(),Q=read(); 69 for(int i=1;i<=m;i++)h[i]=read(); 70 for(int i=1;i<=n;i++)w[i]=read(); 71 for(int i=1;i<n;i++){ 72 int x,y;x=read();y=read(); 73 edge_add(x,y);edge_add(y,x); 74 }tot=0; 75 for(int i=1;i<=n;i++)c[i]=read(),temp[i]=c[i],siz[i]=1; 76 dfs1(1);dfs2(1,1); 77 int len=5000; 78 for(int i=1;i<=tot;i++)pos[i]=(i-1)/len+1; 79 for(int i=1;i<=Q;i++){ 80 int t,x,y;t=read();x=read();y=read(); 81 if(t){ 82 if(st[x]>st[y])swap(x,y); 83 int lc=get_lca(x,y);q[++o1].id=o1; 84 if(lc!=x)q[o1].l=ed[x],q[o1].r=st[y],q[o1].tim=o2,q[o1].lc=lc; 85 else q[o1].l=st[x],q[o1].r=st[y],q[o1].tim=o2; 86 } 87 else{ 88 change[++o2].id=x;change[o2].from=temp[x];change[o2].to=y;temp[x]=y; 89 } 90 }sort(q+1,q+o1+1); 91 int l=1,r=0,t=0;ll now=0; 92 for(int i=1;i<=o1;i++){ 93 while(t<q[i].tim){ 94 t++; 95 if(cnt[change[t].id]&1){ 96 ++ans[change[t].to]; 97 now+=(ll)w[ans[change[t].to]]*(ll)h[change[t].to]; 98 now-=(ll)w[ans[change[t].from]]*(ll)h[change[t].from]; 99 ans[change[t].from]--; 100 }c[change[t].id]=change[t].to; 101 } 102 while(t>q[i].tim){ 103 if(cnt[change[t].id]&1){ 104 ++ans[change[t].from]; 105 now+=(ll)w[ans[change[t].from]]*(ll)h[change[t].from]; 106 now-=(ll)w[ans[change[t].to]]*(ll)h[change[t].to]; 107 ans[change[t].to]--; 108 }c[change[t].id]=change[t].from;t--; 109 } 110 while(l<q[i].l){ 111 cnt[dfn[l]]--; 112 if(cnt[dfn[l]]&1)ans[c[dfn[l]]]++,now+=(ll)w[ans[c[dfn[l]]]]*(ll)h[c[dfn[l]]]; 113 else now-=(ll)w[ans[c[dfn[l]]]]*(ll)h[c[dfn[l]]],ans[c[dfn[l]]]--; 114 l++; 115 } 116 while(l>q[i].l){ 117 l--;cnt[dfn[l]]++; 118 if(cnt[dfn[l]]&1)ans[c[dfn[l]]]++,now+=(ll)w[ans[c[dfn[l]]]]*(ll)h[c[dfn[l]]]; 119 else now-=(ll)w[ans[c[dfn[l]]]]*(ll)h[c[dfn[l]]],ans[c[dfn[l]]]--; 120 } 121 while(r<q[i].r){ 122 r++;cnt[dfn[r]]++; 123 if(cnt[dfn[r]]&1)ans[c[dfn[r]]]++,now+=(ll)w[ans[c[dfn[r]]]]*(ll)h[c[dfn[r]]]; 124 else now-=(ll)w[ans[c[dfn[r]]]]*(ll)h[c[dfn[r]]],ans[c[dfn[r]]]--; 125 } 126 while(r>q[i].r){ 127 cnt[dfn[r]]--; 128 if(cnt[dfn[r]]&1)ans[c[dfn[r]]]++,now+=(ll)w[ans[c[dfn[r]]]]*(ll)h[c[dfn[r]]]; 129 else now-=(ll)w[ans[c[dfn[r]]]]*(ll)h[c[dfn[r]]],ans[c[dfn[r]]]--; 130 r--; 131 } 132 q[i].ans=now+(ll)h[c[q[i].lc]]*(ll)w[ans[c[q[i].lc]]+1]; 133 } 134 sort(q+1,q+o1+1,cmp); 135 for(int i=1;i<=o1;i++)printf("%lld\n",q[i].ans); 136 return 0; 137 }
这两种写法我是都看了,但是我只写过朴素的,没写过vfk的那种qwq,我觉得朴素写法能让我看起来更舒服一些。
朴素写法其实就是将树变成一个序列,然后再像普通莫队那样处理即可,其实如果理解dfn序什么的,写过树剖之类的看这种应该是很容易就懂了,就算没看过应该也很容易看懂的。
我们怎么操作呢?
1、如果是询问子树,那很简单啊,直接dfs一遍标一下号,就是正常的深度优先搜索,每次到达一个点,为其标上一个号,我们可以发现同一个子树总是连续的一段,那我们对于题目中给出的询问,我们可以变成对序列上询问区间dfn[x]-----dfn[x]+siz[x]-1,然后就想普通莫队那样处理即可。
2、如果是询问链,我们该怎么做呢?首先我们依旧dfs,只不过每次遍历到该点和遍历完该点,都要记录在dfn序中,同时记录每一个点的st和ed,也就是遍历到该点时的时间戳和遍历完该点的时间戳。举个例子:我不会上传图片啊qwq,上传的都很模糊,你们自己yy一下,随便yy一棵树。按照我们的dfs方法...dfs出一个序列...是什么呢....
算了我还是描述一棵树吧...下面是树的边
1 2
1 3
2 4
2 5
4 7
4 8
3 6
6 9
那么他遍历完得序列就是:124778845523699631...应该没错
我们分两种情况讨论,对于所给的l,r(默认l比r先遍历到)
1).如果l是r的祖先,那么我们对应序列上需要询问的便是st[l]--st[r],为什么呢...自己体会我也说不太明白
2).如果不是呢,那么就是ed[l]--st[r]。
如果按照以上方法,我们可以发现一个规律,对于一段序列,其中出现次数为奇数次的数字,他对应树上的节点一定在该序列两端点所对应树上的两个点所成的链上
但同时又发现了一件事情,对于第二种情况,我们发现他的lca并没有被我们计算,于是我们需要单独在计算一下lca的贡献。
截止到此,我们就已经可以将树上的询问转变为序列上的询问了,如果其中带有修改操作,其实无所谓,都是同理。
还有啊例题windows下需要扩栈的,扩栈代码见我代码了。
那么最基本的莫队就算说完了?qwq
难得写了这么多,评论区欢迎提问啊,虽然我可能也不会qwq
希望能成为我第一篇像样一点的blog!!!