dsu on tree 学习笔记

0.前言
因为校内要做学习算法的微课的原因学习了dsu on tree
1.简介
dsu on tree 树上启发式合并,借鉴了启发式合并的思想,是一种优雅的暴力(划掉)
它一般用于解决具有以下特征的问题(摘自这个博客
1.只有对子树的询问(有的时候不明显,需要转换)
2.没有修改(这很重要)
它的优点:码量小,易理解,在有些时候可以爆踩某些算法
2.为什么 & 怎么做
暴力怎么做?遍历每个节点,暴力计算答案 !\(O(n^2)\) 爆炸
预处理 !空间吃不消
子树继承前面的答案 !有局限性,只有最后一颗子树的答案能被继承,前面的不行(因为会影响另外与其没有关系的子树)
等等。最后一颗子树(笑容逐渐猥琐),那我们肯定要最大限度利用它。
我们把最大的子树放到最后遍历,就可以保留下最多的信息
最大的子树?它的根节点还有一个名称:重儿子(其实就有点像树剖)
别的子节点,都叫轻儿子。
接下来讲怎么做
当我们遍历到一个节点\(u\)
1.遍历它的轻儿子,计算答案,但是计算答案的数组不能保留(什么意思呢,就是比方说,你用了桶统计某一个数出现的次数,遍历后把答案留下来,将桶清空)
2.遍历它的重儿子,保留所有内容
3.若这个节点本身就是轻儿子,清空所有内容,答案重置(其实对应了(1))
这里贴上模板题的代码CF600E Lomsat gelral

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <algorithm>
using namespace std;
#define int long long
const int N = 100005;
int ver[N<<1],head[N<<1],nxt[N<<1];
int siz[N],f[N],son[N],ans[N];
int cnt[N],c[N];
int n,m,tot,Max,sum,Son;
inline int read() {
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9') {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9') {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}
void add(int x,int y) {
	ver[++tot] = y;
	nxt[tot] = head[x];
	head[x] = tot;
}
void dfs1(int x,int fa) {
	int maxsize = 0;
	siz[x] = 1;f[x] = fa;
	for(int i= head[x];i;i = nxt[i]) {
		int y = ver[i];
		if(y == fa) continue;
		dfs1(y,x);
		siz[x] += siz[y];
		if(siz[y] > maxsize) son[x] = y,maxsize = siz[y];
	}
	return;
}
void change(int x,int opt) {
	cnt[c[x]] += opt;
	if(cnt[c[x]] > Max) Max = cnt[c[x]],sum = c[x];
	else if(cnt[c[x]] == Max) sum += c[x];
	for(int i = head[x];i;i = nxt[i]) {
		int y = ver[i];
		if(y == f[x]||y == Son) continue;
		change(y,opt);
	}
	return;
}
void dfs2(int x,int opt) {
	for(int i = head[x];i;i = nxt[i]) {
		int y = ver[i];
		if(y == f[x]||y == son[x]) continue;
		dfs2(y,0);
	}
	if(son[x]) dfs2(son[x],1),Son = son[x];
	change(x,1);
	ans[x] = sum;Son = 0;
	if(!opt) change(x,-1),sum = Max = 0;
}
signed main () {
	n = read();
	for(int i = 1;i <= n;i++) c[i] = read();
	for(int i = 1;i < n;i++) {
		int u,v;
		u = read();v = read();
		add(u,v);add(v,u);
	}
	dfs1(1,0);
	dfs2(1,0);
	for(int i = 1;i <= n;i++) printf("%lld ",ans[i]);
	return 0;
}

3.时间复杂度分析(以下内容借鉴了OI-wiki)
它看似是\(O(n^2)\)的,实际上是\(O(nlogn)\)
我们像树剖一样定义重边和轻边,重边指连接一个节点与其重儿子的边,轻边的定义相似
对于有\(n\)个节点的树,根节点到树上任意节点的轻边数不超过\(log n\),证明如下:
设根节点到任意一个节点(设为\(u\))所经过的轻边数为\(x\),以该节点为根的子树大小为\(y\)
显然一条轻边连接的儿子的子树大小不超过以其父亲为根的子树大小的一半,
\(y<n/{2^x}\),因为每经过一条轻边,子树大小至少除以\(2\)
又因为\(y>1\),所以\(n>2^x\),即\(x < logn\)
又因为如果一个节点是重儿子,那么在它的父亲节点遍历完其它轻儿子之前,它不会被计算。
所以一个节点被遍历的次数是它到根节点路径上轻边数\(+1\),因为之前每一次遍历轻儿子都会遍历一遍它,最后遍历他自己
所以总遍历次数为\(O(nlogn)\)(我们把\(+1\)省掉了)
4.例题
zyx大佬推荐的题单这里

posted @ 2020-12-17 23:03  ctt2006  阅读(67)  评论(0)    收藏  举报