『回滾莫隊及其簡單運用』

<更新提示>

<第一次更新> 基礎莫隊和帶修莫隊能夠看這個 課件c++


<正文>

回滾莫隊

基礎的莫隊算法相信你們都已經熟悉了,而咱們知道,莫隊算法的關鍵就在於如何進行區間的轉移,這就可能涉及到不少的細節。有一類普通莫隊不可解的問題就是在轉移區間過程當中,可能出現刪點或加點操做其中之一沒法實現的問題。那麼咱們就來探討如何利用特殊的莫隊算法來解決這類問題,而這種莫隊算法就稱之爲回滾莫隊算法算法

只加不減的回滾莫隊

咱們考慮一個區間問題,若這個問題在區間轉移中,加點操做得以實現,可是刪點操做沒法有效的實現時,就可使用以下的莫隊算法:數組

\(1.\) 對原序列進行分塊,並對詢問按照以下的方式排序:以左端點所在的塊升序爲第一關鍵字,以右端點升序爲第二關鍵字spa

\(2.\) 對於處理全部左端點在塊\(T\)內的詢問,咱們先將莫隊區間左端點初始化爲\(R[T]+1\),右端點初始化爲\(R[T]\),這是一個空區間code

\(3.\) 對於左右端點在同一個塊中的詢問,咱們直接暴力掃描回答便可。orm

\(4.\) 對於左右端點不在同一個塊中的全部詢問,因爲其右端點升序,咱們對右端點只作加點操做,總共最多加點\(n\)排序

\(5.\) 對於左右端點不在同一個塊中的全部詢問,其左端點是可能亂序的,咱們每一次從\(R[T]+1\)的位置出發,只作加點操做,到達詢問位置便可,每個詢問最多加\(\sqrt n\)次。回答完詢問後,咱們撤銷本次移動左端點的全部改動,使左端點回到\(R[T]+1\)的位置事件

\(6.\) 按照相同的方式處理下一塊ip

根據其操做的過程可知,回滾莫隊的時間複雜度仍然爲\(O(n\sqrt n)\),而且,在回答詢問的過程當中咱們只進行了加點操做,沒有涉及刪點操做,這樣就完成了咱們須要的操做。rem

只減不加的回滾莫隊

和上一種典型的回滾莫隊相似,咱們還能夠實現只有刪點操做沒有加點操做的回滾莫隊,固然,這樣的前提是咱們能夠正確的先將整個序列加入莫隊中,那麼算法流程以下:

\(1.\) 對原序列進行分塊,並對詢問按照以下的方式排序:以左端點所在的塊升序爲第一關鍵字,以右端點降序序爲第二關鍵字

\(2.\) 對於處理全部左端點在塊\(T\)內的詢問,咱們先將莫隊區間左端點初始化爲\(L[T]\),右端點初始化爲\(n\),這是一個大區間

\(3.\) 對於左右端點在同一個塊中的詢問,咱們直接暴力掃描回答便可。

\(4.\) 對於左右端點不在同一個塊中的全部詢問,因爲其右端點降序,從\(n\)的位置開始,咱們對右端點只作刪點操做,總共最多刪點\(n\)

\(5.\) 對於左右端點不在同一個塊中的全部詢問,其左端點是可能亂序的,咱們每一次從\(L[T]\)的位置出發,只作刪點操做,到達詢問位置便可,每個詢問最多加\(\sqrt n\)次。回答完詢問後,咱們撤銷本次移動左端點的全部改動,使左端點回到\(L[T]\)的位置

\(6.\) 按照相同的方式處理下一塊

一樣地,回滾莫隊的時間複雜度仍是\(O(n\sqrt n)\),而且咱們只使用了刪點操做,只有在一開始時將整個序列加入到莫隊中,這樣就完成了咱們須要的操做。

那麼咱們將經過兩道例題來詳細地瞭解整兩種回滾莫隊。

歴史の研究

Description

IOI國曆史研究的第一人——JOI教授,最近得到了一份被認爲是古代IOI國的住民寫下的日記。JOI教授爲了經過這份日記來研究古代IOI國的生活,開始着手調查日記中記載的事件。

日記中記錄了連續N天發生的時間,大約天天發生一件。

事件有種類之分。第i天(1<=i<=N)發生的事件的種類用一個整數X_i表示,X_i越大,事件的規模就越大。

JOI教授決定用以下的方法分析這些日記:

  1. 選擇日記中連續的一些天做爲分析的時間段
  2. 事件種類t的重要度爲t*(這段時間內重要度爲t的事件數)
  3. 計算出全部事件種類的重要度,輸出其中的最大值 如今你被要求製做一個幫助教授分析的程序,每次給出分析的區間,你須要輸出重要度的最大值。

