莫队是高端巧妙的分块!!!
记住上面的话,这样就已经学会了一半了。
例题:给出一个序列,每次询问一个区间内不同的数字个数
线段树套主席树?
提前处理出每个数字和他相同的上一个数字的位置,记为数组V
外层线段树划分区间,对于每一个划分的区间开一个主席树
主席树里面压入对应区间的V数组(权值线段树形态)
那么每次查询就是在区间L-R内查V中小于L的数的个数
时间nlogn
有点麻烦
用莫队,全程维护一个区间,根据每次询问调整区间的左右端点,在区间内记录下每个元素的出现次数即可
对于扩展区间,就是看这个数之前是否出现过,如果没出现过,次数++,ans++,否则只有次数++
删除的时候,如果删到次数为0了,ans--即可。
那么我们为了让算法更快,我们要让区间左右端点的移动次数最少,下面憨憨都知道,对询问排序,按照区间左端点的位置以此查询
但是这样可能会让右端点往复跳,我们不如对l进行分块,在同一块的l按照r排序即可。
这个代码就不放了。
下面对问题加一个单点修改(区间修改出题人就是让你写高级数据结构的,认命吧)
仔细一想,我们刚才是不是相当于把问题换成了一些平面直角坐标系上的点,我们排序的目的是不是找出尽量短的路线遍历所有点(实际上有最短的:曼哈顿最小生成树,但是莫队一般用不到)
加一个修改是不是可以把所有问题分成两部分?
修改前的和修改后的,那么是不是可以把修改操作提出的时间换成坐标系的第三维Z轴?
那么问题就变成了如何找出尽量短的路线遍历空间几何坐标系上的一些点?
那么是不是可以考虑还按照刚才的思路,如果L和R都在同一块的情况下我们按照时间排序?
询问的时间可以视为在哪一次修改的后面
至于时间由后向前推的过程,改过去的再改回来好啦
例题:洛谷P1903:数颜色
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> using namespace std; const int MAXN=2*1e6+10; int pos[MAXN],col[MAXN]; struct PE { int l,r,id,T; }; PE Q[MAXN]; struct C { int p,v; }; C change[MAXN]; int cnt[MAXN],ans[MAXN]; int nl,nr,sum,nt,n,m,a1,a2,TM=0,QE=0; char XS; inline int read() { char c=getchar();int x=0,f=1; while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();} while(c>='0'&&c<='9'){x=x*10+c-'0',c=getchar();} return x*f; } void ADD(int x,int v) { if(v>0) { if(!cnt[col[x]]) { sum++; } cnt[col[x]]++; } if(v<0) { cnt[col[x]]--; if(!cnt[col[x]]) sum--; } } void Change(int x) { if(nl<=change[x].p&&nr>=change[x].p) { cnt[col[change[x].p]]--; if(!cnt[col[change[x].p]]) { sum--; } if(!cnt[change[x].v]) sum++; cnt[change[x].v]++; } swap(col[change[x].p],change[x].v);//现在时间轴已经过修改,复原时相当于恢复原数列,因此交换变化参数 } bool Function(PE x,PE y) { if(pos[x.l]==pos[y.l]) { if(pos[x.r]==pos[y.r]) return x.T<y.T; else return pos[x.r]<pos[y.r]; } else return pos[x.l]<pos[y.l]; } int main() { //freopen("nt2011_color.in","r",stdin); // scanf("%d%d",&n,&m); n=read(); m=read(); for(int i=1;i<=n;i++) { col[i]=read(); } int Block=pow(n,2.0/3.0); for(int i=1;i<=n;i++) { pos[i]=(i-1)/Block+1; } for(int i=1;i<=m;i++) { cin>>XS; a1=read(); a2=read(); if(XS=='Q') { QE++; Q[QE].l=a1; Q[QE].r=a2; Q[QE].T=TM; Q[QE].id=QE; } else { TM++; change[TM].p=a1; change[TM].v=a2; } } sort(Q+1,Q+1+QE,Function); nl=1,nr=0; nt=0; for(int i=1;i<=QE;i++) { while(nl<Q[i].l) { ADD(nl,-1); nl++; } while(nl>Q[i].l) { nl--; ADD(nl,1); } while(nr>Q[i].r) { ADD(nr,-1); nr--; } while(nr<Q[i].r) { nr++; ADD(nr,1); } while(nt<Q[i].T) { nt++; Change(nt); } while(nt>Q[i].T) { Change(nt); nt--; } ans[Q[i].id]=sum; } for(int i=1;i<=QE;i++) { printf("%d ",ans[i]); } return 0; }
如果我们让莫队上树怎么样?
这时候我们要寻找一个合适的序列能表示两点之间经过的所有点
DFS序肯定不行,这货只能表示父子关系
不如我们试试欧拉序(进入这个点的时候标记一次,遍历完所有儿子的时候再标记一次)
我们设st[U]是U第一次出现在欧拉序中的时候的编号
ed[U]则是第二次的编号(不理解的一会看代码的DFS部分)
我们发现从U到V的欧拉序中,如果U或V不是另外一个的祖先的话,除了LCA(U,V)其他从U到V的路径上的点都会在欧拉序中出现1次,另外多余的点会出现两次
这个时候我们只要关注ed[U]到st[V]中只出现一次的就好了(实际上最多也就出现两次,出现一次的时候算上贡献,出现两次的时候再去掉贡献),最后特判算上LCA的贡献
如果U或V的另一个点的祖先的话,路径就是一条链,欧拉序中每个点也只会出现一次,那么就统计st[FA(U,V)]到st[SON(U,V)]的贡献就好
至于怎么去掉贡献嘛,下面代码中有很巧妙的操作:
这样我们就得到了每个问题的询问区间,老套路,分块排序求解
例题:洛谷SP10707:COT2-Count on a tree II
#include<iostream> #include<cstring> #include<cstdio> #include<vector> #include<algorithm> #include<cmath> using namespace std; const int MAXN=1e5+10; int n,q,a1,a2; int pos[MAXN],block; struct PE { int l,r,id,lca,ans; }; PE Q[MAXN]; bool Function(PE x,PE y) { if(pos[x.l]==pos[y.l]) return x.r<y.r; return pos[x.l]<pos[y.l]; } vector<int> b[MAXN]; int a[MAXN],date[MAXN],dep[MAXN],st[MAXN],ed[MAXN],fa[MAXN][26]; void LSH() { sort(date+1,date+1+n); int num=unique(date+1,date+1+n)-date-1; for(int i=1;i<=n;i++) { a[i]=lower_bound(date+1,date+1+num,a[i])-date; } } int pot[MAXN],tot; void DFS(int x,int ff) { fa[x][0]=ff; dep[x]=dep[ff]+1; st[x]=++tot; pot[tot]=x; for(int i=0;i<b[x].size();i++) { int to=b[x][i]; if(fa[x][0]==to) continue; DFS(to,x); } ed[x]=++tot; pot[tot]=x; } void BZ() { for(int i=1;i<=25;i++) { for(int j=1;j<=n;j++) { fa[j][i]=fa[fa[j][i-1]][i-1]; } } } int LCA(int x,int y) { if(dep[x]<dep[y]) swap(x,y); for(int i=25;i>=0;i--) { if(dep[fa[x][i]]>=dep[y]) 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]; } int sum,ans[MAXN],used[MAXN],cnt[MAXN]; void ADD(int x) { if(++cnt[x]==1) { sum++; } } void DELET(int x) { if(--cnt[x]==0) { sum--; } } void Change(int x) { if(used[x]) { DELET(a[x]); } else { ADD(a[x]); } used[x]^=1; //巧妙的操作,如果一个点在欧拉序中出现了两次,就代表没有贡献 //used[x]=1代表原来的区间里面已经有过这个点出现,再次访问等于消除这个点的贡献 } void Work() { sort(Q+1,Q+1+q,Function); int nl=1,nr=0; for(int i=1;i<=q;i++) { while(nl<Q[i].l) { Change(pot[nl]); nl++; //区间缩小使用change是因为如果pot[l]在区间内出现一次,这样相当于消去这一次的贡献 //如果出现了两次,相当于使得另外一次出现产生了贡献 } while(nl>Q[i].l) { nl--; Change(pot[nl]); //区间扩大使用change是因为如果pot[l]没有出现过就计算其贡献 //如果已经出现过就消除其贡献. } while(nr<Q[i].r) { nr++; Change(pot[nr]); } while(nr>Q[i].r) { Change(pot[nr]); nr--; } if(Q[i].lca) { Change(Q[i].lca); } Q[i].ans=sum; if(Q[i].lca) { Change(Q[i].lca); } //由于欧拉序中无法体现两点不在同一条链上的LCA //因此特判LCA,但是特判过程又更新了sum,因此需要复原 } for(int i=1;i<=q;i++) { ans[Q[i].id]=Q[i].ans; } for(int i=1;i<=q;i++) { printf("%d ",ans[i]); } } int main() { scanf("%d%d",&n,&q); block=sqrt(n); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); date[i]=a[i]; } for(int i=1;i<=n*2;i++) { pos[i]=i/block+1; } LSH(); for(int i=1;i<n;i++) { scanf("%d%d",&a1,&a2); b[a1].push_back(a2); b[a2].push_back(a1); } DFS(1,0); BZ(); for(int i=1;i<=q;i++) { scanf("%d%d",&a1,&a2); if(st[a1]>st[a2]) swap(a1,a2); int Lca=LCA(a1,a2); Q[i].id=i; Q[i].r=st[a2]; if(Lca==a1) { Q[i].l=st[a1]; } else { Q[i].l=ed[a1]; Q[i].lca=Lca; } } Work(); return 0; }
树都上了,也不差修改的功夫了吧
实际上我们在上面已经把树上问题转化为序列问题了,那么修改的过程实际上是一样的,都是算上第三维时间轴,一起分块排序求解
这东西比较麻烦,但是千万不要被黑题吓到了。
如果你已经看懂了前面两份代码中每一行的意思,那么第三份代码的顶端注释将帮你理解一切,否则就再去读一读上面的代码吧。
//新加入点的时候,if !used[x]则加入贡献 //if used[x],则cnt--,扣掉贡献 //删除点时,if !used[x](出现了两次),就加入一个贡献 //if used[x](只出现一次)就扣掉一次贡献 //对于修改,如果在区间外,只交换修改的值(方便以后复原) //在区间内,if used[x](就一次)扣掉原来的贡献,算新的 //if !used[x](一定出现了两次),只交换,不管实际操作。 #include<iostream> #include<cstdio> #include<algorithm> #include<vector> #include<cmath> using namespace std; const int maxn=2e5+1; int pot[maxn],pos[maxn],st[maxn],ed[maxn],cnt[maxn],col[maxn],fa[maxn][26],used[maxn],w[maxn],v[maxn],dep[maxn]; int nl,nr,nt,n,m,q,ct,cq,tot,a1,a2,block; long long int sum,ans[maxn]; vector<int> b[maxn]; void read(int &x) { x=0; int f=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); //真就2倍+8倍=10倍呗 ch=getchar(); } x*=f; } struct PE { int l,r,id,T,lca; }; PE Q[maxn]; struct CE { int p,v; }; CE C[maxn]; void Change(int x) { if(used[x]) { sum-=(long long)v[col[x]]*w[cnt[col[x]]]; cnt[col[x]]--; } else { cnt[col[x]]++; sum+=(long long)v[col[x]]*w[cnt[col[x]]]; } used[x]^=1; } void SWAP(int x) { if(used[C[x].p]) { sum-=(long long)v[col[C[x].p]]*w[cnt[col[C[x].p]]]; cnt[col[C[x].p]]--; cnt[C[x].v]++; sum+=(long long)v[C[x].v]*w[cnt[C[x].v]]; } swap(C[x].v,col[C[x].p]); } void DFS(int x,int ff) { dep[x]=dep[ff]+1; fa[x][0]=ff; st[x]=++tot; pot[tot]=x; for(int i=0;i<b[x].size();i++) { int to=b[x][i]; if(to==ff) continue; DFS(to,x); } ed[x]=++tot; pot[tot]=x; } int LCA(int x,int y) { if(dep[x]<dep[y]) swap(x,y); for(int i=20;i>=0;i--) { if(dep[fa[x][i]]>=dep[y]) { x=fa[x][i]; } } if(x==y) { return x; } for(int i=20;i>=0;i--) { if(fa[x][i]!=fa[y][i]) { x=fa[x][i]; y=fa[y][i]; } } return fa[x][0]; } void BZ() { for(int i=1;i<=20;i++) { for(int j=1;j<=n;j++) { fa[j][i]=fa[fa[j][i-1]][i-1]; } } } bool Function(PE x,PE y) { if(pos[x.l]==pos[y.l]) { if(pos[x.r]==pos[y.r]) { return x.T<y.T; } return x.r<y.r; } return x.l<y.l; } void Work() { nl=1,nr=0,nt=0,sum=0; for(int i=1;i<=cq;i++) { while(nl<Q[i].l) { Change(pot[nl]); nl++; } while(nl>Q[i].l) { nl--; Change(pot[nl]); } while(nr<Q[i].r) { nr++; Change(pot[nr]); } while(nr>Q[i].r) { Change(pot[nr]); nr--; } while(nt<Q[i].T) { nt++; SWAP(nt); } while(nt>Q[i].T) { SWAP(nt); nt--; } if(Q[i].lca) { Change(Q[i].lca); ans[Q[i].id]=sum; Change(Q[i].lca); } else { ans[Q[i].id]=sum; } } for(int i=1;i<=cq;i++) { printf("%lld ",ans[i]); } } signed main() { read(n); read(m); read(q); block=pow(n,2.0/3.0); for(int i=1;i<=2*n;i++) { pos[i]=i/block+1; } for(int i=1;i<=m;i++) { read(v[i]); } for(int i=1;i<=n;i++) { read(w[i]); } for(int i=1;i<n;i++) { read(a1); read(a2); b[a1].push_back(a2); b[a2].push_back(a1); } for(int i=1;i<=n;i++) { read(col[i]); } DFS(1,0); BZ(); for(int i=1;i<=q;i++) { read(a1); if(a1==0) { read(a1); read(a2); C[++ct].p=a1; C[ct].v=a2; } else { read(a1); read(a2); cq++; Q[cq].id=cq; Q[cq].T=ct; int lca=LCA(a1,a2); if(dep[a1]>dep[a2]) swap(a1,a2); if(lca==a1) { Q[cq].l=st[a1]; Q[cq].r=st[a2]; continue; } Q[cq].l=ed[a1]; Q[cq].r=st[a2]; Q[cq].lca=lca; } } sort(Q+1,Q+1+cq,Function); Work(); return 0; }
最后还有一个关键的问题:分块的大小!!!
二维莫队:sqrt(n)
三维莫队:pow(n,20./3.0)
四维莫队:pow(n,3.0/4.0)
完结撒花!!!