莫隊算法~講解【更新】

一.什麼是莫隊算法?

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

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

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

準確的說,是離線區間問題。可是如今的莫隊被拓展到了有樹上莫隊,帶修莫隊(即帶修改的莫隊)。這裏只先講普通的莫隊。算法

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

二.爲何要使用莫隊算法?

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

這時候dalao們就直接樹狀數組線段樹主席樹拍上去了,身爲蒟蒻的我躲在角落瑟瑟發抖。該怎麼辦?spa

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

三.莫隊算法的思想?

接着上面的例題,直接暴力怎麼樣??設計

確定會T的啊。指針

可是若是這個暴力咱們給優化一下呢?

咱們想,有兩個指針 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$仍是$T$...

怎麼辦?

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

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

咱們想,由於每次查詢是離線的,因此咱們先給每次的查詢排一個序。

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

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

這個數據已經按照左端點排序了。用上述方法處理時,左端點會移動6次,右端點會移動移動98+97+95+98+95=483次。

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

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

上面的過程啓發咱們:

咱們不該該嚴格按照升序排序,而是根據須要靈活一點的排序方法

那麼排序的方法是

分塊。

咱們把全部的元素分紅多個塊。再按照左端點塊編號從小到大排序,左端點塊編號相同按右端點塊編號從小到大。

這樣對於不一樣的查詢

例如:

咱們有長度爲9的序列。

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

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

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

而後咱們按照這個順序移動指針就好啦~

時間複雜度證實

其實從不一樣的角度看,證法不少:對於左端點在一個塊中時,右端點最壞狀況是從儘可能左到儘可能右,因此右端點跳時間複雜度O(n),左端點一共能夠在n^0.5個塊中,因此總時間複雜度O(n*n^0.5) = O(n^1.5)。

四.具體代碼實現:

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

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

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;
}

2.處理和初始變量:

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

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

3.主體部分及輸出:

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

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;

在這裏着重說下四個while

當curL < L 時,咱們當前curL是已經處理好的了。因此remove時先去除當前curL再++

當curL > L 時,咱們當前curL是已經處理好的了。因此 add 時先--再加上改後curL

當curR > R 時,咱們當前curR是已經處理好的了。因此remove時先去除當前curR再--

當curR < R 時,咱們當前curR是已經處理好的了。因此 add 時先++再加上改後curR

五.一些例題

【luogu P2709 小B的詢問】

$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 P4462 [CQOI2018]異或序列】

ax+ax-1+...+ay = cntx+cnty 這樣把一段序列變成兩段相加跑莫隊。

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
const int maxn = 200010;
int curR = 0, curL = 1, answer,a[maxn], ans[maxn], cnt[maxn], n, m, k, bl;
struct query{
    int l,r,p;
}q[maxn];

bool cmp(const query &a, const query &b)
{
    return (a.l/bl) == (b.l/bl) ? a.r<b.r : a.l<b.l;
}

inline void add(int pos)
{
    cnt[a[pos]]++;
    answer+=cnt[a[pos]^k];
}

inline void remove(int pos)
{
    cnt[a[pos]]--;
    answer-=cnt[a[pos]^k];
}

int main()
{
    scanf("%d%d%d",&n,&m,&k);
    bl = sqrt(n);
    cnt[0] = 1;
    for(int i = 1; i <= n; i++)
    {
        scanf("%d",&a[i]);
        a[i] ^= a[i-1]; 
    }
    
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d",&q[i].l,&q[i].r);
        q[i].p = i;
    }
    
    sort(q+1,q+1+m,cmp);
    
    for(int i = 1; i <= m; i++)
    {
        while(curL < q[i].l) remove(curL-1),curL++;
        while(curL > q[i].l) curL--,add(curL-1);
        while(curR < q[i].r) add(++curR);
        while(curR > q[i].r) remove(curR--);
        ans[q[i].p] = answer;
    }
    for(int i = 1; i <= m; i++)
    printf("%d\n",ans[i]);
    return 0;
}

六.總結

莫隊算法適用條件是比較苛刻的嗎?是的。 ①題目必須離線

②可以以極少的時間推出旁邊區間(通常是O(1))

③沒有修改或者修改不太苛刻

④基於分塊,分塊不行,它也好不了哪裏去

但莫隊的思想美妙,代碼優美,你值得擁有。莫隊的排序思想也爲衆多離線處理的題目提供了完整的思路。

咱們能夠看出來,對於莫隊這種模擬式的暴力算法很好理解。 也比較實用,亂搞神器不是浪得虛名。

莫隊是我自學的,因此在文章裏或許會有些我的理解上的誤差,還請各位dalao能賜教。自學能力是須要培養、鍛鍊的。

其次是也引用了一些dalao寫的很好的文章,也是我自學時用到的資料:

這是幾篇我學莫隊時參考的博客,若是以爲我講的不夠詳細,能夠借鑑。

qvq

qaq

qnq

祝各位OI路途能越走越順!

本蒟蒻$QQ$ 935145183/3203600070

蒟蒻博客園地址

相關文章
相關標籤/搜索