1. 根號分治與分塊
1.1. 根號分治
根號分治,就是在預處理與詢問的復雜度之間尋找平衡的一個算法。通常以根號作為問題規模的分界線,規模小於根號的詢問可以 \(n\sqrt n\) 預處理求出,而回答一次規模為 \(B\geq n\) 的詢問的時間只需要 \(\dfrac n B\leq \sqrt n\),那么整個題目就可以做到 \(n\sqrt n\)。
根號平衡的思想非常重要,它是幾乎所有根號算法的核心思想,例如第三部分的莫隊。學會根號平衡對 OI 水平有很大幫助。
1.2. 分塊
與根號分治有異曲同工之妙,可以用來維護一些 \(\log\) 數據結構無法維護的東西,比如用分塊凸包替換不支持修改的李超樹。
本質是一種暴力:序列分塊就是把序列分成 \(\sqrt n\) 塊,修改時遇到整塊可以區間打標記,遇到散點直接暴力重構,由於最多 \(\sqrt n\) 重構兩個塊,打根號個區間的標記,所以單次修改時間復雜度 \(k\sqrt n\),一般 \(k\) 是常數。
別看分塊時間復雜度沒有 \(\rm polylog\) 的數據結構好,但是它仍是重要算法。它可以靈活地維護一些數據結構無法維護的信息,並且可以用來平衡復雜度。例如在莫隊二次離線算法中,我們有 \(\mathcal{O}(n)\) 次修改,\(\mathcal{O}(n\sqrt n)\) 次查詢,此時使用 \(\mathcal{O}(\sqrt n)\) 修改,\(\mathcal{O}(1)\) 查詢的維護前綴和的分塊即可做到 \(\mathcal{O}(n\sqrt n)\) 的優秀復雜度,比 \(\mathcal{O}(n\sqrt n\times \mathrm{polylog})\) 不知道快到哪去了。
1.3. 例題
I. CF797E Array Queries
題意簡述:給出 \(\{a_i\}\),多次詢問給出 \(p,k\),求至少執行多少次 \(p\gets p+a_p+k\) 才能使 \(p>n\)。
注意到如果 \(k>\sqrt n\) 那么答案必定不大於 \(\sqrt n\),那么對於所有位置預處理出所有 \(k\leq \sqrt n\) 的答案,若 \(k>\sqrt n\) 直接暴力查詢即可。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
const int N=1e5+5;
const int B=333;
int n,m,b,a[N],s[N][B];
int main(){
cin>>n,b=sqrt(n);
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=b;i++)for(int j=n;j;j--)
s[j][i]=j+a[j]+i>n?1:(s[j+a[j]+i][i]+1);
cin>>m;
for(int i=1;i<=m;i++){
int p,k; cin>>p>>k;
if(k<=b)cout<<s[p][k]<<endl;
else{
int ans=0;
while(p<=n)ans++,p+=a[p]+k;
cout<<ans<<endl;
}
}
return 0;
}
II. *CF1039D You Are Given a Tree
題意簡述:給出一棵樹,對每個 \(k\in[1,n]\),求出最多能找出多少條沒有公共點的至少經過 \(k\) 個點的鏈。
注意到若 \(k>\sqrt n\) 則答案一定不大於 \(\sqrt n\)(怎么和上一題那么像,笑)。那么對於 \(1\leq k\leq \sqrt n\),直接暴力樹形 DP。然后再枚舉 \(1\leq ans\leq \sqrt n\),不過這次枚舉的是鏈的條數,即答案。顯然答案單調不升,於是二分出答案為 \(ans\) 的 \(k\) 的區間即可(實際上不需要右端點,只需要左端點)。
樹形 DP 求鏈上經過的點 \(k\) 的答案:該部分比較類似 賽道修建,不過也有一些區別:因為一個點只能被一條鏈經過(而不是賽道修建中的一條邊),於是分兩種情況討論:記 \(mx_1,mx_2\) 為 \(i\) 的兒子所傳入的最長的兩條鏈(所經過點的個數),若 \(mx_1+mx_2+1\geq k\),那么顯然是將 \(i\) 與它的兩個兒子配成一條鏈,答案加 \(1\);否則將 \(mx+1\) 傳上去到 \(fa_i\) 即可。這樣一次 DP 就是 \(\mathcal{O}(n)\) 的。
因此,總時間復雜度為 \(\mathcal{O}(n\sqrt n\log n)\)。
卡常技巧:將每個節點的父親預處理出來,然后按照 dfs 序排序,可以直接循環樹形 DP,不需要 dfs。
#include <bits/stdc++.h>
using namespace std;
#define mem(x,v) memset(x,v,sizeof(x))
const int N=1e5+5;
int ed,ed2,hd[N],nxt[N<<1],to[N<<1];
pii nw[N];
void add(int u,int v){
nxt[++ed]=hd[u],hd[u]=ed,to[ed]=v;
}
int n,p,cnt,ans[N];
void dfs0(int id,int f){
for(int i=hd[id];i;i=nxt[i]){
if(to[i]==f)continue;
nw[++ed2]={id,to[i]},dfs0(to[i],id);
}
}
int dfs(int id){
int mx=0,mx2=0;
for(int i=hd[id];i;i=nxt[i]){
int v=dfs(to[i]);
if(v>=p){cnt++; return 0;}
if(v>=mx)mx2=mx,mx=v;
else if(v>=mx2)mx2=v;
} if(mx+mx2+1>=p){cnt++; return 0;}
return mx+1;
} int run(int x){
cnt=0,p=x,dfs(1);
return cnt;
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int a=read(),b=read();
add(a,b),add(b,a);
} int m=sqrt(n*log2(n));
dfs0(1,0),mem(hd,0),mem(nxt,0),ed=0;
for(int i=1;i<n;i++)add(nw[i].fi,nw[i].se);
for(int i=1;i<=m;i++)ans[i]=run(i);
for(int i=1,pre=n+1;i<=n/m+1;i++){
int l=1,r=pre;
while(l<r){
int m=(l+r>>1)+1;
if(run(m)>=i)l=m;
else r=m-1;
} for(int j=l+1;j<pre;j++)ans[j]=i-1; pre=l+1;
} for(int i=1;i<=n;i++)cout<<ans[i]<<endl;
return flush(),0;
}
III. *2019 五校聯考 - 鎮海 T3 小 ω 的樹
見計算幾何初步凸包部分例題。
IV. CF1580C Train Maintenance
一個非常顯然的根號重構題目。若 \(x+y\leq B\) 我們可以用桶記錄其對 \(i\bmod (x+y)=d\) 的每個天數 \(i\) 的貢獻。若 \(x+y>B\) 直接差分即可。注意取消差分貢獻時下標對 \(i\) 取 \(\max\),因為作用在 \(i\) 以前的位置 \(j\) 的差分需要在 \(i\) 處更新而不是 \(j\):你對差分數組位置 \(j\ (j+1<i)\) 的更新是不會在位置 \(i\) 中體現的,\(j\) 已經過時了。
時間復雜度 \(\mathcal{O}(\dfrac{nm}B+mB)\),取 \(B=\sqrt m\) 有最優復雜度 \(n\sqrt m\)。榮登 cf 最短代碼榜首。
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,x[N],y[N],a[N],p[N],buc[555][555];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)scanf("%d%d",&x[i],&y[i]);
for(int i=1;i<=m;i++){
int op,k,c,ans=0;
scanf("%d%d",&op,&k),c=x[k]+y[k];
if(op==1){
a[k]=i;
if(c<555)for(int j=0;j<c;j++)buc[c][(i+j)%c]+=j>=x[k];
else for(int j=i+x[k];j<=m;j+=c)p[j]++,p[min(m+1,j+y[k])]--;
} else{
if(c<555)for(int j=0;j<c;j++)buc[c][(a[k]+j)%c]-=j>=x[k];
else for(int j=a[k]+x[k];j<=m;j+=c)p[max(i,j)]--,p[max(i,min(m+1,j+y[k]))]++;
}
for(int j=2;j<555;j++)ans+=buc[j][i%j];
printf("%d\n",ans+(p[i]+=p[i-1]));
}
return 0;
}
Trick:根號分治進入較大的分支調不出來時,試試將塊大小設為 \(0\)。
2. cdq 分治
人類智慧算法。
在一個序列中,需要計算滿足某些限制的點對 \((i,j)\)(這里 \(i,j\) 都表示位置)對答案的貢獻,通常這些點對都有 \(\mathcal{O}(n^2)\) 個。cdq 分治的核心思想就是將所有需要計算貢獻的點對 \((i,j)\) 分成三類:第一類 \(i,j\in[1,mid]\);第二類 \(i,j\in(mid,n]\);第三類 \(i\in[1,mid],j\in(mid,n]\)。這樣一來就可以先遞歸處理第一、二類點對的答案,再運用一些方法快速求第三類的答案。
2.1. 例題
I. P3810 【模板】三維偏序(陌上花開)
題意簡述:對每個 \(d\),求使 \(a_j\leq a_i,b_j\leq b_i,c_j\leq c_i,i\neq j\) 的 \(j\) 的個數有 \(d\) 個的 \(i\) 的個數。
首先去重,cdq 一般解決不了有重復元素的問題,除非重復元素之間不算貢獻。接着將所有點按 \(a_i,b_i,c_i\) 分別為第一、二、三關鍵字從小到大排序。
這樣做,排除了 \(a_i\) 對答案的限制。因為右區間的任何一個點都不會對左區間中的任何一個點有貢獻。這樣一來,需要求的就變成了對右區間的每個點 \(i\),求左區間的所有點 \(j\) 中,滿足 \(b_j\leq b_i,c_j\leq c_i\) 的 \(j\) 有多少個。
先將區間內部的點按照 \(b_i,c_i\) 分別為第一、二關鍵字從小到大排序,那么對於每個 \(i\),可能符合條件(\(b_j\leq b_i\))的 \(j\) 一定是左區間的一段隨着 \(i\) 的增大單調不縮的前綴。對於一段前綴,求有多少個 \(j\) 滿足 \(c_j\leq c_i\) 就是樹狀數組的拿手好戲了。
視值域與序列大小同階(離散化一下即可),則時間復雜度為 \(\mathcal{O}(n\log^2 n)\)。
一些注意點:
- 樹狀數組在添加 / 刪除時權值為點的個數而不是 \(1\)。
- 別忘了考慮重復元素之間的貢獻,即最終答案還要加上該重復元素個數 \(-1\)。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,k,f[N];
struct pt{
int a,b,c,ans,cnt;
}d[N],u[N];
bool cmp1(pt a,pt b){return a.a!=b.a?a.a<b.a:a.b!=b.b?a.b<b.b:a.c<b.c;}
bool cmp2(pt a,pt b){return a.b!=b.b?a.b<b.b:a.c<b.c;}
int c[N<<1];
void add(int x,int v){while(x<=k)c[x]+=v,x+=x&-x;}
int query(int x){int s=0; while(x)s+=c[x],x-=x&-x; return s;}
void solve(int l,int r){
if(l==r)return;
int m=l+r>>1,le=l;
solve(l,m),solve(m+1,r);
sort(u+l,u+m+1,cmp2),sort(u+m+1,u+r+1,cmp2);
for(int i=m+1;i<=r;i++){
while(le<=m&&u[le].b<=u[i].b)add(u[le].c,u[le].cnt),le++;
u[i].ans+=query(u[i].c);
} for(int i=l;i<le;i++)add(u[i].c,-u[i].cnt);
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++)d[i].a=read(),d[i].b=read(),d[i].c=read();
sort(d+1,d+n+1,cmp1);
for(int i=1;i<=n;i++){
if(d[i].a!=d[i-1].a||d[i].b!=d[i-1].b||d[i].c!=d[i-1].c)u[++m]=d[i];
u[m].cnt++;
} solve(1,m);
for(int i=1;i<=m;i++)f[u[i].ans+u[i].cnt-1]+=u[i].cnt;
for(int i=1;i<=n;i++)print(f[i-1]),pc('\n');
return flush(),0;
}
II. P4755 Beautiful Pair
首先對其進行 cdq 分治,設當前區間為 \([l,r]\),\(m=\frac{l+r}{2}\)。
對於每個位置 \(i\),若 \(i\in[l,m]\) 則記 \(suf_i=\max_{j=i}^m a_j\),若 \(i\in[m+1,r]\) 則記 \(pre_i=\max_{j=m+1}^i a_j\)。
分別考慮最大值在 \([l,m]\) 之間與在 \([m+1,r]\) 之間的情況:若最大值在左側,則枚舉 \(i\in[l,m]\),找到右側的分界點 \(p\) 使得對於 \(j\in[m+1,p]\) 都有 \(pre_j\leq suf_i\),那么查詢 \([m+1,p]\) 有多少個 \(j\) 使得 $a_j\leq \frac{suf_i}{a_i} $(不等號右邊是定值),這個可以用主席樹或者 BIT 做到。反之同理。
別忘了離散化。注意最大值在右邊時要找分界點 \(p\) 使得對於 \(j\in[p,m]\) 都有 \(suf_j<pre_i\),而不是 \(\leq\),因為后者會多加上最大值在兩邊都出現的情況,而這種情況在考慮最大值在左邊時已經計算過。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+5;
ll n,ans,c,a[N],b[N],d[N],suf[N],pre[N];
ll node,R[N],ls[N<<5],rs[N<<5],val[N<<5];
void modify(int pre,ll &x,int l,int r,int p){
val[x=++node]=val[pre]+1,ls[x]=ls[pre],rs[x]=rs[pre];
if(l==r)return;
int m=l+r>>1;
if(p<=m)modify(ls[pre],ls[x],l,m,p);
else modify(rs[pre],rs[x],m+1,r,p);
} ll query(int l,int r,int p,int x,int y){
if(l==r)return val[y]-val[x];
int m=l+r>>1;
if(p<=m)return query(l,m,p,ls[x],ls[y]);
return val[ls[y]]-val[ls[x]]+query(m+1,r,p,rs[x],rs[y]);
}
ll solve(int l,int r){
if(l==r)return b[a[l]]==1;
ll m=l+r>>1,ans=solve(l,m)+solve(m+1,r);
for(int i=m+1;i<=r;i++)pre[i]=max(pre[i-1],a[i]);
for(int i=m;i>=l;i--)d[m-i+1]=suf[i]=max(suf[i+1],a[i]);
for(int i=l;i<=m;i++){
int p=upper_bound(pre+m+1,pre+r+1,suf[i])-pre-1;
if(p>m){
int nd=upper_bound(b+1,b+c+1,b[suf[i]]/b[a[i]])-b-1;
if(nd)ans+=query(1,c,nd,R[m],R[p]);
}
} for(int i=m+1;i<=r;i++){
int p=m+1-(lower_bound(d+1,d+m-l+2,pre[i])-d-1);
if(p<=m){
int nd=upper_bound(b+1,b+c+1,b[pre[i]]/b[a[i]])-b-1;
if(nd)ans+=query(1,c,nd,R[p-1],R[m]);
}
}
for(int i=l;i<=r;i++)pre[i]=suf[i]=0;
return ans;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i],b[i]=a[i];
sort(b+1,b+n+1),c=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+c+1,a[i])-b;
modify(R[i-1],R[i],1,c,a[i]);
}
cout<<solve(1,n)<<endl;
return 0;
}
*3. 莫隊
莫隊是優雅的暴力。
3.1. 普通莫隊
3.1.1. 算法介紹
莫隊常用來離線處理形如 “\(q\) 次詢問一段區間的答案,且區間 \([l,r]\) 擴展到 \([l\pm 1,r\pm 1]\) 的時間復雜度為 \(\mathcal{O}(\mathrm{polylog})\)” 的問題。莫隊可以支持修改,這將在帶修莫隊部分提及。
它的核心思想十分簡單:維護兩個指針 \(l,r\) 表示當前區間,並按照一定順序處理詢問使得時間復雜度最小。如果按照詢問次序伸縮區間,每次詢問的最壞時間復雜度為 \(\mathcal{O}(n)\),無法接受。
一個基本的想法是為了讓指針移動距離盡可能地少,可以將詢問按照某個端點為關鍵字排序。盡管該端點的移動距離是 \(\mathcal{O}(n)\),但另一個端點的移動距離無法保證,是 \(\mathcal{O}(nq)\),仍無法接受。
但注意到兩個端點的移動距離分別是 \(\mathcal{O}(n)\) 和 \(\mathcal{O}(nq)\),一個過優,一個過劣,都不是我們想要的。因此,為了平衡復雜度,自然想到使用根號平衡的思想。如果將左右端點的移動距離都控制在 \(n\sqrt n\) 以內,我們就得到了一個時間復雜度為 \(\mathcal{O}(n\sqrt n\times k)\) 的優秀算法(\(k\) 是擴展復雜度)。
接下來是一個非常神仙(但很好理解)的操作:我們分塊!具體地,將所有詢問離線下來,並按照左端點所在塊編號為第一關鍵字,右端點為第二關鍵字排序,按照該順序處理所有詢問的時間復雜度為 \(\mathcal{O}((n+q)\sqrt n)\)。每個塊內,左端點的移動距離不超過 \(\sqrt n\),而右端點的總移動距離不超過 \(n\)(因為它有序)。故左端點總移動距離為 \(q\sqrt n\),右端點總移動距離為 \(n\sqrt n\),因此得到上述優秀的時間復雜度。
3.1.2. 常數優化
關於莫隊,有一個人盡皆知的常數優化:奇偶排序。具體地說,如果左端點在奇塊,那么右端點從小到大排序,否則從大到小排序,這保證了我們右端點在左端點跨塊的時候不會從很右邊移到很左邊導致常數增大,因為這樣排序后右端點會類似波浪一樣左右掃動。一般可以優化 \(25\%\)。
3.1.3. 與分塊結合
注意到莫隊的本質是 \(n\sqrt n\) 次修改 + \(n\) 次詢問。因此對於一些需要使用數據結構維護的題目,如果使用 \(\mathcal{O}(1)\) 修改,\(\mathcal{O}(\sqrt n)\) 查詢的分塊可以去掉數據結構 \(\log\)。
3.2. 莫隊二次離線
3.2.1. 算法簡介
一般情況下,伸縮區間的時間復雜度為 \(\mathcal{O}(1)\)。但如果遇到無法在線性時間內伸縮區間的題目,但數據范圍又很大(如 \(n\sqrt n\log n\) 跑 2e5)怎么辦?GG,雙手離開鍵盤並問候出題人的母親。
首先思考為什么有些題目無法線性時間伸縮區間:新增加的位置對整個區間的貢獻和區間內每個數都有關,需要用數據結構維護,例如區間逆序對數。大部分情況下這樣的信息是可減的,此時我們可以使用莫隊二次離線方法做到更優的復雜度。
現在看一道例題:P4887 【模板】莫隊二次離線
我們設 \(f(i,[l,r])\) 表示 \(i\) 對區間 \([l,r]\) 的貢獻,即 \(\sum_{\\j=l}^r[\mathrm{popcount}(a_j\oplus a_i)=k]\)。我們再設 \(g([l_1,r_1],[l_2,r_2])\) 表示 \(\sum_{\\i=l_1}^{r_1}f(i,[l_2,r_2])\),即 \(\sum_{\\i=l_1}^{r_1}\sum_{\\j=l_2}^{r_2}[\mathrm{popcount}(a_i\oplus a_j)=k]\)。
考慮右端點向右移動時的貢獻 \(f(r,[l,r-1])\)。由於信息可減性,我們可以將其寫作 \(f(r,[1,r-1])-f(r,[1,l-1])\)。因此,假設右端點 \(r\to r+d\),那么新的貢獻即為
前半部分很容易 \(\dbinom{14}k\) 預處理出來,因此考慮如何計算后面的 \(-g([r+1,r+d],[1,l-1])\)。注意到這其實是一段前綴對一段區間的貢獻,而區間總長度是 \(n\sqrt n\) 的。如果我們將所有這樣的 “貢獻對” 按照前綴的位置從小到大排序(直接開個 vector 桶排,這就是二次離線:莫隊離線后將產生的詢問進行第二次離線),那么我們只需要支持 \(n\) 次修改,\(n\sqrt n\) 次查詢:每次加入 \(a_i\) 時,將桶中所有位置 \(j\) 使得 \(\mathrm{popcount}(j\oplus a_i)=k\) 的值 \(+1\),查詢時直接查桶中 \(a_c\ (r+1\leq c\leq r+d)\) 的值。時間復雜度 \(\mathcal{O}\left(n\left(\dbinom{14}k+\sqrt n\right)\right)\)。
需要注意上述只是 \(r\to r+k\) 的情況,剩下來三種情況如法炮制即可,請讀者自行推導。
從上面的例子中,我們可以初步感受到莫隊二次離線的威力:在運用莫隊的根號平衡思想基礎上,更進一步地運用了平衡復雜度的方法,是一個非常美妙且可愛(逐漸 ymx 化)的算法。
3.2.2. 注意點
-
推導過程中注意 \(g([1,x],[l,r])\) 的符號。
-
例題 \(k=0\) 時,若當前 \(c\leq i\ (r+1\leq c\leq r+d)\),這說明 \(b_{a_c}\) 受到了它自身的貢獻(因為桶中存儲的是前綴信息,而前綴右端點在當前點的右邊,故當前點對該前綴信息有貢獻),這是不可以的(題目中要求二元組 \(i,j\) 滿足 \(i<j\)),故需減去 \(1\)。從中可以看出計算貢獻需特別注意去掉不合法的貢獻。
3.3. 信息無法刪除:回滾莫隊
如果遇到維護如區間最大值這樣的信息導致我們無法快速縮短區間時,考慮如何不刪除地回答詢問:運用撤銷的思想。
將所有詢問按照左端點所在的塊排序,然后依次處理所有左端點落在某個塊內的詢問 \((l_i,r_i)\),需要確保這些詢問按照 \(r_i\) 從小到大有序。類似普通莫隊,我們仍然維護兩個指針,不同的是左指針 \(l\) 初始指向當前塊下一個塊的開頭,右指針 \(r=l-1\) 表示當前區間為空。然后,對於右端點,由於其有序,我們可以直接擴展。右端點擴展完畢后,我們再擴展左端點直到我們想要的位置並記錄答案。
接下來我們撤回擴展左端點時對信息的修改,這個可以在 \(\sqrt n\times\mathrm{polylog}\) 的時間內完成,因為左端點移動長度不超過 \(\sqrt n\)。撤回完畢后再處理下一個詢問,這就是回滾莫隊了。
注意點:
- 左右端點在同一個塊時,直接暴力即可。
- 每做完一個塊需要將信息清空。
3.4. 帶修:多參數莫隊
3.5. 例題
I. P5047 Yuno loves sqrt technology II
開幕 Ynoi,二次離線莫隊裸題。
設 \(F/G([1,x],[l,r])\) 表示區間 \([1,x]\) 比 \(a_i\ (1\leq i\leq x)\) 大 / 小的數的個數,\(f_x=F([1,x-1],x)\),\(g_x=G([1,x-1],x)\)。
-
右端點向右擴展:\(\sum f_i-F([1,l-1],i)\),第二項寫為 \(-F([1,l-1],[r+1,r+d])\)。
-
左端點向左擴展:\(\sum G([1,r],i)-g_i\),第一項寫為 \(G([1,r],[l-d,l-1])\)。
-
右端點向左擴展:\(\sum -f_i+F([1,l-1],i)\),第二項寫為 \(F([1,l-1],[r-d+1,r])\)。
-
左端點向右擴展:\(\sum -G([1,r],i)+g_i\),第一項寫為 \(-G([1,r],[l,l+d-1])\)。
因此我們需要維護兩個分塊數組,一個加入 \(a_i\) 時將 \(1\sim a_i-1\) 加 \(1\) 為了查詢 \(F\),另一個將 \(a_i+1\sim n\) 加 \(1\) 為了查詢 \(G\)。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
const int N=1e5+5;
const int B=321;
ll n,m,a[N],d[N],f[N],g[N],ans[N];
int bl[N],lp[N],rp[N];
struct Block{
int tag[N],sum[N];
void add(int x,int v){
if(x>n)return;
for(int i=x;i<=rp[bl[x]];i++)sum[i]+=v;
for(int i=bl[x]+1;i<=bl[n];i++)tag[i]+=v;
}
void add(int l,int r,int v){add(l,v),add(r+1,-v);}
int query(int x){return x?tag[bl[x]]+sum[x]:0;}
int query(int x,int y){return query(y)-query(x-1);}
}b,c,ept;
struct query{
int x,y,b,id;
bool operator < (const query &v) {
return b!=v.b?b<v.b:b&1?y>v.y:y<v.y;
}
}q[N];
struct data{
int l,r,id,type;
data(int x,int y,int z,int w){l=x,r=y,id=z,type=w;}
};
vector <data> buc[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)d[i]=a[i]=read(),bl[i]=i/B;
for(int i=0;i<=bl[n];i++)lp[i]=max(1,i*B),rp[i]=min((int)n,i*B+B-1);
for(int i=1;i<=m;i++)q[i].b=(q[i].x=read())/B,q[i].y=read(),q[i].id=i;
sort(d+1,d+n+1),sort(q+1,q+m+1);
for(int i=1;i<=n;i++)a[i]=lower_bound(d+1,d+n+1,a[i])-d;
for(int i=1;i<=n;i++)
f[i]=b.query(a[i]+1,n),g[i]=b.query(a[i]-1),b.add(a[i],1);
for(int i=1,l=1,r=0;i<=m;i++){
if(r<q[i].y){
buc[l-1].pb(r+1,q[i].y,-q[i].id,0);
while(r<q[i].y)ans[q[i].id]+=f[++r];
}
if(l>q[i].x){
buc[r].pb(q[i].x,l-1,q[i].id,1);
while(l>q[i].x)ans[q[i].id]-=g[--l];
}
if(r>q[i].y){
buc[l-1].pb(q[i].y+1,r,q[i].id,0);
while(r>q[i].y)ans[q[i].id]-=f[r--];
}
if(l<q[i].x){
buc[r].pb(l,q[i].x-1,-q[i].id,1);
while(l<q[i].x)ans[q[i].id]+=g[l++];
}
} b=ept;
for(int i=1;i<=n;i++){
b.add(a[i]+1,1),c.add(1,a[i]-1,1);
for(auto it:buc[i]){
int id=abs(it.id),tp=id/it.id;
for(int j=it.l;j<=it.r;j++)
ans[id]+=(it.type?b.query(a[j]):c.query(a[j]))*tp;
}
}
for(int i=1;i<=m;i++)ans[q[i].id]+=ans[q[i-1].id];
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}
II. P4887 【模板】莫隊二次離線(第十四分塊(前體))
莫隊二離的例題,這里給出代碼。
const int N = 1e5 + 5;
const int B = 320;
int n, m, k, a[N], f[N], buc[1 << 14];
ll cnt, vec[1 << 12], ans[N];
struct query {
int x, y, b, id;
bool operator < (const query &v) const {
return b != v.b ? b < v.b : b & 1 ? y > v.y : y < v.y;
}
} q[N];
struct modify {
int l, r, id;
modify(int x, int y, int z) {l = x, r = y, id = z;}
};
vector <modify> c[N];
int main() {
cin >> n >> m >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 0; i < 1 << 14; i++)
if(__builtin_popcount(i) == k) vec[++cnt] = i;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= cnt; j++) f[i] += buc[a[i] ^ vec[j]];
buc[a[i]] += 1;
} mem(buc, 0, 1 << 14);
for(int i = 1; i <= m; i++)
cin >> q[i].x >> q[i].y, q[i].b = q[i].x / B, q[i].id = i;
sort(q + 1, q + m + 1);
for(int i = 1, l = 1, r = 0; i <= m; i++) {
if(r < q[i].y) {
c[l - 1].pb(r + 1, q[i].y, -q[i].id);
while(r < q[i].y) ans[q[i].id] += f[++r];
}
if(l > q[i].x) {
c[r].pb(q[i].x, l - 1, q[i].id);
while(l > q[i].x) ans[q[i].id] -= f[--l];
}
if(r > q[i].y) {
c[l - 1].pb(q[i].y + 1, r, q[i].id);
while(r > q[i].y) ans[q[i].id] -= f[r--];
}
if(l < q[i].x) {
c[r].pb(l, q[i].x - 1, -q[i].id);
while(l < q[i].x) ans[q[i].id] += f[l++];
}
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= cnt; j++) buc[a[i] ^ vec[j]]++;
for(modify it : c[i]) {
int id = abs(it.id), tp = id / it.id;
for(int j = it.l; j <= it.r; j++)
ans[id] += tp * (buc[a[j]] - (j <= i && !k));
}
}
for(int i = 1; i <= m; i++) ans[q[i].id] += ans[q[i - 1].id];
for(int i = 1; i <= m; i++) printf("%lld\n", ans[i]);
return 0;
}
*III. P5501 [LnOI2019]來者不拒,去者不追
考慮右端點右移時需要求哪些信息:\(a_{r+1}\) 的排名以及 \([l,r]\) 比 \(a_{r+1}\) 大的數的和,用數據結構維護是 \(n\sqrt n\log a\) 的,無法接受,那么使用莫隊二離即可。
時間復雜度 \((n+m)(\sqrt n+\sqrt a)\)。還是比較卡常的。注意特殊考慮端點處 \(a_{l/r}\) 的貢獻。
const int N=5e5+5;
const int V=1e5+5;
const int BV=333;
const int B=777;
int mx,lp[V+1],rp[V+1],bl[V+1];
struct Block{
ll sum[V+1],tag[V+1];
void add(int x,int v){
if(x>V)return;
for(int i=x;i<=rp[bl[x]];i++)sum[i]+=v;
for(int i=bl[x]+1;i<=mx;i++)tag[i]+=v;
}
ll query(int x){return sum[x]+tag[bl[x]];}
void add(int l,int r,int v){add(l,v),add(r+1,-v);}
ll query(int x,int y){return query(y)-query(x-1);}
}num,sum,ept;
ll n,m,a[N],f[N],ans[N];
struct query{
int x,y,b,id;
bool operator < (const query &v) const{
return b!=v.b?b<v.b:b&1?y>v.y:y<v.y;
}
}q[N];
struct data{
int l,r,id;
data(int x,int y,int z){l=x,r=y,id=z;}
};
vector <data> buc[N];
int main(){
for(int i=0;i<=V;i++)bl[i]=i/BV; mx=bl[V];
for(int i=0;i<=mx;i++)lp[i]=i*BV,rp[i]=min(V,(i+1)*BV-1);
cin>>n>>m;
for(int i=1;i<=n;i++){
a[i]=read(),f[i]=num.query(1,a[i]-1)*a[i];
f[i]+=sum.query(a[i]+1,V);
num.add(a[i],1),sum.add(a[i],a[i]);
}
for(int i=1;i<=m;i++)q[i].b=(q[i].x=read())/B,q[i].y=read(),q[i].id=i;
sort(q+1,q+m+1);
for(int i=1,l=1,r=0;i<=m;i++){
if(r<q[i].y){
buc[l-1].pb(r+1,q[i].y,-q[i].id);
while(r<q[i].y)r++,ans[q[i].id]+=f[r]+a[r];
}
if(l>q[i].x){
buc[r].pb(q[i].x,l-1,q[i].id);
while(l>q[i].x)l--,ans[q[i].id]-=f[l]-a[l];
}
if(r>q[i].y){
buc[l-1].pb(q[i].y+1,r,q[i].id);
while(r>q[i].y)ans[q[i].id]-=f[r]+a[r],r--;
}
if(l<q[i].x){
buc[r].pb(l,q[i].x-1,-q[i].id);
while(l<q[i].x)ans[q[i].id]+=f[l]-a[l],l++;
}
}
sum=num=ept;
for(int i=1;i<=n;i++){
num.add(a[i]+1,V,1),sum.add(1,a[i]-1,a[i]);
for(auto it:buc[i]){
int id=abs(it.id),tp=it.id/id;
for(int j=it.l;j<=it.r;j++)
ans[id]+=tp*(sum.query(a[j])+num.query(a[j])*a[j]);
}
}
for(int i=1;i<=m;i++)ans[q[i].id]+=ans[q[i-1].id];
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}
IV. P4462 [CQOI2018]異或序列
一道莫隊裸題,根據 \(a\oplus k=b\Leftrightarrow b\oplus k =a\),開個桶記錄一下每個數的出現次數即可。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
const int N=1e5+5;
const int B=333;
int n,m,k,a[N],buc[N<<1];
struct query{
int l,r,b,id;
bool operator < (const query &v) const {
return b!=v.b?b<v.b:b&1?r>v.r:r<v.r;
}
}q[N];
ll cur,ans[N];
int main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++)cin>>a[i],a[i]^=a[i-1];
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r,q[i].l--;
q[i].b=q[i].l/B,q[i].id=i;
}
sort(q+1,q+m+1),buc[0]=1;
for(int i=1,l=0,r=0;i<=m;i++){
while(r<q[i].r)r++,cur+=buc[a[r]^k],buc[a[r]]++;
while(l>q[i].l)l--,cur+=buc[a[l]^k],buc[a[l]]++;
while(r>q[i].r)buc[a[r]]--,cur-=buc[a[r]^k],r--;
while(l<q[i].l)buc[a[l]]--,cur-=buc[a[l]^k],l++;
ans[q[i].id]=cur;
}
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}
V. CF617E XOR and Favorite Number
雙倍經驗,代碼就不放了。
VI. P4396 [AHOI2013]作業
莫隊 + 對值域分塊即可。比較裸,時間復雜度 \(n\sqrt n\)。
const int N=1e5+5;
int B1,B2,lp[N],rp[N],bl[N],buc[N],sum[N],bsum[N];
void add(int x){if(!buc[x])sum[bl[x]]++; buc[x]++,bsum[bl[x]]++;}
void del(int x){buc[x]--,bsum[bl[x]]--; if(!buc[x])sum[bl[x]]--;}
int query1(int l,int r){
int ans=0;
for(int i=bl[l];i<=bl[r];i++)ans=ans+bsum[i];
for(int i=lp[bl[l]];i<l;i++)ans=ans-buc[i];
for(int i=r+1;i<=rp[bl[r]];i++)ans=ans-buc[i];
return ans;
}
int query2(int l,int r){
int ans=0;
for(int i=bl[l];i<=bl[r];i++)ans=ans+sum[i];
for(int i=lp[bl[l]];i<l;i++)ans=ans-(buc[i]>0);
for(int i=r+1;i<=rp[bl[r]];i++)ans=ans-(buc[i]>0);
return ans;
}
int n,m,a[N],ans1[N],ans2[N];
struct query{
int l,r,a,b,bl,id;
bool operator < (const query &v) const {
return bl!=v.bl?bl<v.bl:bl&1?r>v.r:r<v.r;
}
}q[N];
int main(){
cin>>n>>m,B1=sqrt(n),B2=250;
for(int i=1;i<N;i++)bl[i]=i/B2;
for(int i=0;i<=bl[N-1];i++)lp[i]=max(1,i*B2),rp[i]=min(N-1,(i+1)*B2-1);
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=m;i++){
q[i].l=read(),q[i].r=read(),q[i].a=read(),q[i].b=read();
q[i].bl=q[i].l/B1,q[i].id=i;
}
sort(q+1,q+m+1);
for(int i=1,l=1,r=0;i<=m;i++){
while(r<q[i].r)add(a[++r]);
while(l>q[i].l)add(a[--l]);
while(r>q[i].r)del(a[r--]);
while(l<q[i].l)del(a[l++]);
ans1[q[i].id]=query1(q[i].a,q[i].b);
ans2[q[i].id]=query2(q[i].a,q[i].b);
}
for(int i=1;i<=m;i++)printf("%d %d\n",ans1[i],ans2[i]);
return 0;
}
VII. P5906 【模板】回滾莫隊&不刪除莫隊
回滾莫隊的裸題。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
const int N=2e5+5;
const int B=333;
int n,m,c,a[N],d[N],res[N];
struct query{
int l,r,bl,id;
bool operator < (const query &v) const{
return bl!=v.bl?bl<v.bl:r<v.r;
}
}q[N];
int pre[N],suf[N],ans;
int top,stc[N],pp[N],ps[N],pa[N];
void rollback(){
while(top){
pre[stc[top]]=pp[top];
suf[stc[top]]=ps[top];
ans=pa[top],top--;
}
}
void add(int x,int tp){
if(tp)stc[++top]=a[x],pp[top]=pre[a[x]],ps[top]=suf[a[x]],pa[top]=ans;
pre[a[x]]=min(pre[a[x]],x),suf[a[x]]=max(suf[a[x]],x);
ans=max(ans,suf[a[x]]-pre[a[x]]);
}
int main(){
cin>>n,mem(pre,0x3f,N);
for(int i=1;i<=n;i++)d[i]=a[i]=read();
sort(d+1,d+n+1);
for(int i=1;i<=n;i++)a[i]=lower_bound(d+1,d+n+1,a[i])-d;
cin>>m;
for(int i=1;i<=m;i++){
int l=read(),r=read(),ans=0;
if(l/B==r/B){
for(int j=l;j<=r;j++){
pre[a[j]]=min(pre[a[j]],j);
suf[a[j]]=max(suf[a[j]],j);
ans=max(ans,suf[a[j]]-pre[a[j]]);
} res[i]=ans;
for(int j=l;j<=r;j++)pre[a[j]]=1e9,suf[a[j]]=0;
continue;
}
q[++c]={l,r,l/B,i};
}
sort(q+1,q+c+1);
for(int i=1,l,r;i<=c;i++){
if(i==1||q[i].bl!=q[i-1].bl){
mem(pre,0x3f,N),mem(suf,0,N),top=ans=0;
l=q[i].bl*B+B,r=l-1;
}
while(r<q[i].r)add(++r,0);
while(l>q[i].l)add(--l,1);
res[q[i].id]=ans,rollback(),l=q[i].bl*B+B;
}
for(int i=1;i<=m;i++)printf("%d\n",res[i]);
return 0;
}
VIII. P3709 大爺的字符串題
題意翻譯過來就是求區間眾數,那么只需莫隊維護每個數的出現次數以及出現次數為 \(i\) 的數有多少個即可。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
代碼使用了回滾莫隊,信息維護更簡單一些。
const int N=2e5+5;
const int B=300;
int n,m,ans,a[N],d[N],buc[N],res[N];
struct query{
int l,r,b,id;
bool operator < (const query &v) const {
return b!=v.b?b<v.b:r<v.r;
}
}q[N];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i],d[i]=a[i];
sort(d+1,d+n+1);
for(int i=1;i<=n;i++)a[i]=lower_bound(d+1,d+n+1,a[i])-d;
for(int i=1;i<=m;i++)cin>>q[i].l>>q[i].r,q[i].b=q[i].l/B,q[i].id=i;
sort(q+1,q+m+1);
for(int i=1,r;i<=m;i++){
int l=q[i].b*B+B;
if(i==1||q[i].b!=q[i-1].b)mem(buc,0,N),r=l-1,ans=0;
if(q[i].l/B==q[i].r/B){
int tmp=0;
for(int j=q[i].l;j<=q[i].r;j++)
tmp=max(tmp,++buc[a[j]]);
res[q[i].id]=tmp;
for(int j=q[i].l;j<=q[i].r;j++)buc[a[j]]=0;
continue;
}
while(r<q[i].r)ans=max(ans,++buc[a[++r]]);
int tmp=ans;
for(int j=q[i].l;j<l;j++)ans=max(ans,++buc[a[j]]);
res[q[i].id]=ans,ans=tmp;
for(int j=q[i].l;j<l;j++)buc[a[j]]--;
}
for(int i=1;i<=m;i++)printf("%d\n",-res[i]);
return 0;
}
IX. P3730 曼哈頓交易
仍然是莫隊裸題,求出現次數第 \(k\) 大可以用分塊,將根號平衡的思想貫徹到底。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
const int N=1e5+5;
const int B=333;
int mx,bl[N],lp[N],rp[N],buc[N],pos[N],sum[N];
void add(int x,int v){pos[x]+=v,sum[bl[x]]+=v;}
int query(int k){
int pre=0;
for(int i=0;i<=mx;i++){
if(pre+sum[i]<k)pre+=sum[i];
else for(int j=lp[i];j<=rp[i];j++)
if((pre+=pos[j])>=k)return j;
}
}
int n,m,a[N],d[N],res[N],tot;
struct query{
int l,r,k,b,id;
bool operator < (const query &v) const {
return b!=v.b?b<v.b:b&1?r>v.r:r<v.r;
}
}q[N];
void add(int x){
if(buc[x])add(buc[x],-1);
else tot++;
add(++buc[x],1);
}
void del(int x){
add(buc[x],-1);
if(--buc[x])add(buc[x],1);
else tot--;
}
int main(){
for(int i=1;i<N;i++)bl[i]=i/B; mx=bl[N-1];
for(int i=0;i<=mx;i++)lp[i]=max(1,i*B),rp[i]=min(N-1,i*B+B-1);
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i],d[i]=a[i];
sort(d+1,d+n+1);
for(int i=1;i<=n;i++)a[i]=lower_bound(d+1,d+n+1,a[i])-d;
for(int i=1,l,r,k;i<=m;i++)cin>>l>>r>>k,q[i]={l,r,k,bl[l],i};
sort(q+1,q+m+1);
for(int i=1,l=1,r=0;i<=m;i++){
while(r<q[i].r)add(a[++r]);
while(l>q[i].l)add(a[--l]);
while(r>q[i].r)del(a[r--]);
while(l<q[i].l)del(a[l++]);
if(tot<q[i].k)res[q[i].id]=-1;
else res[q[i].id]=query(q[i].k);
}
for(int i=1;i<=m;i++)printf("%d\n",res[i]);
return 0;
}
*X. P7708「Wdsr-2.7」八雲藍自動機 Ⅰ
一道莫隊好題。私以為本題最有價值的地方在於對單點修改的轉化以及對交換兩個數的處理:需要維護原來每個位置現在的位置,以及現在每個位置原來的位置。
注意到這個單點修改並不方便實現(如果是單點加就好做了),我們可以使用一個小技巧將其轉化為交換兩個數,即新建一個 \(a_c=k\),並將其看做 \(a_x\) 與 \(a_c\) 交換。這一步非常巧妙,因為它消滅了單點修改這一麻煩的操作。
對於多次詢問一段區間的操作結果,我們通常需要使用莫隊實現,因此考慮區間在伸縮時需要維護什么東西。為了支持在操作序列最前面加入交換兩個數的操作,我們不難想到維護:
-
序列 \(a\) 在操作后變成了什么樣。
-
\(pos_i\) 表示現位置 \(i\) 是原來的哪個位置。
-
\(rev_i\) 表示原位置 \(i\) 現在在哪。
-
\(add_i\) 表示原位置 \(i\) 上的數被查詢了多少次。
-
當右端點右移 \(r-1\to r\) 時:
- 若第 \(r\) 個操作是交換 \(x,y\),則交換 \(a_x,a_y\),\(pos_x,pos_y\),\(rev_{pos_x},rev_{pos_y}\)。
- 若第 \(r\) 個操作是查詢 \(x\),則令 \(ans\gets ans+a_x\),\(add_{pos_x}\gets add_{pos_x}+1\)。
-
當左端點左移 \(l+1\to l\) 時:
- 若第 \(l\) 個操作是交換 \(x,y\),注意我們相當於交換 “原位置” 的兩個數,因此對答案有影響。令 \(del=a_{rev_y}-a_{rev_x}\),答案需加上 \(del\times (add_x-add_y)\),即計算原來的 \(a_x,a_y\) 即現在的 \(a_{rev_x},a_{rev_y}\) 在交換后的貢獻變化量。此外,交換 \(a_{rev_x},a_{rev_y}\) \(add_x,add_y\),\(rev_x,rev_y\) 以及 \(pos_{rev_x},pos_{rev_y}\)。
- 若第 \(l\) 個操作是查詢 \(x\),則令 \(ans\gets ans+a_{rev_x}\),\(add_x\gets add_x+1\),意義顯然。
右端點左移和左端點右移的情況分別與上述兩種情況相似,不再贅述。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
const int N=4e5+5;
const int B=666;
uint n,m,q,cnt,a[N],id[N],op[N],x[N],y[N];
struct query{
int l,r,blk,id;
bool operator < (const query &v) const {
return blk!=v.blk?blk<v.blk:blk&1?r>v.r:r<v.r;
}
}c[N];
uint ans,res[N],pos[N],rev[N],add[N];
int main(){
cin>>n>>m,cnt=n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++){
cin>>op[i]>>x[i]; if(op[i]!=3)cin>>y[i];
if(op[i]==1)a[++cnt]=y[i],op[i]=2,y[i]=cnt;
}
for(int i=1;i<=cnt;i++)pos[i]=rev[i]=i;
cin>>q;
for(int i=1,l,r;i<=q;i++)cin>>l>>r,c[i]={l,r,l/B,i};
sort(c+1,c+q+1);
for(int i=1,l=1,r=0;i<=q;i++){
while(r<c[i].r){
r++;
if(op[r]==2){
swap(pos[x[r]],pos[y[r]]),swap(a[x[r]],a[y[r]]);
swap(rev[pos[x[r]]],rev[pos[y[r]]]);
}
else ans+=a[x[r]],add[pos[x[r]]]++;
}
while(l>c[i].l){
l--;
if(op[l]==2){
uint del=a[rev[y[l]]]-a[rev[x[l]]];
swap(rev[x[l]],rev[y[l]]),ans+=(add[x[l]]-add[y[l]])*del;
swap(a[rev[x[l]]],a[rev[y[l]]]);
swap(pos[rev[x[l]]],pos[rev[y[l]]]),swap(add[x[l]],add[y[l]]);
}
else ans+=a[rev[x[l]]],add[x[l]]++;
}
while(r>c[i].r){
if(op[r]==2){
swap(pos[x[r]],pos[y[r]]),swap(a[x[r]],a[y[r]]);
swap(rev[pos[x[r]]],rev[pos[y[r]]]);
}
else ans-=a[x[r]],add[pos[x[r]]]--;
r--;
}
while(l<c[i].l){
if(op[l]==2){
uint del=a[rev[y[l]]]-a[rev[x[l]]];
swap(rev[x[l]],rev[y[l]]),ans+=(add[x[l]]-add[y[l]])*del;
swap(a[rev[x[l]]],a[rev[y[l]]]);
swap(pos[rev[x[l]]],pos[rev[y[l]]]),swap(add[x[l]],add[y[l]]);
}
else ans-=a[rev[x[l]]],add[x[l]]--;
l++;
}
res[c[i].id]=ans;
}
for(int i=1;i<=q;i++)cout<<res[i]<<"\n";
return 0;
}