莫隊演算法講解

2021-08-07 04:09:54 字數 3394 閱讀 8947

問題:有n個數組成乙個序列,有m個形如詢問l, r的詢問,每次詢問需要回答區間內至少出現2次的數有哪些。

樸素的解法需要讀取o(nm)次數。如果資料範圍小,可以用陣列,時間複雜度為o(nm)。如果使用stl的map來儲存出現的次數,則需要o(nmlogn)的複雜度。有沒有更快的方法呢?

得出。如果能安排適當的詢問順序,使得每次詢問都能用上上次執行產生的中間變數,那麼我們將可以在更優的複雜度完成整個詢問。

(1) 如果資料較小,用陣列,時間複雜度為o(1);如果資料較大,可以考慮用離散化或map,時間複雜度為o(logn)。

那如何安排詢問呢?這裡有個時間複雜度非常優秀的方法:首先將每個詢問視為乙個「點」,兩個點p1, p2之間的距離為abs(l1 - l2) + abs(r1 - r2),即曼哈頓距離,然後求這些點的最小生成樹,然後沿著樹邊遍歷一次。由於這裡的距離是曼哈頓距離,所以這樣的生成樹被稱為「曼哈頓最小生成樹」。最小曼哈頓生成樹有專用的演算法(2),求生成樹時間複雜度可以僅為o(mlogm)。

(2) 其實這裡是建邊的演算法,建邊後依然使用傳統的prim或者kruskal演算法來求最小生成樹。

不幸的是,曼哈頓最小生成樹的寫法很複雜,考場上不建議這樣做。 

一種直觀的辦法是按照左端點排序,再按照右端點排序。但是這樣的表現不好。特別是面對精心設計的資料,這樣方法表現得很差。

舉個例子,有6個詢問如下:(1, 100), (2, 2), (3, 99), (4, 4), (5, 102), (6, 7)。

這個資料已經按照左端點排序了。用上述方法處理時,左端點會移動6次,右端點會移動移動98+97+95+98+95=483次。右端點大幅度地來回移動,嚴重影響了時間複雜度——排序的複雜度是o(mlogm),所有左端點移動次數僅為為o(n),但右端點每個詢問移動o(n),共有m個詢問,故總移動次數為o(nm),移動總數為o(mlogm + nm)。執行時間上界並沒有減少。

其實我們稍微改變一下詢問處理的順序就能做得更好:(2, 2), (4, 4), (6, 7), (5, 102), (3, 99), (1, 100)。

左端點移動次數為2+2+1+2+2=9次,比原來稍多。右端點移動次數為2+3+95+3+1=104,右端點的移動次數大大降低了。

上面的過程啟發我們:①我們不應該嚴格按照公升序排序,而是根據需要靈活一點的排序方法;②如果適當減少右端點移動次數,即使稍微增多一點左端點移動次數,在總的複雜度上看,也是划算的。

在排序時,我們並不是按照左右端點嚴格公升序排序詢問,而只是令其左右端點處於「大概是公升序」的狀態。具體的方法是,把所有的區間劃分為不同的塊,將每個詢問按照左端點的所在塊序號排序,左端點塊一樣則按照右端點排序。注意這個與上乙個版本的不同之處在於「第一關鍵字」是左端點所在塊而非左端點。

這就是莫隊演算法。為什麼叫莫隊演算法呢?據說這是2023年國家集訓隊的莫濤(3)在作業裡提到了這個方法。

(3) 由於莫濤經常打比賽做隊長,大家都叫他莫隊,該演算法也被稱為莫隊演算法。(感謝汝佳大神、莫隊的指出)

莫隊演算法首先將整個序列分成√n個塊(同樣,只是概念上分的塊,實際上我們並不需要嚴格儲存塊),接著將每個詢問按照塊序號排序(一樣則按照右端點排序)。之後,我們從排序後第乙個詢問開始,逐個計算答案。

int len;    // 塊長度

struct query  // 建構函式過載

query(int l, int r, int id):l(l), r(r), id(id)

bool operator < (const query rhs) const

}queries[maxm];

mapbuf;

inline void insert(int n)

inline void erase(int n)

int a[maxn];        // 原序列

queueanss[maxm];  // 儲存答案

int main()

cin >> m;

