帶修莫隊分塊
帶修莫隊分塊
在這篇部落格中,我已經介紹了“靜態莫隊”演算法,它可以離線解決一類靜態(不帶修改)的區間問題。
經過後人的不斷完善,出現了“帶修莫隊”,讓莫隊可以支援修改操作。
什麼是帶修莫隊?什麼是帶修莫隊?如果你想了解,什麼是帶修莫隊的話,現在就帶你研究。
Part 1 帶修莫隊原理
引入時間軸
帶修莫隊和普通莫隊的基本原理大同小異,都是排序後優化訪問順序,然後暴力。
因為要支援修改操作,帶修莫隊除了要知道查詢區間的位置之外,還要知道它在什麼時候進行查詢,以便把序列更新到這次查詢時應有的狀態。於是在詢問的結構體中多開一個變數 \(t\) ,用來記錄這次詢問之前第一個修改操作的位置。也就是說,這次查詢基於第 \(t\)
在排序的時候,先按照左端點所在塊由小到大排序,再按照右端點所在塊由小到大排序(可以根據左端點所在塊的編號進行奇偶性優化),再按照時間從小到大排序。
在用上一次的答案 \(Q_{i-1}\) 更新這一次答案 \(Q_i\) 的時候,像普通莫隊一樣,通過移動左右指標進行區間的增減,把上一次查詢的區間位置 \([l_{i-1},r_{i-1}]\) 轉換成 \([l_i,r_i]\) 。又因為上一次的查詢基於第 \(t_{i-1}\) 個版本,還要移動時間軸,把序列更新為第 \(t\) 個版本。具體的更新方法就是直接在序列上依次執行 \(t_{i-1}\)
幾何法理解帶修莫隊
還記得靜態莫隊的幾何法證明時間複雜度嗎?當時我們把每個詢問 \([l,r]\) 看成了平面內一個點 \((l,r)\) 。現在加入了時間軸 \(t\) ,就可以把一個詢問 \([l,r],t\) 看成三維空間內一個點 \((l,r,t)\) 。從原點出發,沿著座標軸走(增減、更新序列),當走到點 \((l,r,t)\) 時,得到詢問 \([l,r],t\) 的答案,一直走下去直到空間內所有點都走過,即得到所有詢問的解。
複雜度證明
帶修莫隊的複雜度比較玄學,我也不太會證,這裡寫個大概,僅供參考。
設:塊長為 \(L\)
- 對於時間指標 \(t\) :左端點所在塊相同時,右端點所在塊單調遞增,如果右端點相同,那麼 \(t\) 遞增,此時 \(t\) 最多移動 \(c\) 次。左端點相同的詢問有 \(\frac n L\) 個,則這些詢問中右端點所在塊相同的有 \(\frac {n^2} {L^2}\) 個,總次數 \(\frac {n^2c} {L^2}\) ;
- 對於左指標 \(l\) :在左端點所在的塊內移動,移動次數不超過 \(2L\) ,總次數 \(qL\) ;
- 對於右指標 \(r\) :當左端點所在塊相同時,右端點所在塊遞增,最壞移動為 \(n\) 。一共有 \(\frac n L\) 個塊,總次數 \(\frac {n^2} L\) ;
故所有指標的總移動複雜度是 \(O\left( \frac {n^2c}{L^2}+qL+\frac {n^2}{L} \right)\) 。
但是一般的題目不會告訴你具體多少次詢問修改,所以統一用運算元 \(m\) 表示,即 \(O\left( \frac {n^2m}{L^2}+mL+\frac {n^2}{L} \right)\) 。
這裡我們想要莫隊跑的更快,操作空間就只有塊長 \(L\) 。
那麼 \(L\) 具體取多少呢......藉助一些神奇的計算軟體,我得到了這個式子:
\[L=\frac {n^2}{\sqrt[3] 3\sqrt[3]{\left(9m^3n^2+\sqrt 3\sqrt{27m^6n^4-m^3n^6}\right)}}+\frac{\sqrt[3] {\left(9m^2n^2+\sqrt 3\sqrt {27m^6n^4-m^3n^6}\right)}}{\sqrt[3]{n^2}m} \]emmm...... 還是不要糾結塊長多少的好。視作 \(n,m\) 為同數量級,有 \(L=\sqrt[3]{n^2}\) 時取得漸進時間複雜度約為 \(O(\sqrt[3]{n^5})\) 。
所以在設定塊長的時候可以 len=(int)pow(n,0.6666666666);
。
Part 2 帶修莫隊例題
帶修莫隊我目前沒找到大量練習題目,只有這一道板子。這裡挖個坑:以後如果遇到帶修莫隊的題目要在這裡整理總結。
[國家集訓隊]數顏色
題目連結:Link
題目描述:
給你長度為 \(N\) 的序列 \(A\) ,有 \(m\) 次操作。
- 形如 Q L R 的指令,查詢 \([L,R]\) 之間有多少個不同的元素。
- 形如 R P C 的指令,表示把 \(A_P\) 修改為 \(C\) 。
Solution:
這題和 HH 的項鍊那題非常像,就是多了一個修改操作,別的沒了。
於是用帶修莫隊時間軸維護修改即可,注意程式碼實現與常數優化(否則你過不去這個板子)。
莫隊由於本身效率算不上高,這裡有一些卡常數小技巧:
-
每一條語句能精簡就精簡,不要使用過多的
if-else
語句巢狀,儘量使用三目運算子替代。語句中==
符號可以用異或x^x
替代(這個做法我不知道有沒有用)。比如這一段(過載小於號運算子用來排序),上面的寫法會比下面的寫法快(儘管看上去是等價的)。
在同樣評測環境下(C++11 標準,開啟 O2 優化,luogu 評測機),第一種寫法最大資料點僅僅執行 861ms,而第二種寫法卻會超時(執行時間大於 2700 ms)。
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) ? bel[a.r]<bel[b.r]:bel[a.r]>bel[b.r]) : a.t < b.t); } /* inline bool operator < (const Node a,const Node b){ if(bel[a.l]!=bel[b.l]) return bel[a.l]<bel[b.l]; else if(bel[a.r]!=bel[a.r]){ if(bel[a.l]&1) return bel[a.r]<bel[b.r]; else return bel[a.r]>bel[b.r]; }else return a.t<b.t; } */ //即使上面那種寫法比較陰間,但是你也得硬著頭皮這麼寫!
-
注意奇偶性優化,這題我一開始沒加奇偶性優化,TLE 到飛起(不過好像有人沒開奇偶優化也過了)。
-
儘量少使用 STL 模板庫中一些實現簡單的函式,因為它會慢(但是它不像某些人所宣傳的那樣慢的駭人聽聞)。比如這一段程式碼,我用到了交換也就是
std::swap()
函式。#define swap(x,y) x^=y,y^=x,x^=y; swap(x,y); /* #include<algorithm> std::swap(x,y); */
上面的
swap
是我巨集定義的,而下面是演算法庫裡自帶的。在同樣評測環境下(C++11 標準,開啟 O2 優化,luogu 評測機),上面的寫法最大資料點執行 861 ms,下面的寫法最大資料點執行 931 ms(雖然差不了多少,但是能快一點是一點啊)。
Code:
感覺上面敘述了一大頓也沒講明白,那就看程式碼吧...
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cmath>
//using namespace std;
const int maxn=140005;
#define swap(x, y) x ^= y, y ^= x, x^= y
// #define int long long
template <typename _T>
inline void 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<<3)+(x<<1)+ch-'0';
ch=getchar();
}
x*=fh;
}
int n,m,len;
int A[maxn],bel[maxn];//A存序列,表示第i個元素屬於bel[i]塊
struct Node{
int l,r,t,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) ? bel[a.r]<bel[b.r]:bel[a.r]>bel[b.r]) : a.t < b.t);
}//奇偶性優化
struct QAQ{
int pos,val;
};
int Mnum;//修改總數
struct QAQ modify[maxn];//存修改操作
int ans,cnt[1000005];//答案和用來更新它的桶
inline void add(int i){
ans+=!cnt[i]++;//陰間卡常操作
}
inline void del(int i){
ans-=!--cnt[i];
}
inline void change(const int now,const int i){
if(modify[now].pos >= query[i].l && modify[now].pos <=query[i].r)
del(A[modify[now].pos]),add(modify[now].val);//如果修改在這段詢問區間內,那麼要更新答案
swap(modify[now].val,A[modify[now].pos]);
//交換值,這裡不能直接賦值,因為在之後的求解中有可能要把序列改回之前的某一個版本。
}
int ans1[maxn];
signed main(){
#ifdef WIN32
freopen("a.in", "r", stdin);
freopen("a.out","w",stdout);
#endif
read(n),read(m);
len=(int)pow(n,0.6666666666);//上面已證帶修莫隊最佳塊長
for(int i=1;i<=n;++i){
bel[i]=i/len+1;
read(A[i]);
}
for(int i=1;i<=m;++i){
char opt[3];
scanf("%s",opt);
if(opt[0]=='Q'){
++Qnum;
read(query[Qnum].l);
read(query[Qnum].r);
query[Qnum].t=Mnum;
query[Qnum].org=Qnum;
}else{
++Mnum;
read(modify[Mnum].pos);
read(modify[Mnum].val);
}
}//讀入所有操作
std::sort(query+1,query+Qnum+1);
for(int i=1;i<=Qnum;++i){
for(int j=query[i-1].l;j<query[i].l;++j)
del(A[j]);
for(int j=query[i-1].l-1;j>=query[i].l;--j)
add(A[j]);
for(int j=query[i-1].r+1;j<=query[i].r;++j)
add(A[j]);
for(int j=query[i-1].r;j>query[i].r;--j)
del(A[j]);
for(int j=query[i-1].t+1;j<=query[i].t;++j)
change(j,i);//移動時間軸,上面的都和普通莫隊無二
for(int j=query[i-1].t;j>query[i].t;--j)
change(j,i);
ans1[query[i].org]=ans;
}
for(int i=1;i<=Qnum;++i)
printf("%d\n",ans1[i]);
return 0;
}
繁華盡處,
尋一靜謐山谷,
築一木製小屋,
砌一青石小路,
與你晨鐘暮鼓,
安之若素。