这个算法是由之前的国家队队长莫涛巨神(Orz….%%%64)发明的,所以尊称莫队算法。
事实上,莫队算法这种东西,应该叫做——
如果我们知道区间[L,R]的话,那就可以用莫队算法了。
有一种经典的问题:给你一些不带修改的区间询问,要求快速回答
显然,有一些我们可以通过线段树来完成,因为线段树是O(NlogN)的
但是,线段树有的东西是维护不了的。
看一个例子
给你一个数列,若干询问,要求回答区间内同种颜色大小。
线段树很难做,怎么办?
用莫队算法!
莫队算法的实质是通过将询问排序,每个询问均由前一个询问(排序后的)转移得来,通过一定的排序优化时间复杂度。往往可以有O(NN−−√)的效果
回到题目
显然对于两次询问L,R的答案。
|L−L′|+|R−R′|。这个东西,数学上称之为曼哈顿距离
把每个询问看作是二维平面上的点,那么我们的最小总时间,就是这些点的最小曼哈顿距离生成树, 按照这个树的顺序做,复杂度变成了O(NN−−√)(为什么?不好意思,我不会证),而且这个生成树连边也有特别的技巧,可以去看莫队在知乎上推荐的那篇。
有一个优美方便简洁好理解的替代品
把整个序列分块,把L本身!)为第二关键字排序。
为什么要分块不能直接排呢?
分块很好的减少了一种情况的影响。
L<L′<L′′排序就会浪费非常多的时间。
由于每个块的大小是N−−√
分块使得两个询问之间差异平均到了L,R上。
因此,理论复杂度大约是O(NN−−√)
然后我们继续分块。
把二元组L,R是这次询问在修改第几次后。
然后把R看作第三关键字排序即可。
转移时直接恢复(或删除)两次询问之间的修改。如果在区间内还要计算对答案的影响
然而修改的复杂度不同
最优分块方式是每块N23块
左指针移动N∗N23=N53次
右指针移动N∗N23=N53次
修改指针移动(N13)2∗N=N53次
总复杂度N53
在实际操作中,有时取一些什么n−−√∗10之类的奇葩的数可能更快
有一道板题
http://blog.csdn.net/hzj1054689699/article/details/51880644
另一份
看了楼下的题解,表示什么都看不懂(我没学过普通莫队。。)
于是决定写份题解。。
原版莫队:
时间复杂度O(n*√n),不支持修改,就拿本题为例:
如果我们知道(L,R)区间有多少个不同颜色,我们就可以在O(1)的时间求出(L,R+1)或(L+1,R)或(L-1,R)或(L,R-1)的值
举个例子,
5个数,颜色分别为2 3 1 4 5
我们知道(2,3)的答案为2,要知道(2,4)的答案,我们只需要判断color[4]是否出现过,如果否,那么ans++。无论如何,标记color[4]出现的次数+1。
莫队是一个离线算法,被称为优雅的暴力。
这个算法只是把所有询问记录下来,然后交换回答的顺序,按上面的算法去做。
从(L,R)转移到(L1,R1)的时间复杂度是|L1-L|+|R1-R|,可以证明,整个程序时间复杂度O(n*√n)。 那么怎么交换顺序呢?
只需要先将每个询问的左端点分块,分成√n块,每块√n,最后多出的新开一块,
然后按左端点所在块的序号(L/sqrt(n))为第一关键词,右端点为第二关键词排序就行了。
排序完直接按之前说的转移状态就行了。
至于带修改莫队,我们需要加一个记录询问的变量X,表示在这个询问之前进行了多少次修改。
并且要记录每次修改,修改的位置,修改前的颜色,修改后的颜色。
和普通莫队一样处理,
当碰到一个询问需要X次修改,而当前只执行了X-3次修改,我们就要执行那3次修改。
同样地,如果当前已经执行了X+3次修改,我们就要倒退3次修改。
大概就是这样。。
可以证明时间复杂度O(n^3/5)
代码丑,,因为我改了太久,,很烦。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define rep(i,m,n) for(int i=m;i<=n;i++)
#define dop(i,m,n) for(int i=m;i>=n;i--)
#define lowbit(x) (x&(-x))
#define ll long long
#define INF 2147483647
using namespace std;
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
const int maxn=10010;
const int maxm=10010;
struct Ask{ //记录询问
int L,R,X,id,lk; //L,R询问区域,lk左端点所在块
}A[maxm];
struct Do{ //记录修改
int x,f,t; //x位置,f原来的颜色,t修改后的颜色
}D[maxm];
int l,r,a[maxn],n,m,color[maxn],ans,len,m1,m2,x,Ans[maxm],c[maxn];
char ch;
void move(int now,int mode){
color[a[now]]+=mode;
if(!color[a[now]]&&mode==-1) ans--;
if(color[a[now]]==1&&mode==1) ans++;
}
int cmp(const Ask &A,const Ask &B){
if(A.lk==B.lk) return A.Rreturn A.lkint main(){
ios::sync_with_stdio(false);
cin>>n>>m;
len=(int)sqrt(n); //块的大小
rep(i,1,n)
cin>>a[i],c[i]=a[i]; //把a数组备份
rep(i,1,m){
cin>>ch;
if(ch=='Q'){
cin>>A[++m1].L; //m1询问次数
cin>>A[m1].R;
A[m1].X=m2;
A[m1].id=m1;
A[m1].lk=(A[m1].L-1)/len+1;
}
else{ //m2修改次数
cin>>D[++m2].x; //修改的位置
D[m2].f=a[D[m2].x]; //保存原来的颜色
cin>>D[m2].t; //读入修改后的颜色
a[D[m2].x]=D[m2].t; //修改颜色
}
}
rep(i,1,n) a[i]=c[i]; //把a数组还原
color[a[1]]=1;
ans=1;
l=r=1;
sort(A+1,A+m1+1,cmp); //把询问排序
rep(i,1,m1){ //一个一个处理
//莫队
while(x//x是已执行的修改
Do tmp=D[++x];
if(l<=tmp.x&&r>=tmp.x){
color[a[tmp.x]]--;
ans-=!color[a[tmp.x]];
a[tmp.x]=tmp.t;
color[a[tmp.x]]++;
ans+=color[a[tmp.x]]==1;
}
a[tmp.x]=tmp.t;
}
while(x>A[i].X){
Do tmp=D[x--];
if(l<=tmp.x&&r>=tmp.x){
color[a[tmp.x]]--;
ans-=!color[a[tmp.x]];
a[tmp.x]=tmp.f;
color[a[tmp.x]]++;
ans+=color[a[tmp.x]]==1;
}
a[tmp.x]=tmp.f;
}
Ask tmp=A[i];
while(l0,-1),l++; //普通莫队
while(l>tmp.L) move(l-1,+1),l--;
while(r1,+1),r++;
while(r>tmp.R) move(r+0,-1),r--;
Ans[tmp.id]=ans;
}
rep(i,1,m1) printf("%d\n",Ans[i]);
return 0;
}
感谢洛谷Riven_Yasuo的题解和这篇博客博主Orzhttp://blog.csdn.net/hzj1054689699/article/details/51866615