莫隊算法~講解


用了大約1h搞定了基礎的莫隊算法。寫篇博客算是檢驗下自己的學習成果。

一.什么是莫隊算法?

莫隊算法是用來處理一類無修改的離線區間詢問問題。——(摘自前國家隊隊長莫濤在知乎上對莫隊算法的解釋。)

莫隊算法是前國家隊隊長莫濤在比賽的時候想出來的算法。

傳說中能解決一切區間處理問題的莫隊算法。

准確的說,是離線區間問題。但是現在的莫隊被拓展到了有樹上莫隊帶修莫隊(即帶修改的莫隊)。這里只先講普通的莫隊

還有一點,重要的事情說三遍!莫隊不是提莫隊長!莫隊不是提莫隊長!!莫隊不是提莫隊長!!!

二.為什么要使用莫隊算法?

看一個例題:給定一個n(n<50000)元素序列,有m(m<200000)個離線查詢。每次查詢一個區間L~R,問每個元素出現次數為k的有幾個。(必須恰好是k,不能大於也不能小於)

我們很容易想到用線段樹或者樹狀數組直接做,但是我們想,如果是用線段樹或者樹狀數組做而且我們不會優化的話(請dalao無視掉,您可以直接線段樹做了。)每次修改和維護會很麻煩,線

段樹和樹狀數組的優勢體現不出來。

這時候就要使用莫隊算法了。

三.莫隊算法的思想怎么理解?

接着上面的例題,直接暴力怎么樣??

肯定會T的啊。(luogu P1972 [SDOI2009]HH的項鏈 原數據居然可以mn模擬過......當然現在不行了)

但是如果這個暴力我們給優化一下呢?

我們想,有兩個指針curL和curR,curL指向L,curR指向R。

L和R是一個區間的左右兩端點。

利用cnt[]記錄每個數出現的次數,每次只是cnt[a[curL]] cnt[a[curR]]修改。

舉個栗子:

我們現在處理了curL—curR區間內的數據,現在左右移動,比如curL到curL-1,只需要更新上一個新的3,即curL-1。

那么curL到curL+1,我們只需要去除掉當前curL的值。因為curL+1是已經維護好了的。

curR同理,但是要注意方向哦!curR到curR+1是更新,curR到cur-1是去除。

我們先計算一個區間[curL curR]的answer,這樣的話,我們就可以用O(1)轉移到[curL-1 curR] [curL+1 curR] [curL curR+1] [curL curR-1]上來並且求出這些區間的answer。

我們利用curL和curR,就可以移動到我們所需要求的[L R]上啦~

這樣做會快很多,但是......

如果有個**數據,讓你在每個L和R間來回跑,而且跨度很大呢??

我們每次只動一步,豈不是又T了??

但是這其實就是莫隊算法的核心了。我們的莫隊算法還有優化。

這就是莫隊算法最精明的地方(我認為的qwq),也正是有了這個優化,莫隊算法被稱為:優雅的暴力

我們想,因為每次查詢是離線的,所以我們先給每次的查詢排一個序。

排序的方法是分塊。

我們把所有的元素分成多個塊(即分塊)。分了塊跑的會更快。再按照右端點從小到大,左端點塊編號相同按右端點從小到大。

這樣對於不同的查詢

例如:

我們有長度為9的序列。

1 2 3 4 5 6 7 8 9 分為1——3 4——6 7——9

查詢有7組。[1 2] [2 1000] [1 3] [6 9] [5 8] [3 8] [8 9]

排序后就是:[1 2] [1 3] [3 8] [2 1000] | [5 8] [6 9] | [8 9]

然后我們按照這個順序移動指針就好啦~

這樣,不斷地移動端點指針+精妙的排序,就是普通莫隊的思想啦~

時間復雜度證明

關於時間復雜度的證明:給一個角度,其實從不同的角度看,證法很多: 對於左端點在一個塊中時,右端點最壞情況是從盡量左到盡量右,所以右端點跳時間復雜度O(n),左端點一共可以在n0.5個塊中,所以總時間復雜度O(n*n0.5) = (n1.5)。

四.具體代碼實現:

1.對於每組查詢的記錄和排序:

l,r為左右區間編號,p是第幾組查詢的編號

1 struct query{
2     int l, r, p;
3 }e[maxn];
4 
5 bool cmp(query a, query b)
6 {
7     return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l < b.l;
8 }

2.處理和初始變量:

answer就是所求答案,bl是分塊數量,a[]是原序列,ans[]是記錄原查詢序列下的答案,cnt[]是記錄對於每個數i,cnt[i]表示i出現過的次數,curL和curR不再解釋,nmk題意要求。

 1 int answer, a[maxn], m, n, bl, ans[maxn], cnt[maxn], k, curL = 1, curR = 0;
 2 void add(int pos)//添加 
 3 {
 4     //do sth...
 5 }
 6 void remove(int pos)//去除 
 7 {
 8     //do sth...
 9 }
10 //一般寫法都是邊處理 邊根據處理求答案。cnt[a[pos]]就是在pos位置上原序列a出現的次數。 

3.主體部分及輸出:

預處理查詢編號,用四個while移動指針順便處理。

在這里着重說下四個while

我們設想有一條數軸:

當curL < L 時,我們當前curL是已經處理好的了。所以remove時先去除當前curL再++

當curL > L 時,我們當前curL是已經處理好的了。所以 add  時先--再加上改后curL

當curR > R 時,我們當前curR是已經處理好的了。所以remove時先去除當前curR再--

