update in 2017.12.24:算法
之前写的≈shit,实在看不下去了,重写一遍ide
很早以前就学习了莫队算法。函数
老师讲课的时候就提到过带修改莫队在线莫队树上莫队树上带修改莫队……可是一直都没有作到过有关的题,学习
今天有幸作了一道裸的带修改莫队的题,spa
那就来分享一下本身的经验code
首先咱们要知道,普通的莫队算法是不资瓷修改操做的,blog
不事后人对莫队算法加以改进排序
发明了资瓷修改的莫队算法get
在进行修改操做的时候,修改操做是会对答案产生影响的(废话)it
那么咱们如何避免修改操做带来的影响呢?
首先咱们须要把查询操做和修改操做分别记录下来。
在记录查询操做的时候,须要增长一个变量来记录离本次查询最近的修改的位置
而后套上莫队的板子,与普通莫队不同的是,你须要用一个变量记录当前已经进行了几回修改
对于查询操做,若是当前改的比本次查询须要改的少,就改过去
反之若是改多了就改回来
说的听绕口的
好比,咱们如今已经进行了3次修改,本次查询是在第5次修改以后,那咱们就执行第4,5次修改
这样就能够避免修改操做对答案产生的影响了
update in 2018.5.4
洛谷数据被增强了,块的大小开$sqrt(N)$会T飞,开$n^{\frac{2}{3}}$可无压力过,带修改莫队真是玄学
// luogu-judger-enable-o2 // luogu-judger-enable-o2 #include<cstdio> #include<cmath> #include<algorithm> #define swap(x, y) x ^= y, y ^= x, x^= y using namespace std; const int MAXN= 2*1e6+10; 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; } char obuf[1<<24], *O=obuf; void print(int x) { if(x > 9) print(x / 10); *O++ = x % 10 + '0'; } int N, M; int a[MAXN], where[MAXN]; struct Query { int x, y, pre, id; }Q[MAXN]; int Qnum = 0; struct Change { int pos,val; }C[MAXN]; int Cnum = 0; int color[MAXN], ans=0, base, out[MAXN]; int comp(const Query &a,const Query &b) { return where[a.x] == where[b.x] ? ( where[a.y] == where[b.y] ? a.pre < b.pre : a.y < b.y ): a.x < b.x ; } void Add(int val){if(++color[val]==1) ans++; } void Delet(int val){ if(--color[val]==0) ans--; } void Work(int now, int i) { if(C[now].pos >= Q[i].x && C[now].pos <= Q[i].y) { if( --color[a[C[now].pos]] == 0 ) ans--; if( ++color[C[now].val] == 1) ans++; } swap(C[now].val, a[C[now].pos]); } void MoQueue() { int l = 1, r = 0, now = 0; for(int i = 1;i <= Qnum;i++) { while(l < Q[i].x) Delet(a[l++]); while(l > Q[i].x) Add(a[--l]); while(r < Q[i].y) Add(a[++r]); while(r > Q[i].y) Delet(a[r--]); while(now < Q[i].pre) Work(++now, i); while(now > Q[i].pre) Work(now--, i); out[Q[i].id] = ans; } for(int i = 1;i <= Qnum; i++) print(out[i]), *O++ = '\n'; } int main() { #ifdef WIN32 freopen("a.in", "r", stdin); freopen("a.out","w",stdout); #endif N = read();M = read(); for(int i = 1;i <= N; i++) a[i] = read(); while(M--) { char opt[5]; scanf("%s",opt); if(opt[0] == 'Q') { Q[++Qnum].x = read(); Q[Qnum].y = read(); Q[Qnum].pre = Cnum;//???????????? Q[Qnum].id = Qnum; } else if(opt[0] == 'R') { C[++Cnum].pos = read(); C[Cnum].val = read(); } } base = pow(N, 0.6666666666); for(int i = 1; i <= N; i++) where[i] = i / base + 1; sort(Q+1, Q+Qnum+1, comp); MoQueue(); fwrite(obuf, O-obuf, 1, stdout); return 0; }
#include<cstdio> #include<cmath> #include<algorithm> using namespace std; const int MAXN=2*1e6+10; 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; } int N,M; int a[MAXN],where[MAXN]; struct Query { int x,y,pre,id; }Q[MAXN]; int Qnum=0; struct Change { int pos,val; }C[MAXN]; int Cnum=0; int color[MAXN],ans=0,base,out[MAXN]; int comp(const Query &a,const Query &b) { if(a.x!=b.x) return where[a.x]<where[b.x]; if(a.y!=b.y) return where[a.y]<where[b.y]; return a.pre<b.pre; } void Add(int val) { if(++color[val]==1) ans++; } void Delet(int val) { if(--color[val]==0) ans--; } void Work(int now,int i) { if(C[now].pos>=Q[i].x&&C[now].pos<=Q[i].y)//注意:只有修改在查询的区间内才会对查询的结果产生影响 { if( --color[a[C[now].pos]] == 0 ) ans--; if( ++color[C[now].val] == 1) ans++; } swap(C[now].val,a[C[now].pos]); //这里有个很巧妙的操做 //对于一个操做,下一次须要为的颜色是本次被改变的颜色 //好比,我把颜色3改成了7,那么再进行此次修改的时候就是把7改成3 //因此直接交换两种颜色就好 } void MoQueue() { int l=1,r=0,now=0; for(int i=1;i<=Qnum;i++) { while(l<Q[i].x) Delet(a[l++]); while(l>Q[i].x) Add(a[--l]); while(r<Q[i].y) Add(a[++r]); while(r>Q[i].y) Delet(a[r--]);//以上四句为莫队模板 while(now<Q[i].pre) Work(++now,i);//改少了,改过去 while(now>Q[i].pre) Work(now--,i);//改多了,改回来 out[Q[i].id]=ans;//统计答案 } for(int i=1;i<=Qnum;i++) printf("%d\n",out[i]); } int main() { N=read();M=read(); base=sqrt(N); for(int i=1;i<=N;i++) a[i]=read(),where[i]=(i-1)/base+1; while(M--) { char opt[5]; scanf("%s",opt); if(opt[0]=='Q') { Q[++Qnum].x=read(); Q[Qnum].y=read(); Q[Qnum].pre=Cnum;//别忘了记录最近的修改位置 Q[Qnum].id=Qnum; } else if(opt[0]=='R') { C[++Cnum].pos=read(); C[Cnum].val=read(); } } sort(Q+1,Q+Qnum+1,comp);//玄学排序 MoQueue(); return 0; }
因为刚开始的$now=0$
因此须要先增后执行
撤销的时候须要将最后一次的修改撤销掉
因此先执行后减
普通莫队时间复杂度为$O\left( n\times \sqrt {n}\right)$
证实:
当咱们第$i$个询问转移的第$i+1$个询问时
也就是说,左端点一直在块内移动的总复杂度为$O(n*\sqrt{n})$(由于左端点最多转移$n$次,减去左端点跨越块的部分,不足$n$)
同时因为右端点升序,那么若$l,l+1,,,r-1,r$的询问区间左端点所在块的编号相等,那么右端点的移动不会超过n次。有一位有$\sqrt{n}$个块,
因此这一部分的复杂度是$O(n*\sqrt{n})$的。
又在这个期间,每次左端点跨越的时候,右端点可能要移动$O(n)$次,一共左端点跨越$\sqrt{n}$个块,因此右端点复杂度是$O(n*\sqrt{n})$的。
综上莫队算法的排序保证时间复杂度是$O(n*\sqrt{n})$的
带修改莫队算法的时间复杂度证实
如下内容借鉴自洛谷题解
原版莫队是将区间$(l,r)$视为点$(l,r)$,带修改的即加一维时间轴$(l,r,t)$
对于$t$轴的移动能够保存每次修改,若是修改在$(l,r)$间则更新
分块方法能够参照原版莫队,先将$l$分块,再讲$r$分块,同一块的按$t$排序
块大小为$\sqrt [3] {nt}$能够达到最快的理论复杂度$O\left( \sqrt [3] {n^{4}t}\right)$,证实以下
设分块大小为$a$,莫队算法时间复杂度主要为$t$轴移动,同$r$块$l,r$移动,$l$块间的$r$移动三部分
$t$轴移动的复杂度为$O\left( \dfrac {n^{2}t}{a^{2}}\right)$,同$r$块$l,r$移动复杂度为$0\left( na\right)$,$l$块间的$r$移动复杂度为$0\left( \dfrac {n}{a}\right)$
三个函数max的最小值当$a$为$\sqrt [3] {nt}$取得,为$O\left( \sqrt [3] {n^{4}t}\right)$