Note - 樹分治(點分治、點分樹)

子洢發表於2024-08-16

陳年筆記,現在可能不會了。

點分治

Q1:基本思想是什麼?

將路徑分為經過 \(u\) 和不經過 \(u\) 的兩類,在每次分治中計算經過 \(u\) 的路徑數量。

Q2:如何統計?

  1. 一般:遍歷 \(u\) 的每個子節點 \(v\),把 \(v\) 子樹內的節點記錄下來,得到答案並更新陣列。
  2. 容斥:把 \(u\) 子樹內的節點都記錄下來排序,雙指標得到的 \(u\) 子樹內點對數量,減去每個子節點 \(v\) 子樹內的點對數量,即為經過 \(u\) 的路徑的答案。*

Q3:如何保證複雜度?

每次 \(O(n)\) 尋找重心,子樹大小降為最大 \(\lfloor size/2 \rfloor\),大概 \(\log(n)\) 層。每層遍歷大概 \(n\) 個節點,複雜度上限 \(O(n\log(n))\)

Q4:用途是什麼?

統計樹上滿足條件的路徑(點對)個數(或點權和等),一般條件和距離相關。

void findrt(int u,int f){
	siz[u]=1;
	maxp[u]=0;
	for(int i=fir[u];i;i=nex[i]){
		int v=to[i];
		if(vis[v]||v==f) continue;
		findrt(v,u);
		siz[u]+=siz[v];
		maxp[u]=max(maxp[u],siz[v]);
	}
	maxp[u]=max(maxp[u],sum-siz[u]);
	if(maxp[u]<maxp[rt]) rt=u;
}
void dfz(int u){
	vis[u]=1;
	
	//計算!!!
	
	for(int i=fir[u];i;i=nex[i]){
		int v=to[i];
		if(vis[v]) continue;
		rt=0;
		sum=siz[v];
		findrt(v,u);
		dfz(rt);
	}
}

邊分治

其實和點分治差不多。

點分樹

Q1:基本思想是什麼?

透過點分治的方法遞迴,每次的得到 \(v\) 子樹內的重心與 \(u\) 相連,建出重構樹。

Q2:特殊性質?

  1. 最多 \(log(n)\) 層。
  2. 重構樹上的 LCA(x,y) 必定在原樹 x 到 y 的路徑上,有 \(dis_{x,y}=dis_{x,LCA(x,y)}+dis_{y,LCA(x,y)}\)。*

Q3:如何運用特殊性質?

點分樹又被成為動態點分治。
點分樹和點分治一樣適用於統計滿足條件的路徑的點權和,但點分樹可修改點權,更加靈活,保證了每次只需查詢或修改 \(log(n)\) 層。

Q4:具體實現?

  1. 得到重構樹,其實只需記錄 \(fa_u\)
  2. 一個顯然的思路是在重構樹中將 \(now\) 不斷變為父親節點,得到在 \(fa_{now}\) 子樹內但不在 \(now\) 子樹內的貢獻。
  3. 想想如何更新。我們用 \(f_i\)\(i\) 子樹中 \(i\) 的資料,\(g_i\)\(i\) 子樹內 \(fa_i\) 的資料。

示例:P6329 【模板】點分樹

void modify(int x,int k){
	int now=x;
	while(now){
		F.modify(now,getdis(now,x),k);
		if(Fa[now]) G.modify(now,getdis(Fa[now],x),k);
		now=Fa[now];
	}
}
int query(int x,int k){
	int now=x,pre=0,ret=0;
	while(now){
		int t=getdis(now,x);
		if(t>k){
			pre=now,now=Fa[now];
			continue;
		}
		ret+=F.ask(now,k-t);
		if(pre) ret-=G.ask(pre,k-t);
		pre=now,now=Fa[now]; 
	}
	return ret;
}

相關文章