1. 程式人生 > 其它 >初見 | 資料結構 | 莫隊入門

初見 | 資料結構 | 莫隊入門

初見,莫隊,可愛,快樂,愉悅,單推!

前言

因為很好奇莫隊是個啥於是昨天就去學了一下,不過只學到了普通莫隊和帶修莫隊,剩下的可能以後再補。

預設源

算是老套路了(

可能是因為中間隔了一晚上所以所給出例題的程式碼實現略有不同,不過碼風是一樣的(

#include <iostream>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <string.h>
#define Heriko return
#define Deltana 0
#define Romanno 1
#define S signed
#define LL long long
#define R register
#define I inline
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
#define ON std::ios::sync_with_stdio(false);cin.tie(0)
using namespace std;

template<typename J>
I void fr(J &x)
{
    short f(1);
    char c=getchar();
    x=0;
    while(c<'0' or c>'9')
    {
        if(c=='-') f=-1;
        c=getchar();
    }
    while (c>='0' and c<='9') 
    {
        x=(x<<3)+(x<<1)+c-'0';
        c=getchar();
    }
    x*=f;
}

template<typename J>
I void fw(J x,bool k)
{
    if(x<0) putchar('-'),x=-x;
    static short stak[35];
    short top(0);
    do
    {
        stak[top++]=x%10;
        x/=10;
    }
    while(x);
    while(top) putchar(stak[--top]+'0');
    if(k) putchar('\n');
    else putchar(' ');
}

引入

先來點輕鬆的,雖然目前我學到的莫隊都很輕鬆愉快就是了。

何為莫隊?

莫隊演算法是由莫濤提出的演算法。在莫濤提出莫隊演算法之前,莫隊演算法已經在 Codeforces 的高手圈裡小範圍流傳,但是莫濤是第一個對莫隊演算法進行詳細歸納總結的人。莫濤提出莫隊演算法時,只分析了普通莫隊演算法,但是經過 OIer 和 ACMer 的集體智慧改造,莫隊有了多種擴充套件版本。[1]

其實簡單來說,莫隊是一種優雅的暴力,是一種令人快樂的分塊暴力(

眾所周知,莫隊是由莫濤大神提出的,一種玄學毒瘤暴力騙分區間操作演算法,它以簡短的框架、簡單易記的板子和優秀的複雜度聞名於世。[2]

而本篇又是講的相對入門的東西,於是就更加輕鬆愉悅。

莫隊

根據前面的引入我們也能知道莫隊是一種區間操作演算法,於是我們就來看一看它是如何區間操作的(

由於個人覺得莫隊的講解應當和例題相結合,於是下面我先圍繞一道例題開講 —— SP3267 DQUERY - D-query

普通莫隊

以 SP3267 開講(不過資料範圍擴大一點)

題目簡述

給出一個長度為 \(n\) 的數列 \(a\)\(q\) 個詢問。對於每個詢問給出一個區間 \([l,r]\),要求這個區間裡有多少不同的數字。

資料範圍 \(1\le n,q,a_i\le 5\times10^5\)

思路分析

因為前面說到莫隊是一種優雅的暴力,所以我們先來看看暴力怎麼做。

那麼我們在不考慮時空複雜度的情況下,我們開一個桶 \(cnt\)

來記錄數字是否出現,然後對於每一個詢問從 \(l\)\(r\) 暴力跑一邊統計一下。

顯然這個時間複雜度我們是跑不起的啊,於是下面來介紹優化一下怎麼做。

我們考慮去列舉兩個位置指標 \(l\)\(r\),每次向詢問的區間去移動,直到 \(l\)\(r\) 的和要查詢的區間完全重合。

當我們移動這兩個指標的時候,對於每一個新位置 \(x\) ,判斷一下 \(cnt_{a_x}\) 是否為 0,若是,說明這個數字之前還沒出現過,記錄的數的個數 \(sum\) 加一;同樣,我們在移動的時候顯然是要捨棄一些位置,對於這些從 \([l,r]\) 被刪除的位置 \(y\),同樣去判斷她的 \(cnt_{a_y}\) 再減一之後是否為 0,若是的話,\(sum\) 也同樣減一。

可以發現我們的複雜度已經小了很多,現在我們已經可以相對快速的統計相鄰詢問區間的答案了!

但是發現沒有上面我實際上加粗了是相鄰的區間才可以,因為我們一位一位的移動也需要時間啊QwQ

但是題目中給出的詢問區間並不一定標號和區間都是相連的,所以可有可能會遇到如下圖一樣極限的情況:出題人很喜歡造這種資料,而且還很好造(

但是我們發現上面那道題並不是強制線上,也就是說我們可以把詢問離線下來進行排序,但是如果只是按照左端點或右端點為關鍵字進行排序的話,我們只能優化一個指標的移動,另一個還是原來的樣子。看上去這個複雜度已經無法優化了(

在這個時候,莫隊演算法的出現,給無數OIer帶來了光明(霧)。[2]

下面就來說說如何用莫隊進一步優化。

優化

實際上莫隊在進行查詢區間轉移的時候採取的思路和上面所述的基本一直,簡單來說就是,假如我們能夠在移動指標的時候 \(O(1)\) 的去維護資訊,(也就是剛才所說的新增和刪除操作要求是 \(O(1)\) 的)那麼這道題就能夠用莫隊來做,否則複雜度會炸掉的QxQ

預處理

莫隊的第一步就是將詢問區間離線下來進行預處理,以避免出現我們在上面出現的指標移動距離過長的問題。

莫隊預處理的核心就是分塊 + 排序。我們先把序列分為 \(\sqrt n\) 個塊,並且將其用 \(1\)\(\sqrt n\) 編號,然後將詢問區間按照這個編號進行排序。

具體來說就是把在進行比較的時候,先按照左端點所在的塊排序,然後把屬於相同塊的按照右端點排序。

不過這裡有一個玄學的奇偶性排序,具體就是說左端點同在編號為奇數的塊裡面的詢問區間,按照右端點升序排列,反之按照降序排列。

這樣做的原理貌似就是在右指標跳完奇數塊後在回跳的時候能夠順便把偶數快跳完,先跳偶數塊也一樣。

理論上主演算法執行時間減半,實際情況有所偏差。(不過能優化得很爽就對了)[2]

於是我們就有了這些東西:

struct questions
{
    int l,r,id;
    bool operator < (const questions &x) const
    {
        if(l/sn!=x.l/sn) Heriko l<x.l;
        if((l/sn)&1) Heriko r<x.r;
        Heriko r>x.r;
    }
}

(不過我個人習慣於寫 \(cmp\) 函式而不是過載運算子)

然後我們就可以直接輕鬆愉悅的實現上面那個題了(

Code

CI NXX=3e4+5,QXX=2e5+5,MXX=1e6+5;

int q,n,sn,a[NXX],ans[QXX],cnt[MXX],now,l(1),r;

struct questions
{
    int l,r,id;
    bool operator < (const questions &x) const
    {
        if(l/sn!=x.l/sn) Heriko l<x.l;
        if((l/sn)&1) Heriko r<x.r;
        Heriko r>x.r;
    }
}

ques[QXX];

I void Add(const int &x) {if(!(cnt[a[x]]++)) ++now;}

I void Del(const int &x) {if(!(--cnt[a[x]])) --now;}

S main()
{
    fr(n);sn=sqrt(n);
    for(R int i(1);i<=n;++i) fr(a[i]);
    fr(q);
    for(R int i(1);i<=q;++i) fr(ques[i].l),fr(ques[i].r),ques[i].id=i;
    sort(ques+1,ques+1+q);
    for(R int i(1);i<=q;++i)
    {
        while(l>ques[i].l) Add(--l);
        while(l<ques[i].l) Del(l++);
        while(r<ques[i].r) Add(++r);
        while(r>ques[i].r) Del(r--);
        ans[ques[i].id]=now;
    }
    for(R int i(1);i<=q;++i) fw(ans[i],1);
    Heriko Deltana;
}

在切了這道題之後,小結一下普通莫隊是如何運用和實現的。

預處理

利用分塊和排序把指標跳詢問區間的時間損耗有效降低,不過分塊的大小實際上可以根據題目來定。

定策略

也就是說我們要根據題目所求的部分來定下來我們在新增和刪除時需要維護的資訊。

下面給幾道題來簡單介紹如何去定策略。

莉題一

洛谷 | P1494 小Z的襪子 [國家集訓隊]

思路簡述

我們去開一個桶 \(cnt\) 記錄當前顏色 \(i\) 出現了幾次,然後用 \(sum\) 去記錄有多少可行的配對方案。

那麼顯然我們新增元素就相當於是讓 \(sum+(C_{cnt_i+1}^{2}-C_{cnt_i}^{2})\),刪除元素就是 \(sum+(C_{cnt_i}^{2}-C_{cnt_i-1}^{2})\),然後本次詢問答案即為 \(\dfrac{sum}{C_{r-l+1}^{2}}\)

下面來把這個東西化簡一下:

\[\begin{aligned} \because\ & C_{x}^2=\dfrac{x(x-1)}{2} \\ \therefore\ & C_{x+1}^2 - C_{x}^2=\dfrac{(x+1)x-x(x-1)}{2}=\dfrac{(x+1-x+1)x}{2}=\dfrac{2x}{2}=x\\ \therefore\ & C_{cnt_i+1}^{2}-C_{cnt_i}^{2}=cnt_i \end{aligned} \]

於是我們就有如下程式碼:

Code

CI MXX=50005;

int n,m,mx,co[MXX],cnt[MXX];

LL ann[MXX],ass[MXX],sum;//請不要誤會,ann 和 ass 只是單純的 ans 的變體,並沒有其他意思(

struct Query
{
    int l,r,id;
}

q[MXX];

I bool cmp(const Query &x,const Query &y)
{
    if(x.l/mx != y.l/mx) Heriko x.l<y.l;
    if((x.l/mx)&1) Heriko x.r<y.r;
    Heriko x.r>y.r;
}

I void Add(const int &x) {sum+=(cnt[x]++);}

I void Del(const int &x) {sum-=(--cnt[x]);}

I LL Gcd(LL x,LL y) {Heriko !y?x:Gcd(y,x%y);}

I void Pre()
{
    fr(n),fr(m);
    mx=sqrt(n);
    for(R int i(1);i<=n;++i) fr(co[i]);
    for(R int i(1);i<=m;++i) fr(q[i].l),fr(q[i].r),q[i].id=i;
    sort(q+1,q+1+m,cmp);
}

S main()
{
    Pre();
    for(R int i(1),l(1),r(0);i<=m;++i)
    {
        if(q[i].l==q[i].r) {ann[q[i].id]=0;ass[q[i].id]=1;continue;}
        while(l>q[i].l) Add(co[--l]);
        while(r<q[i].r) Add(co[++r]);
        while(l<q[i].l) Del(co[l++]);
        while(r>q[i].r) Del(co[r--]);
        ann[q[i].id]=sum;
        ass[q[i].id]=(LL)(r-l+1)*(r-l)/2;
    }
    LL G;
    for(R int i(1);i<=m;++i)
    {
        if(ann[i])
        {
            G=Gcd(ann[i],ass[i]);
            ann[i]/=G,ass[i]/=G;
        }
        else ass[i]=1;
        fw(ann[i],0),putchar('/'),fw(ass[i],1);
    }
    Heriko Deltana;
}

莉題二

洛谷 | P2709 小B的詢問

思路簡述

因為要統計一個數字在當前詢問區間中出現的次數,所以我們還是要開一個桶 co

不知道我怎麼想的這幾個題命名一樣的陣列和變數作用完全不一樣(

因為我們在新增或者刪除元素的時候只會對一個數的平方有影響,於是我們考慮 \((x+1)^2,(x-1)^2\)\(x^2\) 的關係。

我們可以用初中知識得到:

\[\begin{aligned} \because\ &(x+1)^2=x^2+2x+1,(x-1)^2=x^2-2x+1.\\ \therefore\ &(x+1)^2-x^2=2x+1,(x-1)^2-x^2=-2x+1. \end{aligned} \]

於是我們就能知道,在每次新增一個 \(x\) 的時候答案 \(cnt\) 增加 \(2x+1\),刪除的時候減少 \(2x-1\)

於是我們就有如下程式碼:

Code

CI MXX=50005;

int n,m,a[MXX],mx,k;

LL ans[MXX],co[MXX],cnt;//請不要搞混了,這裡的 co 和 cnt 都與莉題一的定義不一樣

struct Query
{
    int l,r,id;
}

q[MXX];

I bool cmp(const Query &x,const Query &y)
{
    if(x.l/mx!=y.l/mx) Heriko x.l<y.l;
    if((x.l/mx)&1) Heriko x.r<y.r;
    Heriko x.r>y.r;
}

I void Add(const int &x) {cnt+=(co[x])*2+1;++co[x];}

I void Del(const int &x) {cnt-=(co[x])*2-1;--co[x];}

I void Pre()
{
    fr(n),fr(m),fr(k);mx=sqrt(n);
    for(R int i(1);i<=n;++i) fr(a[i]);
    for(R int i(1);i<=m;++i) fr(q[i].l),fr(q[i].r),q[i].id=i;
    sort(q+1,q+1+m,cmp);
}

S main()
{
    Pre();
    int l(1),r(0);
    for(R int i(1);i<=m;++i)
    {
        while(l<q[i].l) Del(a[l++]);
        while(l>q[i].l) Add(a[--l]);
        while(r<q[i].r) Add(a[++r]);
        while(r>q[i].r) Del(a[r--]);
        ans[q[i].id]=cnt;
    }
    for(R int i(1);i<=m;++i) fw(ans[i],1);
    Heriko Deltana;
}

帶修莫隊

還是老傳統,我隨著一個莉題來講一講(

洛谷 | P1903 數顏色 / 維護佇列 [國家集訓隊]

題意簡述

給你一個長度為 \(n\) 的序列 \(a\),有 \(m\) 個操作。

共用有兩種操作:

  1. \(\texttt{Q}\ l\ r\) 操作,表示詢問區間 \([l,r]\) 中有多少不相同的數。

  2. \(\texttt{R}\ pos\ col\) 操作,把位置為 \(pos\) 的數字改為 \(col\)

思路簡述

前面提到了,莫隊能用的前提是你得能離線,強制線上莫隊一點法沒有(

但是這道題不強制線上,雖然是有修改,但莫隊也是可以稍作修改來大展拳腳的。我們在基礎的普通莫隊上加上一維表示時間。

也就是說我們又加了個指標 \(t\) ,在來回的跳動修改,於是我們的移動方向從 \([l,r+1],[l,r-1],[l-1,r],[l+1,r]\) 這四個方向擴充套件到了 \([l,t+1,r],[l,t-1,r],[l+1,t,r],[l-1,t,r],[l,t,r-1],[l,t,r+1]\) 這六個方向(

於是我們的 \(cmp\) 就變成了這個樣子:

I bool cmp(const Query &a,const Query &b)
{
    Heriko (belong[a.l]^belong[b.l]) ? belong[a.l]<belong[b.l]:((belong[a.r]^belong[b.r]) ? belong[a.r]<belong[b.r]:a.ti<b.ti);
}

形象一點就是(排序之前):

(我也不知道畫的能不能看懂,但是大約就是把詢問區間離線成了這樣的東西QxQ)

其實修改操作基本是一致的,不過有個小優化,即把原值暫存一下備用,由於本人沒有很搞懂,於是就引用一下 WAMonster 的描述罷(

但其實我們也可以不存,只要在修改後把修改操作的值和原值 swap 一下,那麼改回來時也只要 swap 一下, swap 兩次相當於沒搞,就改回來了 qwq [2]

前面還說到了一點,分塊的大小其實是不一定為 \(\sqrt n\) 的,比如這道題就是,這道題的塊的大小應取 \(n^{\frac{2}{3}}\) 以得到總體 \(O(n^\frac{5}{3})\) ,本人不會證明,但是能夠得到這樣是要比總體複雜度為 \(O(n^2)\)\(\sqrt n\) 要優的。

Code

CI QXX=1e6+5,MXX=5e5+5;

int a[MXX],cnt[QXX],ans[MXX],belong[MXX];

struct Query
{
    int l,r,id,ti;
}

q[MXX];

struct Modify
{
    int pos,co,lst;
}

c[MXX];

int cnq,cnc,n,m,sz,blockn,l(1),r,tim,now;

I bool cmp(const Query &a,const Query &b)
{
    Heriko (belong[a.l]^belong[b.l]) ? belong[a.l]<belong[b.l]:((belong[a.r]^belong[b.r]) ? belong[a.r]<belong[b.r]:a.ti<b.ti);
}

I void pre()
{
    fr(n),fr(m);
    sz=pow(n,2.0/3.0);
    blockn=ceil((double)n/sz);
    for(R int i(1);i<=blockn;++i)
        for(R int j=(i-1)*sz+1;j<=i*sz;++j)
            belong[j]=i;
    for(R int i(1);i<=n;++i) fr(a[i]);
    char opts[5];
    for(R int i(1);i<=m;++i)
    {
        scanf("%s",opts);
        if(opts[0]=='Q') fr(q[++cnq].l),fr(q[cnq].r),q[cnq].id=cnq,q[cnq].ti=cnc;
        else if(opts[0]=='R') fr(c[++cnc].pos),fr(c[cnc].co);
    }
    sort(q+1,q+1+cnq,cmp);
}

S main()
{
    pre();
    for(R int i(1);i<=cnq;++i)
    {
        while(l<q[i].l) now-=!--cnt[a[l++]];    
        while(l>q[i].l) now+=!cnt[a[--l]]++;
        while(r<q[i].r) now+=!cnt[a[++r]]++;
        while(r>q[i].r) now-=!--cnt[a[r--]];
        while(tim<q[i].ti)
        {
            ++tim;
            if(q[i].l<=c[tim].pos and c[tim].pos<=q[i].r) now-=!(--cnt[a[c[tim].pos]])-!(cnt[c[tim].co]++);
            swap(a[c[tim].pos],c[tim].co);
        }
        while(tim>q[i].ti)
        {
            if(q[i].l<=c[tim].pos and c[tim].pos<=q[i].r) now-=!(--cnt[a[c[tim].pos]])-!(cnt[c[tim].co]++);
            swap(a[c[tim].pos],c[tim].co);
            --tim;
        }
        ans[q[i].id]=now;
    }
    for(R int i(1);i<=cnq;++i) fw(ans[i],1);
    Heriko Deltana;
}

因為我光把洛谷上紫以下的莫隊做了,於是帶修莫隊就沒什麼莉題了(

尾聲

於是就差不多結束了罷,好久沒有在一篇文章裡寫過這麼多莉題了(

參考資料

Do you like WHAT YOU SEE ?