1. 程式人生 > 其它 >資料結構專題-學習筆記:莫隊#2(帶修莫隊,樹上莫隊)

資料結構專題-學習筆記:莫隊#2(帶修莫隊,樹上莫隊)

目錄

回顧:

上回我們在莫隊演算法總結&專題訓練1講解了莫隊的一般套路以及各種優化方式,但那只是基礎,接下來將會介紹莫隊更多的用法。這篇博文將會講述 帶修莫隊、樹上莫隊、樹上帶修莫隊 的用法,在莫隊演算法總結&專題訓練3 中將會講述 回滾莫隊/不刪除莫隊、莫隊二次離線/第十四分塊(前體) 的思路以及實現。同時總結將會寫在 莫隊演算法總結&專題訓練3 當中。

3.練習題

題單:

普通練手題

這裡的題目都比較簡單,因此程式碼就少貼一點。只需要更改 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++;
}

P2709 小 B 的詢問

簡直模板。

當然這道題到目前為止沒有 \(\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]];
}

P1494 [國家集訓隊]小 Z 的襪子

這道題需要手推一下公式。

假設顏色為 \(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)}\)

所以我們只需要維護平方和就好。程式碼不貼了。

需要注意:

  1. 不要忘記約分。
  2. 道路千萬條,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]\) 呢?

這也就是樹上莫隊對詢問的處理:

  1. 首先假設詢問 \(x->y\) ,那麼算一下 \(lca(x,y)\) 。為了方便,我們規定 \(fir_x<fir_y\) ,不滿足就交換。
  2. 然後,如果 \(lca(x,y)=x\) 那麼使用 \([fir_x,fir_y]\) ,因為此時 \(x,y\) 在一條鏈上。否則,使用 \([las_x,fir_y]\) ,同時記錄 \(lca(x,y)\)

在處理詢問時要注意兩個點:

  1. \(x,y\) 在一條鏈上,不要處理 \(lca\) ,否則要處理 \(lca\)
  2. \(lca\) 需要處理兩次(因為不在區間內)

幾個坑點(重點):

  1. 尤拉序長度是 \(2n\) ,千萬不能在這裡 TLE 了!
  2. 塊長調 \((2n)^{\frac{2}{3}}\)
  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;
}

樹上帶修莫隊

樹上帶修莫隊,顧名思義,就是將 樹上莫隊 和 帶修莫隊 結合在一起的莫隊。

因此只要你掌握了 樹上莫隊 和 帶修莫隊 ,那麼樹上帶修莫隊簡直就是輕而易舉!

具體的思路如下:

  1. 首先按照樹上莫隊思路跑一遍尤拉序,處理操作。
  2. 處理操作時如果是詢問操作那麼按照樹上莫隊處理;同時根據帶修莫隊的處理方式,不要忘記處理修改和時間軸。
  3. 搞三個指標 \(l,r,t\) 處理即可。

P4074 [WC2013]糖果公園

思路與實現參見這篇文章->link

那麼到目前位置,我們已經講完了 普通莫隊、帶修莫隊、樹上莫隊、樹上帶修莫隊 四種莫隊,在 莫隊演算法總結&專題訓練3 中,將會講解最後兩種莫隊:回滾莫隊/不刪除莫隊,莫隊二次離線/第十四分塊(前體),同時將會總結長達三篇博文的莫隊講解。