Input Format

第一行兩個空格分隔的整數N和Q,表示日記一共記錄了N天,詢問有Q次。

接下來一行N個空格分隔的整數X_1...X_N,X_i表示第i天發生的事件的種類

接下來Q行,第i行(1<=i<=Q)有兩個空格分隔整數A_i和B_i,表示第i次詢問的區間爲[A_i,B_i]。

Output Format

輸出Q行,第i行(1<=i<=Q)一個整數,表示第i次詢問的最大重要度

Sample Input

5 5
9 8 7 8 9
1 2
3 4
4 4
1 4
2 4

Sample Output

9
8
8
16
16

解析

大體題意:給定一個長度爲\(n\)的序列,離線詢問\(m\)個問題,每次回答區間內元素權值乘以元素出現次數的最大值。

咱們考慮用莫隊來解決這個問題,顯然,爲了統計每一個元素的出現次數,咱們要用到桶。而加點操做就很好實現了,在桶中給元素的出現次數加一,並查看是否可以更新答案便可。可是刪點操做就難以實現,當咱們刪去一個點時,咱們難以得知新的最大值是多少,因此咱們用只加不減的回滾莫隊。

那麼回滾莫隊中提到的撤銷操做具體就是指在桶中減去出現次數,而無論答案是否改變。在下一次加點的過程當中,答案就得以統計了。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5+20 , SIZE = 1020;
int n,m,size,T,raw[N],val[N],t,cnt[N],cnt_[N];
int belo[N],L[SIZE],R[SIZE];
long long ans[N],Max,a[N];
struct query{int l,r,id;}q[N];
inline void input(void)
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
        scanf("%lld",&a[i]) , raw[++t] = a[i];
    for (int i=1;i<=m;i++)
        scanf("%d%d",&q[i].l,&q[i].r) , q[i].id = i;
}
inline void discrete(void)
{
    sort( raw+1 , raw+t+1 );
    t = unique( raw+1 , raw+t+1 ) - (raw+1);
    for (int i=1;i<=n;i++)
        val[i] = lower_bound( raw+1 , raw+t+1 , a[i] ) - raw;
}
inline void setblocks(void)
{
    size = sqrt(n) , T = n/size;
    for (int i=1;i<=T;i++)
    {
        if ( i * size > n ) break;
        L[i] = (i-1) * size + 1;
        R[i] = i * size;
    }
    if ( R[T] < n ) T++ , L[T] = R[T-1] + 1 , R[T] = n;
    for (int i=1;i<=T;i++)
        for (int j=L[i];j<=R[i];j++)
            belo[j] = i;
}
inline bool compare(query p1,query p2)
{
    if ( belo[p1.l] ^ belo[p2.l] )
        return belo[p1.l] < belo[p2.l];
    else return p1.r < p2.r;
}
// 加點
inline void insert(int p,long long &Maxval)
{
    cnt[val[p]]++;
    Maxval = max( Maxval , 1LL * cnt[val[p]] * a[p] );
}
// 撤銷
inline void resume(int p)
{
    cnt[val[p]]--;
}
inline void CaptainMo(void)
{
    sort( q+1 , q+m+1 , compare );
    int l = 1 , r = 0 , lastblock = 0;
    for (int i=1;i<=m;i++)
    {
        // 處理同一塊中的詢問
        if ( belo[q[i].l] == belo[q[i].r] )
        {
            for (int j=q[i].l;j<=q[i].r;j++) cnt_[val[j]]++;
            long long temp = 0;
            for (int j=q[i].l;j<=q[i].r;j++) 
                temp = max( temp , 1LL * cnt_[val[j]] * a[j] );
            for (int j=q[i].l;j<=q[i].r;j++) cnt_[val[j]]--;
            ans[ q[i].id ] = temp; 
            continue; 
        }
        // 若是移動到了一個新的塊,就先把左右端點初始化
        if ( lastblock ^ belo[q[i].l] )
        {
            while ( r > R[belo[q[i].l]] ) resume(r--);
            while ( l < R[belo[q[i].l]]+1 ) resume(l++);
            Max = 0 , lastblock = belo[q[i].l];
        }
        // 單調地移動右端點
        while ( r < q[i].r ) insert(++r,Max);
        // 移動左端點回答詢問
        long long temp = Max; int  l_ = l;
        while ( l_ > q[i].l ) insert(--l_,temp);
        // 回滾
        while ( l_ < l ) resume(l_++);
        ans[ q[i].id ] = temp;
    }
}
int main(void)
{
    input();
    discrete();
    setblocks();
    CaptainMo();
    for (int i=1;i<=m;i++)
        printf("%lld\n",ans[i]);
    return 0;
}

