update in 2017.12.24:
以前寫的≈shit,實在看不下去了,重寫一遍
pre
很早之前就學習了莫隊演算法。
老師講課的時候就提到過帶修改莫隊線上莫隊樹上莫隊樹上帶修改莫隊……但是一直都沒有做到過有關的題,
今天有幸做了一道裸的帶修改莫隊的題,
那就來分享一下自己的經驗
帶修改的莫隊
首先我們要知道,普通的莫隊演算法是不資瓷修改操作的,
不過後人對莫隊演算法加以改進
發明了資瓷修改的莫隊演算法
思路:
在進行修改操作的時候,修改操作是會對答案產生影響的(廢話)
那麼我們如何避免修改操作帶來的影響呢?
首先我們需要把查詢操作和修改操作分別記錄下來。
在記錄查詢操作的時候,需要增加一個變數來記錄離本次查詢最近的修改的位置
然後套上莫隊的板子,與普通莫隊不一樣的是,你需要用一個變數記錄當前已經進行了幾次修改
對於查詢操作,如果當前改的比本次查詢需要改的少,就改過去
反之如果改多了就改回來
說的聽繞口的
比如,我們現在已經進行了3次修改,本次查詢是在第5次修改之後,那我們就執行第4,5次修改
這樣就可以避免修改操作對答案產生的影響了
code
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$個詢問時
- 如果第$i$個詢問區間和第$i+1$個詢問區間的左端點所在塊的編號相同,那麼左端點的移動不會超過$\sqrt{n}$。
也就是說,左端點一直在塊內移動的總複雜度為$O(n*\sqrt{n})$(因為左端點最多轉移$n$次,減去左端點跨越塊的部分,不足$n$)
同時由於右端點升序,那麼若$l,l+1,,,r-1,r$的詢問區間左端點所在塊的編號相等,那麼右端點的移動不會超過n次。有一位有$\sqrt{n}$個塊,
所以這一部分的複雜度是$O(n*\sqrt{n})$的。
- 考慮左端點跨越塊的情況,每次跨越最大是$O(2*\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)$