【学习笔记】Dsu On Tree

推荐!!cf上的教程

Dsu On Tree 是什么

针对树上问题的一种“优雅的暴力”,类似树上启发式合并。
基于轻重链剖分。

使用条件:

  1. 离线,无修改。
  2. 与子树有关,要遍历所有子树。

复杂度:(O(nlogn))


解决问题示例

Problem

(CF600E)
一棵 (n) 个节点的树,每个结点有一个颜色。
问,以每个节点为根的子树中,出现次数最多的颜色是哪种(若多种,输出种类编号之和)。

朴素的暴力

以每个点为根,遍历每棵子树统计答案。
复杂度 (O(n^2))

优雅的暴力

在朴素的暴力中,许多重复的信息没有重复利用。
比如,当遍历完一棵子树后,我们将它对答案的贡献清除了。但其实在遍历其父节点的子树时,这棵子树的信息是有用的。

在递归 (dfs) 中,可不可以将某些子树的答案保留,再将其它子树的答案暴力并上去,得到父节点的答案?
当然 (ok)
而那棵被保留的子树,贪心地想应是节点数最多的子树,即“重子”。

于是基本思路就出来了——
先做一遍轻重链剖分。然后在递归时先递归轻子,之后将答案清除;再递归重子,将答案保留;随后将轻点的答案暴力加上,统计本节点的答案。
这就是 (dsu) (on) (tree) !撒花!

复杂度证明

轻重链剖分有个性质:每个点到根的路径上最多只有 (logn) 条轻边。
证明:每走过一条轻边,子树大小一定小于之前的一半。把 (n) 减半,最多减 (logn) 次。

本算法中,只有轻边所连的子树会被暴力统计。
考虑每个点被暴力统计多少次,即其在多少个轻边所连子树中,其实就是它到根节点的轻边个数 ,即 (logn)
故,总复杂度 (nlogn)

代码

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

int read(){
	int x=0;
	char ch=getchar();
	while(!isdigit(ch)) ch=getchar();
	while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
	return x;
}

const int N = 100005;
typedef long long ll;

struct node{
	int v;
	node *nxt;
}pool[N*2],*h[N];
int cnt;
void addedge(int u,int v){
	node *p=&pool[++cnt],*q=&pool[++cnt];
	p->v=v;p->nxt=h[u];h[u]=p;
	q->v=u;q->nxt=h[v];h[v]=q;
}

int sz[N],son[N],fa[N];
void dfs1(int u){
	int v,Mson=0;
	sz[u]=1;
	for(node *p=h[u];p;p=p->nxt)
		if(!fa[v=p->v]){
			fa[v]=u;
			dfs1(v);
			sz[u]+=sz[v];
			if(sz[v]>Mson) Mson=sz[v],son[u]=v;
		}
}

int n,c[N],t[N],mx;
ll ans[N],num;

void Plus(int x,int y) { t[x]+=y; if(t[x]==mx) num+=x; if(t[x]>mx) mx=t[x],num=x; }
void add(int u,int x){
	int v;
	Plus(c[u],x);
	for(node *p=h[u];p;p=p->nxt)
		if(fa[v=p->v]==u) add(v,x);
}
void dfs(int u,int ty){
	int v;
	for(node *p=h[u];p;p=p->nxt)
		if(fa[v=p->v]==u && v!=son[u]) dfs(v,0);
	if(son[u]) dfs(son[u],1);
	for(node *p=h[u];p;p=p->nxt) 
		if(fa[v=p->v]==u && v!=son[u]) add(v,1);
	Plus(c[u],1);
	ans[u]=num;
	if(ty==0) add(u,-1),mx=0,num=0;
}

int main()
{
	n=read();
	for(int i=1;i<=n;i++) c[i]=read();
	for(int i=1;i<n;i++) addedge(read(),read());
	
	fa[1]=-1; dfs1(1);
	dfs(1,1);
	
	for(int i=1;i<=n;i++) printf("%lld ",ans[i]);
	
	return 0;
}

算法(伪)代码

//轻重链剖分
void dfs1(int u){
	int v,Mson=0;
	sz[u]=1;
	for(node *p=h[u];p;p=p->nxt)
		if(!fa[v=p->v]){
			fa[v]=u;
			dfs1(v);
			sz[u]+=sz[v];
			if(sz[v]>Mson) Mson=sz[v],son[u]=v;
		}
}
//dsu on tree
void Plus(int u,int x);
void add(int u,int x){ //暴力枚举轻子子树求贡献
        Plus(u,x);
        for(node *p=h[u];p;p=p->nxt) add(p->v,x);
}
void cal();
void work(int u,int ty){
        int v;
        for(node *p=h[u];p;p=p->nxt)
            if((v=p->v)!=son[u]) work(v,0);//递归处理轻子,处理后清理
        if(son[u]) work(son[u],1);//递归处理重子,之后不清理
        
        for(node *p=h[u];p;p=p->nxt)
            if((v=p->v)!=son[u]) add(v,1); //暴力加上轻子的答案
        Plus(u,1);//加上本节点的答案
        
        cal(); //统计

        if(ty==0) {  //若本节点为轻点,则需清除贡献
            add(u,-1); 
            与答案有关变量=0;//能全清为零,是由于轻点在重点之前递归,所有贡献均为该轻点内的贡献,而无需保留的重点的贡献。
        }
} 

其他例题及应用

(详见 (cf) 博客)
(cf570D) 也是个模板题
还可以解决一些点分治的问题。

既然选择了远方,便只顾风雨兼程
原文地址:https://www.cnblogs.com/lindalee/p/11815217.html