當curR < R 時,我們當前curR是已經處理好的了。所以 add  時先++再加上改后curR

 1   n = read(); m = read(); k = read();
 2     bl = sqrt(n);
 3 
 4     for(int i = 1; i <= n; i++)
 5     a[i] = read();
 6     
 7     for(int i = 1; i <= m; i++)
 8     {
 9         e[i].l = read(); e[i].r = read();
10         e[i].p = i;
11     }
12     
13     sort(e+1,e+1+m,cmp);
14     
15     for(int i = 1; i <= m; i++)
16     {
17         int L = e[i].l, R = e[i].r;
18         while(curL < L)
19         remove(curL++);  
20         while(curL > L)
21         add(--curL);
22         while(curR > R)
23         remove(curR--);
24         while(curR < R)
25         add(++curR);
26         ans[e[i].p] = answer;
27     }
28     for(int i = 1; i <= m; i++)
29     printf("%d\n",ans[i]);
30     return 0;

五.實戰莫隊:

【luogu P1972 [SDOI2009]HH的項鏈

https://www.luogu.org/problemnew/show/P1972

因為原來數據被大模擬過了,所以數組50000要多開。add和remove根據不同情況處理,如果當前有相同的了再++肯定不是1,如果當前相同的不止一個,remove--的時候肯定不是0,不會造成影響。反之則可以判斷有多少是不同元素。

 1 //HH的項鏈 
 2 #include <cstdio>
 3 #include <algorithm>
 4 #include <iostream>
 5 #include <cmath>
 6 using namespace std;
 7 const int maxn = 200001;
 8 const int maxm = 500001;
 9 int m, n, bl, answer, curL = 1, curR = 0, ans[maxn], a[maxn], cnt[maxm];//a是原序列 cnt是記錄每個數字出現的次數
10 inline int read()
11 {
12     int k=0;
13     char c;
14     c=getchar();
15     while(!isdigit(c))c=getchar();
16     while(isdigit(c)){k=(k<<3)+(k<<1)+c-'0';c=getchar();}
17     return k;
18 }
19 struct query{
20     int l, r, p;//l 左區間     r 右區間     p 位置的編號 
21      /*friend bool operator < ( query a, query b ) {
22         return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l<b.l ;
23     }*/
24 }e[maxn];
25 bool cmp(query a, query b) 
26 {
27     return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l<b.l;
28 }
29 void add(int pos)
30 {
31     if((++cnt[a[pos]]) == 1) ++answer;
32 }
33 void remove(int pos)
34 {
35     if((--cnt[a[pos]]) == 0) --answer;
36 }
37 int main()
38 {
39     n = read();
40     for(int i = 1; i <= n; i++)
41     a[i] = read();
42     
43     m = read();
44     
45     bl = sqrt(n);
46     
47     for(int i = 1; i <= m; i++)
48     {
49         e[i].l = read(); e[i].r = read();
50         e[i].p = i;
51     }
52     sort(e+1,e+1+m,cmp);
53     
54     for(int i = 1; i <= m; i++)
55     {
56         int L = e[i].l, R = e[i].r;
57         while(curL < L)
58             remove(curL++);
59         while(curL > L)
60             add(--curL);
61         while(curR > R)
62             remove(curR--);
63         while(curR < R)
64             add(++curR);
65         ans[e[i].p] = answer;
66     }
67     for(int i = 1; i <= m; i++)
68     printf("%d\n",ans[i]);
69     return 0;
70 }

【luogu P2709 小B的詢問

https://www.luogu.org/problemnew/show/P2709#sub

add和remove對於平方相加減的運算利用完全平方式逆回去。

1^2 = 1;
2^2 = (1+1)^2 = 1 + 1*2 + 1;
3^2 = (1+2)^2 = 1 + 2*2 + 4;
4^2 = (1+3)^2 = 1 + 3*2 + 9;
......

//小B的詢問 
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int maxn = 50001;
int answer, a[maxn], m, n, bl, ans[maxn], cnt[maxn], k, curL = 1, curR = 0;
void add(int pos)
{
    answer+=(((cnt[a[pos]]++)<<1)+1);//完全平方式展開 
}
void remove(int pos)
{
    answer-=(((--cnt[a[pos]])<<1)+1);//完全平方式展開
}
inline int read()
{
    int k=0;
    char c;
    c=getchar();
    while(!isdigit(c))c=getchar();
    while(isdigit(c)){k=(k<<3)+(k<<1)+c-'0';c=getchar();}
    return k;
}
struct query{
    int l, r, p;
}e[maxn];
bool cmp(query a, query b)
{
    return (a.l/bl) == (b.l/bl) ? a.r < b.r : a.l < b.l;
}
int main()
{
    n = read(); m = read(); k = read();
    bl = sqrt(n);

    for(int i = 1; i <= n; i++)
    a[i] = read();
    
    for(int i = 1; i <= m; i++)
    {
        e[i].l = read(); e[i].r = read();
        e[i].p = i;
    }
    
    sort(e+1,e+1+m,cmp);
    
    for(int i = 1; i <= m; i++)
    {
        int L = e[i].l, R = e[i].r;
        while(curL < L)
        remove(curL++);  
        while(curL > L)
        add(--curL);
        while(curR > R)
        remove(curR--);
        while(curR < R)
        add(++curR);
        ans[e[i].p] = answer;
    }
    for(int i = 1; i <= m; i++)
    printf("%d\n",ans[i]);
    return 0;
}

這兩個題我都用了快讀在里面。可以摘下來當板子背。

最后!我要吐槽一句!!luogu試煉場線段樹和樹狀數組的題!我線段樹一個也過不了!(我真是太蒟蒻了)所以還是莫隊大法好!

這是幾篇我學莫隊時參考的博客,如果覺得我講的不夠詳細,可以借鑒。

https://blog.csdn.net/wzw1376124061/article/details/67640410

https://zhuanlan.zhihu.com/p/25017840

https://www.cnblogs.com/Paul-Guderian/p/6933799.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM