<更新提示>
<第一次更新> 基礎莫隊和帶修莫隊可以看這個課件。
<正文>
回滾莫隊
基礎的莫隊算法相信大家都已經熟悉了,而我們知道,莫隊算法的關鍵就在於如何進行區間的轉移,這就可能涉及到很多的細節。有一類普通莫隊不可解的問題就是在轉移區間過程中,可能出現刪點或加點操作其中之一無法實現的問題。那么我們就來探討如何利用特殊的莫隊算法來解決這類問題,而這種莫隊算法就稱之為回滾莫隊算法。
只加不減的回滾莫隊
我們考慮一個區間問題,若這個問題在區間轉移中,加點操作得以實現,但是刪點操作無法有效的實現時,就可以使用如下的莫隊算法:
\(1.\) 對原序列進行分塊,並對詢問按照如下的方式排序:以左端點所在的塊升序為第一關鍵字,以右端點升序為第二關鍵字
\(2.\) 對於處理所有左端點在塊\(T\)內的詢問,我們先將莫隊區間左端點初始化為\(R[T]+1\),右端點初始化為\(R[T]\),這是一個空區間
\(3.\) 對於左右端點在同一個塊中的詢問,我們直接暴力掃描回答即可。
\(4.\) 對於左右端點不在同一個塊中的所有詢問,由於其右端點升序,我們對右端點只做加點操作,總共最多加點\(n\)次
\(5.\) 對於左右端點不在同一個塊中的所有詢問,其左端點是可能亂序的,我們每一次從\(R[T]+1\)的位置出發,只做加點操作,到達詢問位置即可,每一個詢問最多加\(\sqrt n\)次。回答完詢問后,我們撤銷本次移動左端點的所有改動,使左端點回到\(R[T]+1\)的位置
\(6.\) 按照相同的方式處理下一塊
根據其操作的過程可知,回滾莫隊的時間復雜度仍然為\(O(n\sqrt n)\),並且,在回答詢問的過程中我們只進行了加點操作,沒有涉及刪點操作,這樣就完成了我們需要的操作。
只減不加的回滾莫隊
和上一種典型的回滾莫隊類似,我們還可以實現只有刪點操作沒有加點操作的回滾莫隊,當然,這樣的前提是我們可以正確的先將整個序列加入莫隊中,那么算法流程如下:
\(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教授決定用如下的方法分析這些日記:
- 選擇日記中連續的一些天作為分析的時間段
- 事件種類t的重要度為t*(這段時間內重要度為t的事件數)
- 計算出所有事件種類的重要度,輸出其中的最大值 現在你被要求制作一個幫助教授分析的程序,每次給出分析的區間,你需要輸出重要度的最大值。
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;
}
<后記>