1. 程式人生 > 實用技巧 >dsu on tree 學習筆記

dsu on tree 學習筆記

樹上啟發式合併(dsu on tree)通常用來查詢不帶修的子樹資訊,資訊要求可合併。

對於一個結點 \(u\),其步驟如下:

  1. 求解其輕兒子的答案,同時清除遞迴產生的影響。

  2. 求解其重兒子 \(v\) 的答案,不清除遞迴產生的影響。

  3. 將以 \(u\) 為根的子樹內除了以 \(v\) 為根的子樹的每個結點都合併進答案中,成為以 \(u\) 為根的子樹產生的影響。

來分析一下演算法的時間複雜度,發現每個結點只會在根到該結點路徑上的每條輕邊處被合併一次答案,這樣的輕邊最多隻有 \(\log n\) 條。所以如果合併一次資訊的時間複雜度算 \(O(1)\),dsu on tree 的總時間複雜度就是 \(O(n\log n)\)

的。

來看一道板子題(CF600E)。

合併資訊用一個桶來實現即可。

code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 100005
#define For(i,x,y)for(i=x;i<=(y);i++)
struct node
{
	int next,to;
}e[200005];
ll ans[N],sum;
int head[N],siz[N],c[N],b[N],hea[N],g,mx;
int read()
{
	int A;
	bool K;
	char C;
	C=A=K=0;
	while(C<'0'||C>'9')K|=C=='-',C=getchar();
	while(C>'/'&&C<':')A=(A<<3)+(A<<1)+(C^48),C=getchar();
	return(K?-A:A);
}
inline void add(int u,int v)
{
	e[++g].to=v;
	e[g].next=head[u];
	head[u]=g;
}
void dfs(int u,int fa)
{
	int i,v;
	siz[u]=1;
	for(i=head[u];i;i=e[i].next)
	{
		v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[hea[u]])hea[u]=v;
	}
}
//預處理重兒子 
void insert(int u,int fa,int son)
{
	int i,v;
	b[c[u]]++;
	if(b[c[u]]>mx)mx=b[c[u]],sum=c[u];
	else if(b[c[u]]==mx)sum+=c[u];
	for(i=head[u];i;i=e[i].next)
	{
		v=e[i].to;
		if(v!=fa&&v!=son)insert(v,u,0);
	}
}
void Delete(int u,int fa)
{
	int i,v;
	b[c[u]]=0;
	for(i=head[u];i;i=e[i].next)
	{
		v=e[i].to;
		if(v!=fa)Delete(v,u);
	}
}
void work(int u,int fa)
{
	int i,v;
	for(i=head[u];i;i=e[i].next)
	{
		v=e[i].to;
		if(v==fa||v==hea[u])continue;
		work(v,u);
		Delete(v,u);
		sum=mx=0;
		//清除影響 
	}
	if(hea[u])work(hea[u],u);
	//不清除影響 
	insert(u,fa,hea[u]);
	//統計答案 
	ans[u]=sum;
}
int main()
{
	int n,i,x,y;
	n=read();
	For(i,1,n)c[i]=read();
	For(i,1,n-1)
	{
		x=read(),y=read();
		add(x,y),add(y,x);
	}
	dfs(1,0);
	work(1,0);
	For(i,1,n)printf("%lld ",ans[i]);
	return 0;
}