【算法介绍】
莫队算法是用于离线处理处理区间问题的一类算法,非常易于理解和上手,应用面十分广泛,甚至还可以在树上进行操作。
当我们得到$[L,R]$的答案之后,如果能够以较低的复杂度扩展得到$[L-1,R],[L+1,R],[L,R-1],[L,R+1]$的答案,我们就可以使用莫队算法,通常这个扩展的复杂度是$O(1)$或$O(logn)$。
如果我们对于每个询问都暴力移动左右端点,那么复杂度肯定是$O(n^2)$的,而莫队算法的精髓就在于结合了分块的思想。
设扩展一次的复杂度为$O(f(n))$。将序列分割成$\sqrt{n}$个块,把询问排序,以左端点块编号为第一关键字,右端点为第二关键字,这样扩展的话,对于每一次询问,左端点扩展的复杂度是$O(\sqrt{n}*f(n))$的,而对于左端点的每一个块,右端点扩展的复杂度是$O(n*f(n))$的,可以发现右端点扩展的复杂度是跟询问次数无关的,所以总复杂度为$O((n+m)*\sqrt{n}*f(n))$。
有一个常数优化是左端点在奇数块的话右端点升序排序,在偶数块的话右端点降序排序,因为这样相当于一个右端点来回跑的过程,能够省去很多次右端点的扩展,能够有明显的效率提升。
【算法流程】
将询问排序。
扫一遍询问,从上一次询问的左右端点扩展到当前询问,得出答案。
bool operator < (poi a, poi b) {return bl[a.l]<bl[b.l] || (bl[a.l]==bl[b.l] && ((bl[a.l]&1)?a.r<b.r:a.r>b.r));} inline void update(int x, int delta){...//更新信息} int main() { read(n); read(m); int blo=sqrt(n); for(int i=1;i<=n;i++) read(a[i]); for(int i=1;i<=m;i++) read(q[i].l), read(q[i].r), q[i].pos=i; for(int i=1;i<=n;i++) bl[i]=(i-1)/blo+1; sort(q+1, q+1+m); for(int i=1, l=1, r=0;i<=m;i++) { while(l<q[i].l) update(l++, -1); while(l>q[i].l) update(--l, 1); while(r<q[i].r) update(++r, 1); while(r>q[i].r) update(r--, -1); ...//更新答案 } }
【例题】
例1:bzoj2038: [2009国家集训队]小Z的袜子(hose)
裸题了,求区间的每种颜色出现次数,然后随便算一下贡献即可。
#include<iostream> #include<cstring> #include<cstdlib> #include<cstdio> #include<cmath> #include<algorithm> #define ll long long using namespace std; const int maxn=500010, inf=1e9; struct poi{int l, r, pos;}q[maxn]; int n, m; int a[maxn], bl[maxn], cnt[maxn]; ll fz, ansfz[maxn], ansfm[maxn]; inline void read(int &k) { int f=1; k=0; char c=getchar(); while(c<‘0‘ || c>‘9‘) c==‘-‘&&(f=-1), c=getchar(); while(c<=‘9‘ && c>=‘0‘) k=k*10+c-‘0‘, c=getchar(); k*=f; } bool operator < (poi a, poi b) {return bl[a.l]<bl[b.l] || (bl[a.l]==bl[b.l] && ((bl[a.l]&1)?a.r<b.r:a.r>b.r));} inline void update(int x, int delta) { fz-=1ll*cnt[a[x]]*(cnt[a[x]]-1); cnt[a[x]]+=delta; fz+=1ll*cnt[a[x]]*(cnt[a[x]]-1); } ll gcd(ll a, ll b){return b?gcd(b, a%b):a;} int main() { read(n); read(m); int blo=sqrt(n); for(int i=1;i<=n;i++) read(a[i]); for(int i=1;i<=m;i++) read(q[i].l), read(q[i].r), q[i].pos=i; for(int i=1;i<=n;i++) bl[i]=(i-1)/blo+1; sort(q+1, q+1+m); for(int i=1, l=1, r=0;i<=m;i++) { while(l<q[i].l) update(l++, -1); while(l>q[i].l) update(--l, 1); while(r<q[i].r) update(++r, 1); while(r>q[i].r) update(r--, -1); int len=q[i].r-q[i].l+1; int d=gcd(fz, 1ll*len*(len-1)); ansfz[q[i].pos]=fz/d; ansfm[q[i].pos]=1ll*len*(len-1)/d; } for(int i=1;i<=m;i++) printf("%lld/%lld\n", ansfz[i], ansfm[i]?ansfm[i]:1); }
加个权值树状数组维护一下每种权值的出现次数以及种类即可。
#include<iostream> #include<cstring> #include<cstdlib> #include<cstdio> #include<cmath> #include<algorithm> using namespace std; const int maxn=1000010, inf=1e9; struct poi{int l, r, a, b, pos;}q[maxn]; int n, m; int a[maxn], bl[maxn], tree[2][maxn], ans1[maxn], ans2[maxn], cnt[maxn]; inline void read(int &k) { int f=1; k=0; char c=getchar(); while(c<‘0‘ || c>‘9‘) c==‘-‘&&(f=-1), c=getchar(); while(c<=‘9‘ && c>=‘0‘) k=k*10+c-‘0‘, c=getchar(); k*=f; } bool operator < (poi a, poi b) {return bl[a.l]<bl[b.l] || (bl[a.l]==bl[b.l] && ((bl[a.l]&1)?a.r<b.r:a.r>b.r));} inline void add(int ty, int x, int delta){for(;x<=n;x+=x&-x) tree[ty][x]+=delta;} inline int query(int ty, int x){int sum=0; for(;x;x-=x&-x) sum+=tree[ty][x]; return sum;} inline void update(int x, int delta) { add(0, a[x], delta); if(!cnt[a[x]] && delta==1) add(1, a[x], 1); cnt[a[x]]+=delta; if(!cnt[a[x]] && delta==-1) add(1, a[x], -1); } int main() { read(n); read(m); int blo=sqrt(n); for(int i=1;i<=n;i++) bl[i]=(i-1)/blo+1; for(int i=1;i<=n;i++) read(a[i]); for(int i=1;i<=m;i++) read(q[i].l), read(q[i].r), read(q[i].a), read(q[i].b), q[i].pos=i; sort(q+1, q+1+m); for(int i=1, l=1, r=0;i<=m;i++) { while(l<q[i].l) update(l++, -1); while(l>q[i].l) update(--l, 1); while(r<q[i].r) update(++r, 1); while(r>q[i].r) update(r--, -1); ans1[q[i].pos]=query(0, q[i].b)-query(0, q[i].a-1); ans2[q[i].pos]=query(1, q[i].b)-query(1, q[i].a-1); } for(int i=1;i<=m;i++) printf("%d %d\n", ans1[i], ans2[i]); }
例3