for(int i = 1; i <= m; i++)

sort(queries + 1, queries + m + 1);

int l = 1, r = 1;

buf[a[1]] = 1;

for(int i = 1; i <= m; i++)}}

for(int i = 1; i <= m; i++)

cout << endl;}}

儘管分了塊,但是我們可以對所有的「詢問轉移」一視同仁。上述的**有幾個需要注意的地方。

一是insert和erase,這裡在插入前判斷了是否存在、插入後判斷是否為0,但這不是必須的(insert時會將新節點初始化為0,erase為0後對處理答案不影響);

二是區間變化的順序,insert最好放在前面,erase最好在後面(想一想,為什麼);

三是insert總是使用字首自增自減運算子,erase總是用字尾運算子;

四是我們在訪問我們在「詢問轉移」前宣告了query的引用,來減少執行時定址的計算量;

五是我們過載了query的建構函式。為什麼要過載呢?

我們希望在query得到l, r, id時自動計算塊block,這就要寫乙個建構函式query(int l, int r, int id)來實現。但是,當結構體沒有建構函式,例項化時不會初始化,有建構函式則一定會呼叫建構函式進行初始化。「託他的福」,queries陣列建立時會對每個元素呼叫一次建構函式。可是我們只有有3個引數的建構函式,構造時一定要有3個引數。而建立陣列時卻沒有引數,編譯器會報錯。折中的辦法是寫乙個沒有引數的建構函式,可以避免這一問題。

這樣排序有個特點。l和r都是「大概是公升序」。不過l大概像爬山,總體上公升但是會有區域性的小幅度下降。r則有些難以形容,大概可以看出其由很多段快速上公升,每段上公升到頂端後下降到最底。

下面是隨機生成100個資料,將資料放到wps**後製成圖表後的樣子。

還有乙個問題,為什麼分塊要分成√n塊呢?我們分析一下時間複雜度。

假設我們每k個點分一塊。

如果當前詢問與上一詢問左端點處在同一塊,那麼左端點移動為o(k)。雖然右端點移動可能高達o(n),但是整一塊詢問的右端點移動距離之和也是o(n)(想一想,為什麼)。因此平攤意義下,整塊移動為o(k) × o(k) + o(n),一共有n / k塊,時間複雜度為o(kn + n2 / k)。

如果詢問與上一詢問左端點不處於同一塊,那麼左端點移動為o(k),但右端點移動則高達o(n)。幸運的是,這種情況只有o(n / k)個,時間複雜度為o(n + n2 / k)。

總的移動次數為o(kn + n2 / k)。因此,當k = √n時,執行時間上界最優,為o( n1.5 )。

最後,因此根據每次insert和erase的時間複雜度,乘上o(1)或者o(logn)亦或o(n)不等,得到完整演算法的時間複雜度(**使用了map,為o( logn ))。

參考:莫隊演算法學習筆記

莫隊演算法 講解 更新

莫隊演算法是用來處理一類無修改的離線區間詢問問題。摘自前國家隊隊長莫濤在知乎上對莫隊演算法的解釋。莫隊演算法是前國家隊隊長莫濤在比賽的時候想出來的演算法。傳說中能解決一切區間處理問題的莫隊演算法。準確的說,是離線區間問題。但是現在的莫隊被拓展到了有樹上莫隊,帶修莫隊 即帶修改的莫隊 這裡只先講普通的...

莫隊講解 普通莫隊

結束了分塊,我們來講下莫隊。據我所知,莫隊能解決一切區間問題,除了翻轉。因為它就是個暴力 其實這兩者的關係並不大。僅僅是時間複雜度一樣而已。我們把原序列分成 n塊 好像就是這裡相同 這裡說的序列是查詢序列l r,並不是讀入的a i 之後我們把序列排序 按照第一關鍵字為左端點所在的塊的大小,如果相同就...

HDU4638 莫隊演算法,講解)

解題思路 第一次做莫隊演算法,推薦部落格入門。結合上面那片部落格,我先講一下自己對莫隊演算法時間複雜度的理解。首先我們將乙個block設為sqrt n n為數列的長度。然後按提的問題的區間進行排序,排序的規則是 這麼做的目的是使l和r指標的移動複雜度盡可能相等。1.首先對l的移動複雜度進行分析。l最...