mex

Description

有一個長度爲n的數組{a1,a2,…,an}。m次詢問,每次詢問一個區間內最小沒有出現過的天然數。

Input Format

第一行n,m。

第二行爲n個數。

從第三行開始,每行一個詢問l,r。

Output Format

一行一個數,表示每一個詢問的答案。

Sample Input

5 5
2 1 0 2 1
3 3
2 3
2 4
1 2
3 5

Sample Output

1
2
3
0
3

解析

這道題咱們莫隊是思路就是用桶維護出現過的數字,那麼\(mex\)值就是第一個不在桶中出現的數字。

咱們發現刪點操做很容易實現,能夠順帶的更新答案,可是加點操做難以實現,咱們原來的最小值在加點過程當中出現了,咱們就無從得知新的答案。顯然,一開始將整個序列加入到桶裏並統計答案是可行的,那麼咱們就是用只刪不加的回滾莫隊。

撤銷操做仍是在桶中更新,但無論答案的變化就能夠了。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 200020 , SIZE = 1020;
int n,m,a[N],cnt[N],Min,size,T,ans[N];
int cnt_[N],ans_,belo[N],L[N],R[N];
struct query{int l,r,id;}q[N];
inline void input(void)
{
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for (int i=1;i<=m;i++)
        scanf("%d%d",&q[i].l,&q[i].r) , q[i].id = i;
}
inline void init(void)
{
    for (int i=1;i<=n;i++)
        if ( a[i] <= n+1 )
            cnt[ a[i] ]++;
    while ( cnt[ ans_ ] ) ans_++;
    // 先把整個序列加入桶,同時獲得總體的答案
}
inline void setblocks(void)
{
    size = sqrt(n) , T = n/size;
    for (int i=1;i<=T;i++)
    {
        if ( i * size > n ) break;
        L[i] = (i-1)*size + 1;
        R[i] = i * size;
    }
    if ( R[T] < n ) T++ , L[T] = R[T-1] + 1 , R[T] = n;
    for (int i=1;i<=T;i++)
        for (int j=L[i];j<=R[i];j++)
            belo[j] = i;
}  
inline bool compare(query p1,query p2)
{
    if ( belo[p1.l] ^ belo[p2.l] )
        return belo[p1.l] < belo[p2.l];
    else return p1.r > p2.r;
}
// 刪點
inline void remove(int p,int &Minval) 
{
    if ( a[p] > n+1 ) return;
    cnt[a[p]]--;
    if ( cnt[a[p]] == 0 ) Minval = min( Minval , a[p] );
}
// 撤銷
inline void resume(int p)
{
    if ( a[p] > n+1 ) return;
    cnt[a[p]]++;
}
inline void CaptainMo(void)
{
    sort( q+1 , q+m+1 , compare );
    int l = 1 , r = n , lastblock = 0;
    for (int i=1;i<=m;i++)
    {
        // 處理同一塊中的詢問
        if ( belo[q[i].l] == belo[q[i].r] )
        {
            for (int j=q[i].l;j<=q[i].r;j++) 
                if ( a[j] <= n+1 ) cnt_[a[j]]++;
            int temp = 0;
            while ( cnt_[temp] ) temp++;
            ans[ q[i].id ] = temp;
            for (int j=q[i].l;j<=q[i].r;j++) 
                if ( a[j] <= n+1 ) cnt_[a[j]]--;
            continue;
        }
        // 若是移動到了一個新的塊,就先把左右端點初始化
        if ( belo[q[i].l] ^ lastblock )
        {
            while ( r < n ) resume(++r);
            while ( l < L[belo[q[i].l]] ) remove(l++,ans_);
            Min = ans_ , lastblock = belo[q[i].l];
        }
        // 單調地移動右端點
        while ( r > q[i].r ) remove(r--,Min);
        // 移動左端點回答詢問
        int temp = Min , l_ = l;
        while ( l_ < q[i].l ) remove(l_++,temp);
        // 回滾
        while ( l_ > l ) resume(--l_);
        ans[ q[i].id ] = temp;
    }
}
int main(void)
{
    input();
    init();
    setblocks();
    CaptainMo();
    for (int i=1;i<=m;i++)
        printf("%d\n",ans[i]);
    return 0;
}

<後記>

相關文章
相關標籤/搜索