·排序巧妙優化複雜度,帶來NOIP前的最後一絲寧靜。幾個活蹦亂跳的指針的跳躍次數,決定着莫隊算法的優劣……php
·目前的題型歸納爲三種:普通莫隊,樹形莫隊以及帶修莫隊。ios
若談及入門,那麼BZOJ2038的美妙襪子一題堪稱頂尖。算法
【例題一】襪子數組
·述大意: 數據結構
進行區間詢問[l,r],輸出該區間內隨機抽兩次抽到相同顏色襪子的機率。函數
·分析:優化
首先考慮對於一個長度爲n區間內的答案如何求解。題目要求Ans使用最簡分數表示:那麼分母就是n*n-n(表示兩兩襪子之間的隨機組合),分子是一個累加和-n,累加的內容是該區間內每種顏色i出現次數sum[i]的平方。spa
將莫隊算法擡上議程。莫隊算法的思路是,離線狀況下對全部的詢問進行一個美妙的SORT(),而後兩個指針l,r(本題是兩個,其餘的題可能會更多)不斷以看似暴力的方式在區間內跳來跳去,最終輸出答案。 指針
掌握一個思想基礎:兩個詢問之間的狀態跳轉。如圖,當前完成的詢問的區間爲[a,b],下一個詢問的區間爲[p,q],如今保存[a,b]區間內的每一個顏色出現次數的sum[]數組已經準備好,[a,b]區間詢問的答案Ans1已經準備好,怎樣用這些條件求出[p,q]區間詢問的Ans2?code
考慮指針向左或向右移動一個單位,咱們要付出多大的代價才能維護sum[]和Ans(即便得sum[],Ans保存的是當前[l,r]的正確信息)。咱們美妙地對圖中l,r的向右移動一格進行分析:
如圖啦。l指針向右移動一個單位,所形成的後果就是:咱們損失了一個綠色方塊。那麼怎樣維護?美妙地,sum[綠色]減去1。那Ans如何維護?先看分母,分母從n2變成(n-1)2,分子中的其餘顏色對應的部分是不會變的,綠色卻從sum[綠色]2變成(sum[綠色]-1)2 ,爲了方便計算咱們能夠直接向給Ans減去之前該顏色的答案貢獻(即sum[綠色]2)再加上如今的答案貢獻(即(sum[綠色]-1)2 )。同理,觀賞下面的r指針移動,將是差很少的。
·如圖r指針的移動帶來的後果是,咱們多了一個橙色方塊。因此操做和上文類似,只不過是sum[橙色]++。
·迴歸正題地,咱們美妙的發現,知道一個區間的信息,要求出旁邊區間的信息(旁邊區間指的是當前區間的一個指針經過加一減一獲得的區間),竟只須要O(1)的時間。
·就算是這樣,到這裏爲止的話莫隊算法依舊沒法煥發其光彩,緣由是:若是咱們以讀入的順序來枚舉每一個詢問,每一個詢問到下一個詢問時都用上述方法維護信息,那麼在你腦海中會浮現出l,r跳來跳去的瘋狂景象,瘋狂之處在於最壞狀況下時間複雜度爲:O(n2)————若是要這樣玩,那不如寫一個暴力程序。
·「莫隊算法巧妙地將詢問離線排序,使得其複雜度無比美妙……」在通常作題時咱們時常遇到使用排序來優化枚舉時間消耗的例子。莫隊的優化基於分塊思想:對於兩個詢問,若在其l在同塊,那麼將其r做爲排序關鍵字,若l不在同塊,就將l做爲關鍵字排序(這就是雙關鍵字)。大米餅使用Be[i]數組表示i所屬的塊是誰。排序如:
·值得強調的是,咱們是在對詢問進行操做。
·時間複雜度分析(分類討論思想):
首先,枚舉m個答案,就一個m了。設分塊大小爲unit。
分類討論:
①l的移動:若下一個詢問與當前詢問的l所在的塊不一樣,那麼只須要通過最多2*unit步可使得l成功到達目標.複雜度爲:O(m*unit)
②r的移動:r只有在Be[l]相同時纔會有序(其他時候仍是瘋狂地亂跳,你知道,一提到亂跳,那麼每一次最壞就要跳n次!),Be[l]何時相同?在同一塊裏面l就Be[]相同。對於每個塊,排序執行了第二關鍵字:r。因此這裏面的r是單調遞增的,因此枚舉完一個塊,r最多移動n次。總共有n/unit個塊:複雜度爲:O(n*n/unit)
總結:O(n*unit+n*n/unit)(n,m同級,就統一使用n)
根據基本不等式得:當unit爲sqrt(n)時,獲得莫隊算法的真正複雜度:
O(n*sqrt(n))
·代碼上來了(莫隊喜歡while):
1 #include<stdio.h> 2 #include<algorithm> 3 #include<iostream> 4 #include<math.h> 5 #include<cstring> 6 #define go(i,a,b) for(int i=a;i<=b;i++) 7 #define mem(a,b) memset(a,b,sizeof(a)) 8 #define ll long long 9 using namespace std;const int N=50003; 10 struct Mo{int l,r,ID;ll A,B;}q[N];ll S(ll x){return x*x;} 11 ll GCD(ll a,ll b){while(b^=a^=b^=a%=b);return a;} 12 int n,m,col[N],unit,Be[N];ll sum[N],ans; 13 bool cmp(Mo a,Mo b){return Be[a.l]==Be[b.l]?a.r<b.r:a.l<b.l;} 14 bool CMP(Mo a,Mo b){return a.ID<b.ID;}; 15 void revise(int x,int add){ans-=S(sum[col[x]]),sum[col[x]]+=add,ans+=S(sum[col[x]]);} 16 int main() 17 { 18 scanf("%d%d",&n,&m);unit=sqrt(n); 19 go(i,1,n)scanf("%d",&col[i]),Be[i]=i/unit+1;; 20 go(i,1,m)scanf("%d%d",&q[i].l,&q[i].r),q[i].ID=i; 21 22 sort(q+1,q+m+1,cmp); 23 24 int l=1,r=0; 25 go(i,1,m) 26 { 27 while(l<q[i].l)revise(l,-1),l++; 28 while(l>q[i].l)revise(l-1,1),l--; 29 while(r<q[i].r)revise(r+1,1),r++; 30 while(r>q[i].r)revise(r,-1),r--; 31 32 if(q[i].l==q[i].r){q[i].A=0;q[i].B=1;continue;} 33 q[i].A=ans-(q[i].r-q[i].l+1); 34 q[i].B=1LL*(q[i].r-q[i].l+1)*(q[i].r-q[i].l); 35 ll gcd=GCD(q[i].A,q[i].B);q[i].A/=gcd;q[i].B/=gcd; 36 } 37 38 sort(q+1,q+m+1,CMP); 39 go(i,1,m)printf("%lld/%lld\n",q[i].A,q[i].B); 40 return 0; 41 }//Paul_Guderian
【例題二】數顏色
·述大意:
多個區間詢問,詢問[l,r]中顏色的種類數。能夠單點修改顏色。
·分析:
莫隊能夠修改?那不是爆炸了嗎。
這類爆炸的問題被稱爲帶修莫隊(可持久化莫隊)。
按照美妙類比思想,能夠引入一個「修改時間」,表示當前詢問是發生在前Time個修改操做後的。也就是說,在進行莫隊算法時,看看當前的詢問和時間指針(第三個指針,別忘了l,r)是否相符,而後進行時光倒流或者時光推移操做來保證答案正確性。
·Sort的構造。僅靠原來的sort關鍵字會使得枚舉每一個詢問均可能由於時間指針移動的緣故要移動n次,總共就n2次,那還不如寫暴力。
·爲了防止這樣的事情發生,再加入第三關鍵字Tim:
·如何理解時間複雜度?
首先,R和Tim的關係就像L和R的關係同樣:只有在前者處於同塊時,後者纔會獲得排序的恩賜,不然sort會去知足前者,使得後者開始亂跳。
依舊像上文那樣:枚舉m個答案,就一個m了。設分塊大小爲unit。
分類討論:
①對於l指針,依舊是O(unit*n)
②對於r指針,依舊是O(n*n/unit)
③對於T指針(即Time):
類比r時間複雜度的計算。咱們要尋找有多少個單調段(一個單調段下來最多移動n次)。上文提到,當且僅當兩個詢問l在同塊,r也在同塊時,纔會對可憐的Tim進行排序。局勢明朗。對於每個l的塊,裏面r最壞狀況下佔據了全部的塊,因此最壞狀況下:有n/unit個l的塊,每一個l的塊中會有n/unit個r的塊,此時,在一個r塊裏,就會出現有序的Tim。因此Tim的單調段個數爲:(n/unit)*(n/unit)。每一個單調段最多移動n次。
因此:O((n/unit)2*n)
三個指針彙總:O(unit*n+n2/unit+(n/unit)2*n)
·給你個大米餅代碼:
1 #include<stdio.h>
2 #include<algorithm>
3 #include<math.h>
4 #define go(i,a,b) for(int i=a;i<=b;i++)
5 using namespace std;const int N=10003; 6 struct Query{int l,r,Tim,ID;}q[N]; 7 struct Change{int pos,New,Old;}c[N]; 8 int n,m,s[N],color[N*100],t,Time,now[N],unit,Be[N],ans[N],Ans,l=1,r,T; 9 bool cmp(Query a,Query b) 10 { 11 return Be[a.l]==Be[b.l]?(Be[a.r]==Be[b.r]?a.Tim<b.Tim:a.r<b.r):a.l<b.l; 12 } 13 void revise(int x,int d){color[x]+=d;if(d>0)Ans+=color[x]==1;if(d<0)Ans-=color[x]==0;} 14 void going(int x,int d){if(l<=x&&x<=r)revise(d,1),revise(s[x],-1);s[x]=d;} 15 int main(){ 16 scanf("%d%d",&n,&m);unit=pow(n,0.666666); 17 go(i,1,n)scanf("%d",&s[i]),now[i]=s[i],Be[i]=i/unit+1; 18 go(i,1,m){char sign;int x,y;scanf(" %c %d%d",&sign,&x,&y); 19 if(sign=='Q')q[++t]=(Query){x,y,Time,t}; 20 if(sign=='R')c[++Time]=(Change){x,y,now[x]},now[x]=y; 21 } 22 sort(q+1,q+t+1,cmp);go(i,1,t) 23 { 24 while(T<q[i].Tim)going(c[T+1].pos,c[T+1].New),T++; 25 while(T>q[i].Tim)going(c[T].pos,c[T].Old),T--; 26
27 while(l<q[i].l)revise(s[l],-1),l++; 28 while(l>q[i].l)revise(s[l-1],1),l--; 29 while(r<q[i].r)revise(s[r+1],1),r++; 30 while(r>q[i].r)revise(s[r],-1),r--; 31
32 ans[q[i].ID]=Ans; 33 } 34 go(i,1,t)printf("%d\n",ans[i]);return 0; 35 }//Paul_Guderian
【例題三】達到頂尖
·述大意:
一棵樹,能夠單點修改一個節點的權值,許多詢問和修改,詢問(u,v)表示u到v的路徑上,求出最小的沒有出現的天然數。
·分析:
帶修莫隊+樹形莫隊。要爆炸了。
上文解決了爆炸的帶修莫隊,如何處理樹形莫隊?
·樹形莫隊引入的第一個難點是:如何分塊。注意,分塊的目的是爲了快速訪問與查找(例如上文在分析l指針時間複雜度的時候,發現每次最多移動
unit*2次,這就是由於即便是跨越了塊,這兩個塊的相鄰關係使得時間複雜度不會改變)。
·嘗試在樹上構造相鄰的塊,使得:塊內元素的互相訪問的移動次數控制在一個範圍內(也就是unit)。作法是用棧維護當前節點做爲父節點訪問它的子節點,當從棧頂到父節點的距離大於unit時,彈出這部分元素分爲一塊。
如圖:
(另外,對於剩餘分塊的節點,也就是根節點附近因爲個數小於unit而造成的一坨點,最後再分一塊或加在最後一塊中)
·強調這樣作的好處:使得每個塊內的點到達另外一個點最多移動unit次。
那麼對於sort()就和第二題同樣了。
·接下來還有一個區間移動(即指針u,v,T的移動)沒有處理。很明顯,這道題的樹上路徑的維護又是一個美妙的東西。與上幾道題不一樣的是,u,v指針是在樹上移動。若是當前路徑(u,v)已處理好,下一個詢問是到達(u1,v1).那麼咱們能夠將u一步一步的移動到u1,一路上咱們歡聲笑語,走一個點就記錄上面的天然數使用vis[u]標記這個節點來沒來過,使用抑或就能夠輕鬆求出訪問狀態,v到v1也能夠這樣作。
另外,維護當前已收集的天然數,能夠用離散化+數據結構。(可是這道題好像有BUG,不須要離散化)。給出的代碼用的方法是用分塊維護,但過後想一想,發現樹狀數組可能更美妙。
·這一切都獲得解決,就在代碼要到來時,你偷看了代碼,發現裏面有一個函數叫作LCA!什麼,哪裏要用到倍增求公共祖先?一張圖以下:
·這樣的問題在什麼點出現?u1,v1的最近公共祖先。因此,咱們上文維護天然數的數據結構(u,v)改爲:表示u到v路徑上除開他們的LCA的其餘點的信息,每次u,v歸位後,咱們單獨爲LCA計算一次,這樣既避免了怪異狀況影響答案,有保證了LCA對答案的貢獻。
·網上對這種路徑問題還有一種本質相同出發點不一樣的妙解,它也能幫助理解爲何會有怪異狀況:求出該樹的歐拉序(相似於dfs序,但每一個點有頭有尾),那麼對於(u,v)路徑,就是在序列中僅出現一次的數字。這樣作一樣也要處理公共祖先卡機的怪異狀況,畫圖看看吧。
·終於出場的大米餅代碼:
1 #include<stdio.h>
2 #include<algorithm>
3 #include<math.h>
4 #define go(i,a,b) for(int i=a;i<=b;i++)
5 #define ro(i,a,b) for(int i=a;i>=b;i--)
6 #define fo(i,a,x) for(int i=a[x],v=e[i].v;i;i=e[i].next,v=e[i].v)
7 using namespace std;const int N=50009; 8 struct E{int v,next;}e[N*3]; 9 int k=1,head[N],unit,Be[N],m,st[N],top,fa[N][18],deep[N]; 10 int n,Q,a[N],t[N],op,x,y,p,tim,u=1,v=1,T,ans[N],vis[N]; 11 void ADD(int u,int v){e[k]=(E){v,head[u]};head[u]=k++;} 12 void dfs(int u){ 13
14 go(i,1,19)if((1<<i)>deep[u])break; 15 else fa[u][i]=fa[fa[u][i-1]][i-1]; 16
17 int bottom=top; 18 fo(i,head,u)if(v!=fa[u][0]) 19 { 20 fa[v][0]=u;deep[v]=deep[u]+1;dfs(v); 21 if(top-bottom>=unit){m++;while(top!=bottom)Be[st[top--]]=m;} 22 } 23 st[++top]=u; 24 } 25 int LCA(int x,int y) 26 { 27 if(deep[x]<deep[y])swap(x,y);int Dis=deep[x]-deep[y]; 28 go(i,0,16)if((1<<i)&Dis)x=fa[x][i]; 29 if(x==y)return x; 30 ro(i,16,0)if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i]; 31 return x==y?x:fa[x][0]; 32 } 33 struct Change{int u,New,Old;}cq[N]; 34 struct Query{int u,v,tim,id;bool operator <(const Query &a) const{ 35 return Be[u]==Be[a.u]?(Be[v]==Be[a.v]?tim<a.tim:Be[v]<Be[a.v]):Be[u]<Be[a.u]; 36 }}q[N]; 37 struct Datalock{ 38 struct _blo{int l,r;}b[350]; 39 int n,Be[N],m,unit,num[N],sum[350]; 40 void init() 41 { 42 unit=sqrt(n);m=(n-1)/unit+1; 43 go(i,1,n)Be[i]=(i-1)/unit+1; 44 go(i,1,m)b[i].l=(i-1)*unit+1,b[i].r=i*unit; 45 b[m].r=n; 46 } 47 void Add(int v){if(v<=n)sum[Be[v]]+=(++num[v])==1;} 48 void Del(int v){if(v<=n)sum[Be[v]]-=(--num[v])==0;} 49 int mex() 50 { 51 go(i,1,m)if(sum[i]!=b[i].r-b[i].l+1) 52 go(j,b[i].l,b[i].r)if(!num[j])return j; 53 return -1; 54 } 55 }Data; 56 void revise(int u,int d){if(vis[u])Data.Del(a[u]),Data.Add(d);a[u]=d;} 57 void Run(int u){if(vis[u])Data.Del(a[u]),vis[u]=0;else Data.Add(a[u]),vis[u]=1;} 58 void move(int x,int y) 59 { 60 if(deep[x]<deep[y])swap(x,y); 61 while(deep[x]>deep[y])Run(x),x=fa[x][0]; 62 while(x!=y)Run(x),Run(y),x=fa[x][0],y=fa[y][0]; 63 } 64 void Mo() 65 { 66 go(i,1,p) 67 { 68 while(T<q[i].tim)T++,revise(cq[T].u,cq[T].New); 69 while(T>q[i].tim)revise(cq[T].u,cq[T].Old),T--; 70
71 if(u!=q[i].u)move(u,q[i].u),u=q[i].u; 72 if(v!=q[i].v)move(v,q[i].v),v=q[i].v; 73 int anc=LCA(u,v);Run(anc);ans[q[i].id]=Data.mex()-1;Run(anc); 74 } 75 } 76 int main(){scanf("%d%d",&n,&Q);unit=pow(n,0.45); 77 go(i,1,n)scanf("%d",&a[i]),t[i]=++a[i]; 78 go(i,2,n){int uu,vv;scanf("%d%d",&uu,&vv);ADD(uu,vv);ADD(vv,uu);} 79 dfs(1);while(top)Be[st[top--]]=m; 80 go(i,1,Q) 81 { 82 scanf("%d%d%d",&op,&x,&y); 83 if( op)p++,q[p]=(Query){x,y,tim,p}; 84 if(!op)tim++,cq[tim]=(Change){x,y+1,t[x]},t[x]=y+1; 85 } 86 Data.n=n+1;Data.init();sort(q+1,q+1+p);Mo(); 87 go(i,1,p)printf("%d\n",ans[i]); 88 }//Paul_Guderian
[小小總結]
莫隊算法適用條件是比較苛刻的嗎?是的。
①題目必須離線
②可以以極少的時間推出旁邊區間(通常是O(1))
③沒有修改或者修改不太苛刻
④基於分塊,分塊不行,它也好不了哪裏去(況且如今還有可持久化數據結構維護的分塊)
但莫隊的思想美妙,代碼優美,你值得擁有。莫隊的排序思想也爲衆多離線處理的題目提供了完整的思路。
我想告別不堪回首破碎的過去,告別滿身的糾結和沉迷。
在那萬里以外遼闊寂寞的遠方,是我拋棄的上千個黎明。————汪峯《上千個黎明》