莫隊套值域分塊
莫隊套值域分塊
在經典的“靜態區間第 \(k\) 小”問題中,我們已經知道有可持久化線段樹(主席樹)做法和線段樹套平衡樹等做法。假設現在要求支援單點修改,變成“動態區間第 \(k\) 小”問題,線段樹套平衡樹仍然可做,主席樹則需要在外側套一個樹狀陣列。這樣總複雜度為 \(O(Nlog^2N)\) 。
但是眾所周知,樹套樹死難寫,一不小心就會造成“想題 5 分鐘,寫掛 2 小時”的慘案。今天介紹一種更簡單的實現“動態區間 \(k\) 小”的做法——莫隊套值域分塊,也就是“塊套塊”。
Part 1 值域分塊
值域分塊,顧名思義是對序列 \(A\) 的值域 \((A_i\in [1,M])\) 進行分塊。主要用途是維護一堆數,支援 \(O(1)\)
問題引入
在介紹值域分塊之前,先思考這樣一個問題:
- 如果讓你用一個桶來維護一列正整數數中的第 \(k\) 小,你會怎麼做?
一個顯然的做法是把所有的數離散化後扔進桶裡(這相當於桶排序),每次從 1(值域下界)開始列舉到值域上界 \(M\),找到出現的第 \(k\) 個數是幾。假設有 \(N\) 次詢問,那麼這個演算法的時間複雜度是 \(O(NM)\) 的,並不優秀。如果我們使用分塊來維護這個桶,就可以把時間複雜度降低到 \(O(NT)\) ,其中 \(T\) 為塊長。而這,就是所謂“值域分塊”。
操作介紹
假設一個無序數列 \(A\) ,其中 \(A_i\in [1,M]\) (需離散化)。
-
預處理
對區間 \([1,M]\) 分塊處理,設塊長 \(s=T\) 。設桶 \(cnt[x]\) 表示數 \(x\) 出現的次數,另外塊內再維護一個數組 \(num[x]\) ,表示第 \(x\) 塊內一共有幾個數(也就是 \(\sum_{l_x}^{r_x} cnt[i]\))。 然後暴力地把 \(A\) 中所有的數插入到桶 \(cnt\) 中,利用上面的式子計算出 \(num[i]\) 的值。
-
查詢 \(k\) 小
因為把所有操作都講一遍實在太複雜了,這裡用查詢 \(k\) 小舉例子。
“問題引入”部分中提到過,查詢“第 \(k\) 小”核心的演算法是:從 1(值域下界)開始列舉到值域上界,桶中出現的第 \(k\) 個數就是答案。
現在從第一塊開始一塊一塊地列舉,\(num[1]\) 表示值域區間 \([1,T]\) 中有幾個數。如果 \(k\geq num[1]\) ,說明值在 \([1,T]\) 中的數不足 \(k\) 個,那麼 \(k\) 小也就肯定不在第一塊裡。現在已經出現了 \(num[1]\) 個數了,我們一共要找 \(k\) 個,那麼還要找 \(k-num[1]\) 個數,更新 \(k\) 的值,去下一塊內找。下一塊處理過程以此類推。
重複這個尋找過程,直到在第 \(i\) 塊內,\(k\leq num[i]\) ,這說明 \(k\) 小一定出現在這個塊內。
第 \(i\) 塊代表的值域區間是 \([l_i,r_i]\) ,答案一定在區間 \([l_i,r_i]\) 裡。從 \(l_i\) 列舉到 \(r_i\) 。利用剛才預處理出來的桶 \(cnt[x]\) 檢查當前列舉到的數 \(x\) 是不是在序列中出現過,如果出現過,\(k\) 自減 \(cnt[x]\) 。
重複這個尋找過程,更新 \(k\) 的值,直到 \(k\leq 0\) 此時列舉到的數就是 \(k\) 小,即答案。
-
插入、刪除
這個操作很簡單,直接在桶中和這個數對應的塊中增減出現次數即可。
-
時間複雜度
查詢操作先 \(O(T)\) 枚舉了塊,然後在某個塊內暴力列舉 \(k\) 小的大小(列舉不超過 \(T\) 次),這樣查詢總複雜度 \(O(T)\) 。
插入、刪除操作顯然是 \(O(1)\) 的,直接在對應陣列(\(cnt[\ ],num[\ ]\))中增減即可。
但是這樣寫也有可能不對,值域分塊的正確寫法應該是塊狀連結串列。
如果一個塊內出現的數字過多,時間複雜度很有可能退化到 \(O(N)\) ,這時需要塊狀連結串列的分裂操作。
然而塊狀連結串列太難寫,況且離散化之後同一塊內數字過多這種情況很難出現,所以直接寫分塊就好。
Part 2 莫隊套值域分塊
書接上回說到:值域分塊可以解決序列上的一些問題,其具體功能類似於平衡樹。注意到值域分塊插入刪除都是 \(O(1)\) 的,這和需要大量插入刪除操作以維護區間資訊的莫隊簡直是天作之合。
例1 經典區間第 \(k\) 小問題
- 您需要寫一種資料結構,支援查詢一段序列中給定區間 \([l,r]\) 中所有元素中第 \(k\) 小的數。
讀者可能已經有思路了:用普通的莫隊維護區間,問題間轉移的時候 \(O(1)\) 增減值域分塊中的元素。當莫隊把已知區間移動到詢問區間的時候,利用值域分塊查詢答案即可。
例2 二逼平衡樹
例題 1 很簡單對吧?現在看一個加強版的例題:
- 您需要寫一種資料結構,來維護一個有序數列,其中需要提供以下操作:
- 查詢 \(v\) 在區間 \([l,r]\) 中的排名。
- 查詢區間 \([l,r]\) 中排第 \(k\) 名元素的值。
- 修改某個位置上的數值。
- 在區間 \([l,r]\) 內查詢 \(k\) 的前驅(嚴格小於 \(k\) 且最大的數,若不存在輸出 -2147483647)。
- 在區間 \([l,r]\) 內查詢 \(k\) 的後繼(嚴格大於 \(k\) 且最小的數,若不存在輸出 2147483647)。
查詢區間第 \(k\) 小、查排名、查前驅、查後繼,如果讀者已經理解了值域分塊的原理,那麼這些操作都不難實現。
等等...這個題還要求支援修改操作,怎麼辦?你怕不是忘了莫隊可以帶修啊?
好了恭喜您又雙叒叕秒了一個題,用帶修莫隊套值域分塊的辦法,可以在 \(O(n^\frac 5 3 +n\sqrt n)\) 的時間內求解本題。
\(\text{Talk is cheap,shou you the code.}\)
需要注意的是,這個題的值域很大,需要離散化處理,特別是操作中的某些數也需要一起離散化掉。
ps:變數名在初始化函式中定義,先閱讀 void Init()
函式有助於理解程式碼。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
namespace Fast_IO{
template <typename _T>
inline _T const& read(_T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-'0';
ch=getchar();
}
return x*=fh;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
}
// namespace Fast_IO
using namespace Fast_IO;
//using namespace std;
const int maxn=100005;
const int maxm=10005;
#define ll long long
#define swap(x, y) x ^= y, y ^= x, x^= y
int n,m,tot,len,it;
int A[maxn],B[maxn*2];
int num[maxm],cnt[maxn];//num[i]表示第i塊中數字的數量,cnt[i]表示i出現的數量
int bel[maxn],L[maxm],R[maxm];//bel[i]表示i屬於第幾塊.L[i],R[i]表示第i塊的左右端點
struct Node{
int opt,l,r,t,k,org;
};
int Qnum;
struct Node query[maxn];
inline bool operator < (const Node a,const Node b){
return bel[a.l]^bel[b.l] ? bel[a.l]<bel[b.l] : ( bel[a.r]^bel[b.r] ? (bel[a.l]&1 ? a.r<b.r : a.r>b.r) : a.t<b.t);
}//過載運算子,用以排序
struct QAQ{
int pos,val;
};
int Mnum;
struct QAQ modify[maxn];//記錄詢問
inline void add(const int i){
cnt[i]++;
num[bel[i]]++;
}
inline void del(const int i){
cnt[i]--;
num[bel[i]]--;
}//在桶中新增,刪除元素,很簡單的
inline void change(const int now,const int i){
if(query[i].l<=modify[now].pos && modify[now].pos<=query[i].r)
add(modify[now].val),del(A[modify[now].pos]);
swap(modify[now].val,A[modify[now].pos]);
}//帶修莫隊要更新維護時間軸
int get_rank(int k){//get rank of k in range[l,r]
int rank=1;
for(int i=1;i<=tot;++i){//找到值域上界
if(k<=R[i])//k不如這個塊右端點大,那麼k一定在這個塊中
for(int j=L[i];j<k;++j)//暴力列舉到k,找到比k小的有幾個書
rank+=cnt[j];
else rank+=num[i];//如果k比這個塊中元素都大,那麼答案加上這個塊中的元素數量。
}
return rank;
}
int get_kth(int k){//get kth number in range[l,r]
for(int i=1;i<=tot;++i){//同上,列舉到值域上界
if(k<=num[i])//如果k出現次數不足這個塊中的數的數量了,說明第k名在這個塊內
for(int j=L[i];j<=R[i];++j){//列舉這個塊中所有元素
k-=cnt[j];//每找到一個元素,k減去這個元素數量
if(k<=0) return B[j];//如果k小於0,說明當前元素就是第k名
}
else k-=num[i];//k不在這個塊中,減掉這個塊中數的數量,去下個塊查
}
return -1;//gg,返回-1
}
/*
查k的前驅可以先查k的排名rank,然後查排名為rank-1的數的值
查k的後繼同上
*/
int get_pre(const int k){
int rank=get_rank(k);
if(rank==1) return -2147483647;//k已經是第一名了,不存在前驅
int pre=get_kth(rank-1);//查排名前一位元素
return pre;
}
int get_back(const int k){
int rank=get_rank(k);
if(rank==-1) return 2147483647; //k最大,不存在答案
int back=cnt[k]>0?get_kth(rank+1):get_kth(rank);
//這裡細節一波,因為查的數本身可能出現在序列裡,故特判
if(back==-1) return 2147483647; //沒查到後繼,不存在答案
return back;
}
void Init(){
read(n),read(m);//n個元素,m個操作
//對原陣列分塊
len=pow(n,0.6666666666);
for(int i=1;i<=n;++i){
bel[i]=(i-1)/len+1;//預處理,第i個元素屬於bel[i]塊
B[++it]=read(A[i]);//準備離散化
}
//讀入,對詢問排序
for(int i=1,op;i<=m;++i){
read(op);
if(op==3){//replace A[pos] with val
Mnum++;
read(modify[Mnum].pos);
B[++it]=read(modify[Mnum].val);
}else{
Qnum++;
query[Qnum].opt=op,query[Qnum].t=Mnum;
//其實這裡查排名為k的元素不能離散化,但是為了方便,一起加入離散化陣列到時候再特判掉
read(query[Qnum].l),read(query[Qnum].r);
B[++it]=read(query[Qnum].k);
query[Qnum].org=Qnum;
}
}
std::sort(query+1,query+1+Qnum);//排序
//離散化
std::sort(B+1,B+1+it);
it=std::unique(B+1,B+1+it)-B-1;
for(int i=1;i<=n+Mnum+Qnum;++i){
if(i<=n) A[i]=std::lower_bound(B+1,B+it+1,A[i])-B;
else if(i<=n+Mnum) modify[i-n].val=std::lower_bound(B+1,B+it+1,modify[i-n].val)-B;
else if(query[i-n-Mnum].opt!=2) query[i-n-Mnum].k=std::lower_bound(B+1,B+it+1,query[i-n-Mnum].k)-B;//2操作是查排名為k的元素,這個k不能離散化,特判掉
}
//初始化值域分塊
len=sqrt(it);//塊長為sqrt(it)
tot=it/len;//總共tot個塊
for(int i=1;i<=it;++i)
bel[i]=(i-1)/len+1;//重定義bel[i]表示值為i的元素屬於值域分塊的bel[i]塊
for(int i=1;i<=tot;++i){
if(i*len>it) break;
L[i]=(i-1)*len+1;
R[i]=i*len;//預處理每一塊的左右端點
}
if(R[tot]<it)
tot++,L[tot]=R[tot-1]+1,R[tot]=it;
}
int ans1[maxn];
signed main(){
Init();
int l=1,r=0,now=0;
for(int i=1;i<=Qnum;++i){
while(l<query[i].l) del(A[l++]);
while(l>query[i].l) add(A[--l]);
while(r<query[i].r) add(A[++r]);
while(r>query[i].r) del(A[r--]);
while(now<query[i].t) change(++now,i);
while(now>query[i].t) change(now--,i);//正常帶修莫隊操作
if(query[i].opt==1)
ans1[query[i].org]=get_rank(query[i].k);
else if(query[i].opt==2)
ans1[query[i].org]=get_kth(query[i].k);
else if(query[i].opt==4)
ans1[query[i].org]=get_pre(query[i].k);
else if(query[i].opt==5)
ans1[query[i].org]=get_back(query[i].k);//按 要 求 回 答 問 題
}
for(int i=1;i<=Qnum;++i)
printf("%d\n",ans1[i]);//輸出答案
return 0;
}
例3 [AHOI]2013 作業
其實本來例 3 不是這道題,但是由於原來準備當例 3 的那個題理解起來簡直讓人逝世(各種陣列連環巢狀),所以臨時決定換這道題。正好也是省選題目,質量應該也比某谷月賽題目好罷。
題目連結:Link
題目描述:
給一個長度為 \(n(n\leq 10^5)\) 的正整數數列。
\(m(m\leq 10^5)\) 次操作,每次給定 \(l,r,a,b\) ,要求輸出大小在 \([a,b]\) 之間的數的個數,以及符合條件的數有幾種。
Solution:
經典莫隊+值域分塊的題目。查詢 \([a,b]\) 中的數時,先暴力 \(a,b\) 所在段元素,然後整段處理。如果 \(a,b\) 在同一段直接暴力。另外對於第二問再開一個桶分別統計答案即可,具體看程式碼。
Code:
由於上面的註釋比較詳細了,這裡程式碼不再加註。望理解。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
namespace IO{
template <typename _T>
inline _T const& read(_T &x){
x=0;int fh=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')
fh=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-'0';
ch=getchar();
}
return x*=fh;
}
inline void write(long long a){
if(a>=10) write(a/10);
putchar(a%10+'0');
}
} // namespace IO
using namespace IO;
//using namespace std;
const int maxn=100005;
const int maxm=2005;
int n,m,len,tot;
int A[maxn],bel[maxn];//*對A[i]值域分塊
int cnt[maxn];
int L[maxm],R[maxm],num[maxm],val[maxm];//num[i]表示第i塊內的數字個數,val[i]表示第i塊內的數字種類
struct Node{
int l,r,a,b,org;
};
struct Node query[maxn];
bool operator < (const Node a,const Node b){
return bel[a.l]^bel[b.l]?bel[a.l]<bel[b.l]:(bel[a.l]&1?a.r<b.r:a.r>b.r);
}
inline void add(const int i){
++num[bel[A[i]]];
++cnt[A[i]];
if(cnt[A[i]]==1) val[bel[A[i]]]++;
}
inline void del(const int i){
--num[bel[A[i]]];
--cnt[A[i]];
if(cnt[A[i]]==0) val[bel[A[i]]]--;
}
int ans,kind;
inline void get_num(int a,int b){
ans=0;kind=0;
if(bel[a]==bel[b]){
for(int i=a;i<=b;++i)
ans+=cnt[i],kind+=cnt[i]>0;
return;
}
for(int i=a;i<=R[bel[a]];++i)
ans+=cnt[i],kind+=cnt[i]>0;
for(int i=bel[a]+1;i<=bel[b]-1;++i)
ans+=num[i],kind+=val[i];
for(int i=L[bel[b]];i<=b;++i)
ans+=cnt[i],kind+=cnt[i]>0;
}
void Init(){
read(n),read(m);
len=sqrt(n);
tot=n/len;
for(int i=1;i<=tot;++i){
if(i*len>n) break;
L[i]=(i-1)*len+1;
R[i]=i*len;
}
if(R[tot]<n)
tot++,L[tot]=R[tot-1]+1,R[tot]=n;
for(int i=1;i<=n;++i){
bel[i]=(i-1)/len+1;
read(A[i]);
}
for(int i=1;i<=m;++i)
read(query[i].l),read(query[i].r),read(query[i].a),read(query[i].b),query[i].org=i;
std::sort(query+1,query+1+m);
}
std::pair<int,int> ans1[maxn];
signed main(){
Init();
int l=1,r=0;
for(int i=1;i<=m;++i){
while(l<query[i].l) del(l++);
while(l>query[i].l) add(--l);
while(r<query[i].r) add(++r);
while(r>query[i].r) del(r--);
get_num(query[i].a,query[i].b);
ans1[query[i].org].first=ans;
ans1[query[i].org].second=kind;
}
for(int i=1;i<=m;++i)
printf("%d %d\n",ans1[i].first,ans1[i].second);
return 0;
}
繁華盡處,
尋一靜謐山谷,
築一木製小屋,
砌一青石小路,
與你晨鐘暮鼓,
安之若素。