淺談虛樹


前言

先貼一道模板題https://www.luogu.org/problemnew/show/P2495
題意,給你一棵n個點的有邊權樹,有m次詢問,每次詢問k個點,要刪除一些邊使得這k個點均不與1號點聯通。
數據范圍:2<=n<=250000,m>=1,sigma(ki)<=500000,1<=ki<=n-1;
考慮樹形dp

LL get_ans(int u){
	bool leaf=1;
	LL ret=0;
	for (int p=head[u];p;p=nxt[p]){
		ret+=get_ans(a[p]);
		leaf=0;
	}
	head[u]=0;
	if (del[u]){
		del[u]=0;
		return ff[u];
	}
	return min(ret,1ll*ff[u]);
}

ff表示我連向我父親的邊的邊權。
我是直接暴力dfs一遍,如果我這個點要刪除,那么一定是刪我的ff邊最優。
否則選擇刪我ff邊或者一個一個刪我的子節點

這樣dp一遍是O(n)的,但是m次詢問就T了,但是注意到sigma(k)並不大,於是虛樹閃亮登場了

介紹

虛樹就是把原樹中少量的有效節點和他們兩兩的lca拿出來,這樣就可以去除一些無效節點,從而降低復雜度。
如果有效節點是k個,那么虛樹中節點的個數是2*k個,為什么,請看下文。

實現

先講講如何建虛樹,在本題中,虛樹上的邊權就是原先這條路徑上邊權的min,因為你要刪肯定是刪最小邊最優。
先dfs一遍,求出基本信息。

void dfs(int u){
	dfn[u]=++tim;
	for (int p=head[u];p;p=nxt[p]){
		int v=a[p];
		if (v!=f[u][0]){
			f[v][0]=u;
			minn[v][0]=b[p];
			dep[v]=dep[u]+1;
			dfs(v); 
		}
	}
}

先倍增求lca預處理好,倍增的時候最小值也處理好

for (int j=1;j<=20;j++){
		for (int i=1;i<=n;i++) f[i][j]=f[f[i][j-1]][j-1],
		minn[i][j]=min(minn[i][j-1],minn[f[i][j-1]][j-1]);
	}
int lca(int x,int y){
	if (dep[x]<dep[y]) swap(x,y);
	int tmp=dep[x]-dep[y];
	for (int i=0;i<=20;i++){
		if (tmp&(1<<i)) x=f[x][i];
	}
	if (x==y) return x;
	for (int i=20;i>=0;i--){
		if (f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
int dist(int x,int y){
	if (dep[x]<dep[y]) swap(x,y);
	int tmp=dep[x]-dep[y],ret=1e9;
	for (int i=0;i<=20;i++){
		if (tmp&(1<<i)) ret=min(ret,minn[x][i]),x=f[x][i];
	}
	return ret;
}

然后把所有有效點按照dfn值排序,每次新加入一個節點,他最多會和前面的一個節點產生一個lca。
簡單證明一下,
設當前加入的節點x,與y節點產生了一個新的lca,lca1。
假設x還與z產生了一個新的lca,lca2,不妨假設dep[lca2]>dep[lca1] (不然交換y,z即可)
那么z,y的lca必定為lca1,所以假設不成立
這樣我們就證明了虛樹中節點個數是min(n,2*k)個的。

構建虛樹

維護一個棧,表示從根到棧頂元素的這條鏈
我們新加入一個節點記為x,鏈的末端,即棧頂,為p,lca為lca(x,p),
有兩種情況:
  1.p和x分立在lca的兩棵子樹下.
  2.lca是p.
  為什么lca不能是x?
   因為如果lca是x,說明dfn[lca]=dfn[x]$ <$ dfn[p],而我們是按照dfs序號遍歷的,於是dfn[p]$ <$ dfn[x],矛盾.)
對於第二種情況,直接在棧中插入節點x即可,不要連接任何邊(后面會說為什么).
對於第一種情況,要仔細分析.
我們是按照dfn遍歷的(因為很重要所以多說幾遍......),有dfn[x]>dfn[p]>dfn[lca].
這說明什么呢? 說明一件很重要的事:我們已經把lca所引領的子樹中,p所在的子樹全部遍歷完了!
  簡略的證明:如果沒有遍歷完,那么肯定有一個未加入的點h,滿足dfn[h]$ <$ dfn[x],
        我們按照dfs序號遞增順序遍歷的話,應該把h加進來了才能考慮x.
這樣,我們就直接構建lca引領的,p所在的那個子樹. 我們在退棧的時候構建子樹.
p所在的子樹如果還有其它部分,它一定在之前就構建好了(所有退棧的點都已經被正確地連入樹中了),就剩那條鏈.
如何正確地把p到lca那部分連進去呢?
設棧頂的節點為p,棧頂第二個節點為q.
重復以下操作:
  如果dfn[q]>dfn[lca],可以直接連邊q->p,然后退一次棧.
  如果dfn[q]=dfn[lca],說明q=lca,直接連邊lca->p,此時子樹已經構建完畢.
  如果dfn[q]$ <$ dfn[lca],說明lca被p與q夾在中間,此時連邊lca->p,退一次棧,再把lca壓入棧.此時子樹構建完畢
最后,為了維護dfs鏈,要把x壓入棧. 整個過程就是這樣

上面這個討論的過程來自chenhuan001的博客,把一些有小錯誤的地方改正了,我就是看着這個學會的,講的非常清楚。
還不明白的可以結合代碼

void insert(int x){
	if (!top){
		st[++top]=x;
		return;
	}
	int ll=lca(st[top],x);
	while (dep[st[top-1]]>dep[ll]&&top>1){
		add(st[top-1],st[top],dist(st[top-1],st[top]));
		top--;
	}
	if (dep[ll]<dep[st[top]]){
		add(ll,st[top],dist(ll,st[top]));
		top--; 
	}
	if (!top||dep[st[top]]<dep[ll]) st[++top]=ll;
	st[++top]=x;
}

那我們把虛樹建出來后,用最前面講的dp跑一邊就好了。
好了,
以上就是我個人對虛樹的一些理解,希望可以幫助大家學習,如果還有疑問可以給我留言,或者到我好友的博客看更詳細的代碼和建樹過程的描述。
https://blog.csdn.net/zhouyuheng2003/article/details/79110326
謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM