最近入門了莫隊演算法,覺得好玄妙,為自己寫個小結。
我初學莫隊演算法是通過這個部落格理解的:戳這裡,只不過是英文的,其他人也寫過這篇博文的翻譯,特此註明。
下面我的小結也主要圍繞這個問題來談。
考慮這個問題:
給定乙個序列,共有m次詢問,每次詢問區間中出現次數大於等於3的數字有多少個。
我們先來考慮n,m較小的情況。
暴力統計可以解決,用cn
t[n]
表示n在該子串行中的數量,直接統計答案。
int solve(int l,int r)
return rtn;
}
這個樣子做,複雜度看得出來,最壞情況下(每次查詢整個區間)是o(
nm) 的。
我們得想辦法優化一下。
…………
…………
…………
由於我們並沒有涉及到修改操作,所以每乙個a[
i]對答案的貢獻一定是固定的,因而我們在多次查詢中,會重複統計許多a[
i]對答案的貢獻。事實上,上乙個查詢的區間如果與下乙個查詢的區間有交集(甚至重合)的話,我們是完全沒有必要再統計一遍他們公共部分的答案的!
看下面這段**:
int n,m;
int cnt[100010],a[100010];
int ans[100010],now_ans;//now_ans:當前答案 ans[i]:第[i]次詢問的答案
struct queryq[100010];//記錄每個查詢的資訊
void del(int
pos)
void inc(int
pos)
void solve()
while(now_r<=rr)
while(now_lwhile(now_r>rr+1)
ans[i]=now_ans;}}
噫!一下子變得這麼長了!還套進了四個while迴圈和兩個函式!
不要急不要急,如果你能理解這段**,那就是學會了半個莫隊演算法呢!
首先看到solve函式:
這段**的核心思想是只修改上乙個區間與當前區間的不同位置,因此我們需要從記錄的上乙個區間的兩個頂點出發(now_l和now_r),移動到當前查詢的區間兩端點,沿途修改答案。
那麼那麼為什麼是這樣移動的呢?
那麼那麼為什麼還要+1-1什麼的呢?
別急……
對於左端點而言,它到右端點的這一區間是已經統計過了的。那麼如果它還要往右移動,就必然會經過已統計過答案的區間!那麼這就使原來統計過的部分留在了左端點左邊(也就是說,不在我的下乙個統計區間了),因此,左端點要向右移動的話,沿途的部分對答案的貢獻需要被刪除。與此同時,如果向左移動,左端點左邊的區間必然不在我的原區間內,所以向左移動,需要增加它對答案的貢獻。
右端點也是可以模擬的。
至於加一減一,大於等於的問題,我們可以畫出上個區間與下個區間關係的不同情況,自己按照演算法推演一下,也就不難理解了。(其實是我不會qaq)
…… ……
…… 請仔細理解上面的**,然後再往下看。
我們還是不難發現,這個**的複雜度,取決於now_l和now_r的移動次數,最壞情況下,每次查詢,都要從頭已到尾,就依然是個o(
nm) 的演算法,本質上沒什麼提高。
說好的優化呢???
別急……
既然複雜度取決於兩端點的移動,那麼……
我們合理安排一下查詢的順序,讓兩端點科學有效地運動,不就好了嗎?
怎麼排序呢……
now_l:按我排序,按左端點從小到大排序!我最多隻從左到右走一遍!
now_r: →_→那我呢……我每次不是就只能瞎跑了……你倒好只跑一遍,我每查一次都恨不得要跑一趟tot
now_l:(⊙o⊙)…那怎麼辦……
now_r:你看啊,按你排序,我每次的複雜度就是o(
n),按我排序,你的複雜度就是o(
n),所以啊……我們可以委曲求全……搞個o(
n√) 出來。
now_l :蛤?我們妥協是怎麼妥協出o(
n√) 來的?
now_r :→_→你沒學過分塊嗎……
now_l :我只是個端點而已……
now_r :……
我們將1-n分成o(
n√) 個塊,每個塊內大致就有o(
n√) 個元素,每個元素也就有自己對應的塊編號。對於所有的詢問,我們找到左端點,按照它所在塊的編號排序。
now_l:那有好多查詢的塊編號一樣的呢!(now_r:閉嘴!)
對於塊編號相等的詢問,我們就按照右端點的公升序排列。
例如對於如下詢問:
我們先按照塊編號排序:
再對同一塊內的查詢按右端點公升序排序:
按照這個順序處理所有詢問。
現在,我們就面臨莫隊演算法的最後乙個問題:
複雜度?
我們來這麼看:
對於右端點而言,在每個塊中,由於按照公升序排列,所以最多移動n次,由於總共有n√個塊,所以右端點最多移動nn
√ 次。(或者說是n3
2 )
對於左端點而言,每次詢問的移動雖然不確定,但是由於我把同乙個塊的元素排在了一起,所以每次查詢,都相當於在塊內移動,因而最大幅度為mn
√ 次。
綜上,莫隊演算法的複雜度為o(
(n+m
)n√)
即o(nn√)
(或o(
n32)
)。到此,莫隊演算法的基本概念介紹完畢。
這是解決這個問題的最終**:
#include
#include
#include
#include
using namespace std;
int n,m,num;
int cnt[100010],a[100010];
int ans[100010],now_ans;//now_ans:當前答案 ans[i]:第[i]次詢問的答案
struct queryq[100010];//記錄每個查詢的資訊
int cmp(query x,query y)
void inc(int
pos)
void solve()
while(now_r<=rr)
while(now_lwhile(now_r>rr+1)
ans[q[i].id]=now_ans;//q[i]實際上是第q[i].id次詢問
}}int main()
sort(q+1,q+1+m,cmp);//排序
solve();
for(int i=1;i<=m;i++)printf("%d\n",ans[i]) ;
return
0;}
神奇的莫隊演算法,只通過排序就可以將o(
nm) 的演算法重置為o(
nn√)
o(1) 內完成,有可能是o(
logn
) 甚至是o(
n√) ,無形中加大了複雜度。
3.在10
5 的範圍內,莫隊演算法的表現很不錯,甚至可以與一些o(
logn
) 的演算法相近,但是資料規模上到106
的話,莫隊演算法的表現就夠嗆了。比如cf 703d這道題,相關的操作非常適合莫隊,但是由於資料範圍達到106
,儘管時限達到3.5s,但是不加快速讀入輸出以及其他優化的話,很容易得tle。
下面就是我用cena測試的結果:
90以內的資料都在105
以內,91-100規模為200000,101-110為500000,111-120為1000000.
2016.08.27 未完待續
莫隊入門總結
這是一篇適合蒟蒻的講解 大佬可以自行離開 莫隊是一種是離線的演算法 即它在詢問的時候是不會修改的,所以我們可以通過調整詢問的次序來獲得答案。比如區間3到5和區間3到6 他們之間只差了1 於是我們只需要看下新加進來的這個元素對原來答案的影響就好了 對吧?問題是 我們應該如何給詢問區間排序使得時間複雜度...
莫隊演算法入門
昨天重溫了一下captainmo的職業生涯 莫隊的模板,看了下別人的部落格,把三個板子打了,做練習前先小小總結了一下吧。一.基礎莫隊演算法 莫隊演算法 離線 暴力 分塊,它通常用於不修改只查詢的一類區間問題,複雜度為 主要就是通過排序過後再處理詢問能優化暴力,排序則是利用分塊,至於為什麼更優,附張別...
總結 莫隊演算法
機房中的各位神仙都會莫隊就我不會,然後如果有些題實在想不出也可以用這個做一下。如果一些操作可以在知道 ans l,r 的情況下,o 1 的時間內求出 ans l 1,r ans l 1,r ans l,r 1 ans l,r 1 那麼就可以用莫隊求解。將操作離線 按照分塊的思路排序 這是保證複雜度合...