資料結構專題-學習筆記:莫隊#2(帶修莫隊,樹上莫隊)
回顧:
上回我們在莫隊演算法總結&專題訓練1講解了莫隊的一般套路以及各種優化方式,但那只是基礎,接下來將會介紹莫隊更多的用法。這篇博文將會講述 帶修莫隊、樹上莫隊、樹上帶修莫隊 的用法,在莫隊演算法總結&專題訓練3 中將會講述 回滾莫隊/不刪除莫隊、莫隊二次離線/第十四分塊(前體) 的思路以及實現。同時總結將會寫在 莫隊演算法總結&專題訓練3 當中。
3.練習題
題單:
- 普通練手題
- CF220B Little Elephant and Array
- P2709 小 B 的詢問
- P1494 [國家集訓隊]小 Z 的襪子
- 帶修莫隊
- P1903 [國家集訓隊]數顏色 / 維護佇列
- 樹上莫隊
- SP10707 COT2 - Count on a tree II
- 樹上帶修莫隊
- P4074 [WC2013]糖果公園
- 回滾莫隊/不刪除莫隊
- AT1219 歴史の研究
- 莫隊二次離線/第十四分塊(前體)
- P4887 【模板】莫隊二次離線(第十四分塊(前體))
普通練手題
這裡的題目都比較簡單,因此程式碼就少貼一點。只需要更改 del&add 函式即可。主函式幾乎不需要改。
CF220B Little Elephant and Array
首先可以發現,\(a_i>n\) 的資料完全沒有用,因此 \(cnt\) 統計 \(a_i<n\)
其次需要注意,只有 \(cnt_{a_i}=a_i\) 的資料是有效資料,大於或小於都不行
知道了這些,就跟例題沒什麼兩樣了(見莫隊演算法總結&專題訓練1)。
貼一下 del&add 函式:
void del(int x) { if(a[x]>n) return ; if(cnt[a[x]]==a[x]) total--; cnt[a[x]]--; if(cnt[a[x]]==a[x]) total++; } void add(int x) { if(a[x]>n) return ; if(cnt[a[x]]==a[x]) total--; cnt[a[x]]++; if(cnt[a[x]]==a[x]) total++; }
簡直模板。
當然這道題到目前為止沒有 \(\log\) 級別的做法。
更新答案時有兩種操作,一種手推公式更新,一種直接暴力先減掉 \(cnt_x^2\) 處理完之後再加回去,我採用的是第二種。
貼一下 del&add 函式:
void Delete(int x)
{
sum-=cnt[a[x]]*cnt[a[x]];
cnt[a[x]]--;
sum+=cnt[a[x]]*cnt[a[x]];
}
void Add(int x)
{
sum-=cnt[a[x]]*cnt[a[x]];
cnt[a[x]]++;
sum+=cnt[a[x]]*cnt[a[x]];
}
這道題需要手推一下公式。
假設顏色為 \(a,b,c\) 的資料分別出現了 \(x,y,z\) 次,那麼答案:
\(\dfrac{\frac{x \times (x-1)}{2} + \frac{y \times (y-1)}{2} + \frac{z \times (z-1)}{2} +...}{\frac{(r-l) \times (r-l+1)}{2}}\)
\(= \dfrac{x^2+y^2+z^2+......-(x+y+z+...)}{(r-l) \times (r-l+1)}\)
\(=\dfrac{x^2+y^2+z^2+......-(r-l+1)}{(r-l) \times (r-l+1)}\)
所以我們只需要維護平方和就好。程式碼不貼了。
需要注意:
- 不要忘記約分。
道路千萬條,long long 第一條。乘積存 int ,爆零兩行淚。
帶修莫隊
這裡我們將接觸莫隊的第一個變種:帶修莫隊。
在上一個博文中,我說過一般的莫隊是不能支援線上的,但是對於一部分修改,莫隊還是能夠承受的。
最典型的題目:P1903 [國家集訓隊]數顏色 / 維護佇列
這道題的詢問簡直就是太水了呀!但是修改卻使得莫隊不能簡單實現。
我們想一想,當初我們是怎麼解決莫隊的優化二的?兩個指標 \(l,r\) 移來移去。那麼究其原因,到底為什麼我們要用兩個指標 \(l,r\) 呢?難道只是從尺取法上得到的啟發嗎?
不僅僅是尺取法!我們使用 \(l,r\) 的很重要的原因就是因為他的詢問是二維的(指區間是 \([l,r]\) 是二維的),可以使用兩個指標操作!
那麼這裡,我們可以將修改看作多出來的一維時間,區間變成了 \([l,r,t]\) 三維,要解決它我們直接再弄一維時間 \(t\) 上去,讓第三個指標 \(t\) 在時間軸上移來移去不就好了?
這就是帶修莫隊的主要思路:使用第三個指標 \(t\) 在時間軸上動,將兩個修改之間的所有詢問操作看成一個時間上的(包括這些詢問操作之前的第一個修改操作也是同一個時間點),這樣移動的時候將在區間裡面的數和答案更新一下,同時處理修改即可。
這裡的排序也需要注意:第一關鍵字是左端點所在的塊,第二關鍵字是右端點所在的塊,第三關鍵字是時間。
放程式碼:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=133333+10,MAXA=1e6+10;
int n,m,a[MAXN],cnt[MAXA],total,ans[MAXN],cntq,cntc,size,block,ys[MAXN<<1];
struct query
{
int l,r,id,Time;
}q[MAXN];
struct change
{
int pos,val;
}c[MAXN];
int read()
{
int sum=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+(ch^48);ch=getchar();}
return sum;
}
void print(int x,char tail=0)
{
if(x>9) print(x/10);
putchar(x%10+48);
if(tail) putchar(tail);
}
bool cmp(const query &fir,const query &sec)
{
if(ys[fir.l]^ys[sec.l]) return ys[fir.l]<ys[sec.l];
if(ys[fir.r]^ys[sec.r]) return ys[fir.r]<ys[sec.r];//再次提醒,第二關鍵字是右端點所在的塊而不是右端點!
return fir.Time<sec.Time;
}
int main()
{
n=read();m=read();size=2000;//手動調塊長qwq
for(int i=1;i<=n;++i) ys[i]=(i-1)/size+1;
for(int i=1;i<=n;++i) a[i]=read();
for(int i=1;i<=m;++i)
{
char ch=getchar();
while(ch==' '||ch=='\n'||ch=='\r') ch=getchar();
if(ch=='Q')
{
q[++cntq].l=read();
q[cntq].r=read();
q[cntq].id=cntq;
q[cntq].Time=cntc;//處理時間
}
else
{
c[++cntc].pos=read();
c[cntc].val=read();
}//儲存修改操作
}
sort(q+1,q+cntq+1,cmp);
int l=1,r=0,t=0;
for(int i=1;i<=cntq;++i)
{
while(l<q[i].l) total-=!--cnt[a[l++]];
while(l>q[i].l) total+=!cnt[a[--l]]++;
while(r<q[i].r) total+=!cnt[a[++r]]++;
while(r>q[i].r) total-=!--cnt[a[r--]];
while(t<q[i].Time)
{
++t;
if(q[i].l<=c[t].pos&&c[t].pos<=q[i].r) total-=!--cnt[a[c[t].pos]]-!cnt[c[t].val]++;
swap(a[c[t].pos],c[t].val);
}
while(t>q[i].Time)
{
if(q[i].l<=c[t].pos&&c[t].pos<=q[i].r) total-=!--cnt[a[c[t].pos]]-!cnt[c[t].val]++;
swap(a[c[t].pos],c[t].val);
--t;
}
ans[q[i].id]=total;
}
for(int i=1;i<=cntq;++i) print(ans[i],'\n');
return 0;
}
總結一下:
對於帶修莫隊這種二維變三維的東西,最簡單的方法就是新開一個變數/陣列/資料結構維護。比如帶修莫隊就是使用了第三個指標 \(t\) 來維護時間軸。
樹上莫隊
到目前為止所碰到的莫隊,都是在序列上操作的。既然莫隊如此萬能(霧),那麼我們能不能夠讓莫隊去樹上玩一玩呢?
答案是肯定的。不要認為這個東西很難,實際上還是一個序列。
前置知識:求最近公共祖先(lca),任何一種方法都可以。
作者使用的是倍增演算法求 lca 。
SP10707 COT2 - Count on a tree II
由於莫隊只能夠在序列上操作,因此我們首先就要想辦法將樹變成一個序列。最容易想到的當然是 DFS序。
比如下面這棵樹:
DFS序:1 2 4 9 14 15 10 11 5 12 3 6 13 7 8
4->13上的節點:4 2 1 3 6 13
在 DFS序 上對應的區間:唉等等,區間呢?
這裡,我可以很負責任的告訴你:普通的 DFS序 搞不了樹上莫隊。
那麼還有沒有什麼辦法呢?
有!不要忘記我們還有 尤拉序 這一利器。
首先跑一遍尤拉序:
1 2 4 9 14 14 15 15 9 10 10 11 11 4 5 12 12 5 2 3 6 13 13 6 7 7 8 8 3 1
關於尤拉序的求法可以自行百度(不過應該都看出來了),那麼它為什麼能夠將樹上莫隊轉換成區間莫隊呢?
假設我們要找 1->10 上的節點:1 2 4 10
把尤拉序上 1->10 的區間拿出來······等等,有兩個 1,那麼拿哪一個呢?
這裡,我們統一取前面的 1 。為了方便,接下來令 \(fir_i\) 表示 \(i\) 節點在尤拉序中第一次出現的位置,\(las_i\) 表示 \(i\) 節點在尤拉序中第二次出現的位置。
那麼我們把 \(fir_1,fir_{10}\) 的區間拉出來:1 2 4 9 14 14 15 15 9 10
也不對啊,這裡面還是有 9,14,15 等干擾資料啊,這方法是不是不靠譜啊?
但是不要忘記在講優化二的時候我提到過:莫隊處理答案時先加一個數在刪一個數是對答案沒有影響的。
所以我們可以用一個 \(vis\) 陣列來記錄當前節點有沒有被訪問過,訪問過就刪,否則就加。這樣,刪除了出現兩次的資料, \([fir_1,fir_{10}]\) 就等價於 1 2 4 10
。
然而這樣做有個問題。
比如我們要查詢 4->13 對應的節點:4 2 1 3 6 13
把 \([fir_4,fir_{13}]\) 拉出來:4 9 14 14 15 9 10 10 11 11 4 5 12 12 5 2 3 6 13
刪除重複元素······4呢?怎麼刪沒了?
這就是問題 1:我們很容易在操作的時候把 4 幹掉了,這樣答案就會不正確。
處理方法:使用 \([las_4,fir_{13}]\) 的區間。這樣,就可以避免刪除 4。
但是上面又說使用 \([fir,fir]\) 區間,那麼什麼時候用 \(las\) ,什麼時候用 \(fir\) 呢?
不急,先把 \([las_4,fir_{13}]\) 拉出來:4 5 12 12 5 2 3 6 13
刪除公共元素:4 2 3 6 13
。等等, 1 呢? 1 是 \(lca(4,13)\) 啊!
那麼我們在處理的時候還要加上 \(lca(4,13)\) 。但是因為 \(lca(4,13)\) 不在區間內,那麼在算完答案之後還要刪掉。
那麼解答前面的問題:什麼時候用 \([fir,fir]\) ,什麼時候用 \([las,fir]\) 呢?
這也就是樹上莫隊對詢問的處理:
- 首先假設詢問 \(x->y\) ,那麼算一下 \(lca(x,y)\) 。為了方便,我們規定 \(fir_x<fir_y\) ,不滿足就交換。
- 然後,如果 \(lca(x,y)=x\) 那麼使用 \([fir_x,fir_y]\) ,因為此時 \(x,y\) 在一條鏈上。否則,使用 \([las_x,fir_y]\) ,同時記錄 \(lca(x,y)\) 。
在處理詢問時要注意兩個點:
- 若 \(x,y\) 在一條鏈上,不要處理 \(lca\) ,否則要處理 \(lca\) 。
- \(lca\) 需要處理兩次(因為不在區間內)
幾個坑點(重點):
- 尤拉序長度是 \(2n\) ,千萬不能在這裡 TLE 了!
- 塊長調 \((2n)^{\frac{2}{3}}\)。
- 針對這個題不要忘記離散化。
上程式碼:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e5+10;
int n,m,cnt[MAXN],eular[MAXN<<1],cnte,ans[MAXN],total,ys[MAXN<<1],block,fa[MAXN][21],dep[MAXN],a[MAXN],fir[MAXN],las[MAXN],vis[MAXN],b[MAXN],lastn;
struct node
{
int l,r,id,lca;
}q[MAXN];
vector<int>Next[MAXN];
int read()
{
int sum=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+(ch^48);ch=getchar();}
return sum;
}
void dfs(int x)
{
eular[++cnte]=x;
fir[x]=cnte;
for(int i=0;i<Next[x].size();i++)
{
int u=Next[x][i];
if(dep[u]) continue;
dep[u]=dep[x]+1;
fa[u][0]=x;
dfs(u);
}
eular[++cnte]=x;
las[x]=cnte;
}
void st()
{
for(int j=1;j<=20;j++)
for(int i=1;i<=n;i++)
fa[i][j]=fa[fa[i][j-1]][j-1];
}
int getlca(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int j=20;j>=0;j--) if(dep[x]>=dep[y]) x=fa[x][j];
if(x==y) return x;
for(int j=20;j>=0;j--) if(fa[x][j]!=fa[y][j]) x=fa[x][j],y=fa[y][j];
return fa[x][0];
}
bool cmp(const node &fir,const node &sec)
{
if(ys[fir.l]^ys[sec.l]) return ys[fir.l]<ys[sec.l];
if(ys[fir.l]&1) return fir.r<sec.r;
return fir.r>sec.r;
}
void del(int x)
{
int t=lower_bound(b+1,b+lastn+1,a[x])-b-1;
total-=!--cnt[t];
}
void add(int x)
{
int t=lower_bound(b+1,b+lastn+1,a[x])-b-1;
total+=!cnt[t]++;
}
void work(int pos)
{
vis[pos]?del(pos):add(pos);vis[pos]^=1;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) b[i]=a[i]=read();
sort(b+1,b+n+1);
lastn=unique(b+1,b+n+1)-b-1;
for(int i=1;i<n;i++)
{
int x=read(),y=read();
Next[x].push_back(y);
Next[y].push_back(x);
}
fa[1][0]=1;dep[1]=1;dfs(1);st();
block=ceil(sqrt(cnte));
for(int i=1;i<=(n<<1);i++) ys[i]=(i-1)/block+1;
for(int i=1;i<=m;i++)
{
int x=read(),y=read(),lca=getlca(x,y);q[i].id=i;
if(fir[x]>fir[y]) swap(x,y);
if(fir[x]==lca) q[i].l=fir[x],q[i].r=fir[y];
else q[i].l=las[x],q[i].r=fir[y],q[i].lca=lca;
}
sort(q+1,q+m+1,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++)
{
while(l<q[i].l) work(eular[l++]);
while(l>q[i].l) work(eular[--l]);
while(r<q[i].r) work(eular[++r]);
while(r>q[i].r) work(eular[r--]);
if(q[i].lca) work(q[i].lca);
ans[q[i].id]=total;
if(q[i].lca) work(q[i].lca);
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
樹上帶修莫隊
樹上帶修莫隊,顧名思義,就是將 樹上莫隊 和 帶修莫隊 結合在一起的莫隊。
因此只要你掌握了 樹上莫隊 和 帶修莫隊 ,那麼樹上帶修莫隊簡直就是輕而易舉!
具體的思路如下:
- 首先按照樹上莫隊思路跑一遍尤拉序,處理操作。
- 處理操作時如果是詢問操作那麼按照樹上莫隊處理;同時根據帶修莫隊的處理方式,不要忘記處理修改和時間軸。
- 搞三個指標 \(l,r,t\) 處理即可。
思路與實現參見這篇文章->link
那麼到目前位置,我們已經講完了 普通莫隊、帶修莫隊、樹上莫隊、樹上帶修莫隊 四種莫隊,在 莫隊演算法總結&專題訓練3 中,將會講解最後兩種莫隊:回滾莫隊/不刪除莫隊,莫隊二次離線/第十四分塊(前體),同時將會總結長達三篇博文的莫隊講解。