Dsu On Tree 笔记

关于这个技巧我甚至都记不清是什么时候学的了,反正就是很早很早之前,当时学了之后看什么子树查询都想上 Dsu On Tree,后来也没怎么写过了,不过这个东西确确实实很强劲。

今天写了一上午教练的题单,大概获得了三天的时间来写自己想写的,就去写写各种莫队吧。

结果写到一个树上莫队,突然想使用这个东西,于是想着顺便写一些这个东西的文章。

由于忘了 CF 账号,所以好多写过的题都忘了是啥了,所以接下来是入门向的,目的防止我自己忘掉。

引入

对于一些树上的问题,每次查询子树,并且这些信息难以合并,需要开个桶或者使用 DS 进行维护,我们可以尝试这个东西。

如果 \(O(1)\) 一次查询或删改,这个东西的复杂度是 \(O(nlogn)\) 的。

这个东西在一定程度上可以替代点分治,注意我指的是一定程度上。

我们的整个算法是有一个特定的流程的,所以写起来是异常的简单。

就按照 OIWIKI 上的例子来引入吧。

给你一棵数,有一些询问,问你一个子树中的颜色数量是多少。

很好理解的题意。

我们发现判断颜色的数量,必须一定程度上去维护每一个颜色的信息,显然这个东西难以进行有效的合并,所以容易想到开桶维护每一个颜色的出现个数,暴力扫描每一个子树,明显这个算法是 \(O(n^2)\) 级别的。

这个桶就是数组维护每个颜色的出现次数,怕我的语言过分抽象。

先不谈我们怎么减少这类做法的时间复杂度,我们先思考有什么办法来减轻我们的工作量。

我们可以尝试这么一个方案:假设我们在处理一棵子树的时候,我们肯定是要 dfs 到下边的子树的,所以我们可以在桶里边保留一些东西的贡献,这个样子我们似乎就可以稍微优化一些了。

我们来讨论一下如何进行合法的保留,仔细思索后我们会发现能保留的东西是很苛刻的。

我们是无法保留两个子树的,所以贪心来想,我们去选择重儿子所在的子树进行保留就行。

这个重儿子就是我们树链剖分的那个重儿子,别告诉我有人连树剖都不会。

这样我们就成功进行了优化,现在这个东西大概就是 \(O(nlogn)\) 级别的了。

什么?这就完了?凭什么?我不信。

我们冷静分析一波,来思考一下每个节点会被扫到多少次?

容易发现,在某个节点到根中路径中,我们除去它本身会被遍历的这一次,这个路径中每出现一次轻边,这个点就会被扫一次,又因为这个数目不会超过 \(log n\) 级别的,所以 \(O(nlogn)\) 的时间复杂度也并不难理解了。

引入的解答

我们来考虑一下如何解决上边的引入。

给一下链接:https://www.luogu.com.cn/problem/U41492

按照上边的思路,我们写一下最经典的树剖,读入,这个我们不多说了,结下来我们讲一下 dsu on tree 的基本流程。

  1. 不保留对桶中的贡献,尝试去解决轻儿子所在子树

  2. 保留对桶中的贡献,解决重儿子所在子树

  3. 再一次遍历轻儿子所在子树,把这一次的东西加入桶中,计算答案。

int siz[MN], son[MN], dfn_cnt, l[MN], r[MN], col[MN], tmp[MN];
void dfs1(int u, int father){
	siz[u]=1; l[u]=++dfn_cnt; col[dfn_cnt]=tmp[u];
	for(int i=head[u];i;i=node[i].nxt){
		int v=node[i].to;
		if(v==father) continue;
		dfs1(v,u); siz[u]+=siz[v];
		if(siz[son[u]]<siz[v]) son[u]=v;
	}
	r[u]=dfn_cnt;
}
int cnt[MN], res, ans[MN];
vector <int> querys[MN];
void Add(int col){
	cnt[col]++;
	if(cnt[col]==1) res++;
}
void Del(int col){
	cnt[col]--;
	if(cnt[col]==0) res--;
}
int Getans(){
	return res;
}
void solve(int u, int father, bool keep){
	for(int i=head[u];i;i=node[i].nxt){
		int v=node[i].to;
		if(v==father||v==son[u]) continue;
		solve(v,u,false);
	}
	if(son[u]) solve(son[u],u,true);
	for(int i=head[u];i;i=node[i].nxt){
		int v=node[i].to;
		if(v==father||v==son[u]) continue;
		for(int j=l[v]; j<=r[v]; ++j) Add(col[j]);
	}
	Add(tmp[u]);
	for(auto qry:querys[u]) ans[qry]=Getans();
	if(!keep){
		for(int j=l[u]; j<=r[u]; ++j) Del(col[j]);
	}
}
int n, m;
int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n;
	for(int i=1,u,v; i<n; ++i){
		cin>>u>>v; insert(u,v); insert(v,u);
	}
	for(int i=1; i<=n; ++i) cin>>tmp[i];
	cin>>m;
	for(int i=1; i<=m; ++i){
		int v; cin>>v; querys[v].push_back(i);
	}
	dfs1(1,1); solve(1,1,0);
	for(int i=1; i<=m; ++i) cout<<ans[i]<<'\n';
	return 0;
}

解释一下我的代码可能令人不解的点。

我们会发现我们需要不同的遍历,为了更方便扫描子树中的东西,我们使用 dfs 序来更简洁处理问题。

这下明白 l, r 是什么意思了吧。

这个实现千奇百怪,但是并没有什么难度,所以就不多说了。

某些例题。

P9233 [蓝桥杯 2023 省 A] 颜色平衡树

题意

给你一颗节点数为 \(n\) 的树,每个点有一个颜色,询问有多少节点满足它子树中各个颜色节点数相等。

我们还需要 \(O(n\log n)\) 左右的算法。

分析

询问子树上的统计问题,我们可以使用树上启发式合并,这个题还是很套路的。

如何快速判断各个颜色数相等?我们记录一下总共出现的颜色数和每个节点数的颜色数量就行,这两个在树上启发式合并过程中维护就行,很简单,最后询问的时候我们利用所询问节点的颜色判断即可。

算是板子,具体实现看一下代码便懂了。

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MN=5e5+515;
struct Node{
    int nxt, to;
}node[MN];
int head[MN], tottt;
inline void insert(int u, int v){
    node[++tottt].to=v;
    node[tottt].nxt=head[u];
    head[u]=tottt;
    return;
}
int cntid[MN];//统计节点数量为i的颜色个数
int col[MN], tmp[MN];//存颜色的
int cntcol[MN], totcol;//统计一共有多少种颜色
void Add(int c){
    if(cntcol[c]==0) totcol++;
    cntcol[c]++;
    cntid[cntcol[c]-1]--;
    cntid[cntcol[c]]++;
}
void Del(int c){
    if(cntcol[c]==1) totcol--;
    cntcol[c]--;
    cntid[cntcol[c]+1]--;
    cntid[cntcol[c]]++;    
}
int Getsub(int u){
    int c=tmp[u];
    if(totcol==cntid[cntcol[c]]) return true;
    return false;
}
int depth[MN], fa[MN], siz[MN], son[MN], l[MN], r[MN];
int dfn_cnt;
void dfs1(int u, int father){
    fa[u]=father; depth[father]=depth[u]-1; siz[u]=1;
    l[u]=++dfn_cnt; col[dfn_cnt]=tmp[u];
    for(int i=head[u];i;i=node[i].nxt){
        int v=node[i].to;
        if(v==father) continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        if(siz[son[u]]<siz[v]) son[u]=v;
    }
    r[u]=dfn_cnt;
}
int ans=0;
void dfs(int u, int father, bool keep){
    for(int i=head[u];i;i=node[i].nxt){
        int v=node[i].to;
        if(v==father||v==son[u]) continue;
        dfs(v,u,false);
    }
    if(son[u]) dfs(son[u],u,true);
    for(int i=head[u];i;i=node[i].nxt){
        int v=node[i].to;
        if(v==son[u]||v==father) continue;
        for(int j=l[v]; j<=r[v]; ++j){
            Add(col[j]);
        }
    }
    Add(tmp[u]);
    ans+=Getsub(u);
    if(!keep){
        for(int i=l[u]; i<=r[u]; ++i) Del(col[i]);
    }
}
int n;
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>n;
    for(int i=1,c,father; i<=n; ++i){
        cin>>c>>father; tmp[i]=c;
        if(father) insert(i,father),insert(father,i);
    }
    dfs1(1,1);
    dfs(1,1,false);
    cout<<ans<<'\n';
    return 0;
}

CF375D Tree and Queries

这个也是同样的简单,一个道理,我们使用 BIT 快速搞就行。

#include <bits/stdc++.h>
using namespace std;
const int MN=1e6+116;
struct Node{
	int nxt, to;
}node[MN];
int head[MN], tottt;
inline void insert(int u, int v){
	node[++tottt].to=v;
	node[tottt].nxt=head[u];
	head[u]=tottt; return;
}
int siz[MN], son[MN], col[MN];
int dfn_cnt, l[MN], r[MN], tmp[MN];
void dfs1(int u, int father){
	siz[u]=1; l[u]=++dfn_cnt; col[dfn_cnt]=tmp[u];
	for(int i=head[u]; i; i=node[i].nxt){
		int v=node[i].to;
		if(v==father) continue;
		dfs1(v,u); siz[u]+=siz[v];
		if(siz[son[u]]<siz[v]) son[u]=v;
	}
	r[u]=dfn_cnt;
}
int n, m, k, res=0;
int tr[MN], cnt[MN];
int lowbit(int x){
	return x&(-x);
}
void update(int pos, int val){
	for(int i=pos; i<MN; i+=lowbit(i)) tr[i]+=val; 
}
int qval(int pos){
	int res=0;
	for(int i=pos; i; i-=lowbit(i)) res+=tr[i];
	return res;
}
void Add(int col){
	if(cnt[col]) update(cnt[col],-1);
	cnt[col]++;
	if(cnt[col]==1) res++;
	update(cnt[col],1);
}
void Del(int col){
	update(cnt[col],-1);
	cnt[col]--;
	if(cnt[col]==0) res--;
	if(cnt[col]) update(cnt[col],1);
}
int Getans(int k){
	return qval(k-1);
}
struct Qrys{
	int id, k;
};
vector <Qrys> querys[MN];
int ans[MN];
void solve(int u, int father, int keep){
	for(int i=head[u];i;i=node[i].nxt){
		int v=node[i].to;
		if(v==father||v==son[u]) continue;
		solve(v,u,false);
	}
	if(son[u]) solve(son[u],u,true);
	for(int i=head[u];i;i=node[i].nxt){
		int v=node[i].to;
		if(v==father||v==son[u]) continue;
		for(int j=l[v]; j<=r[v]; ++j) Add(col[j]);
	}
	Add(tmp[u]);
	for(auto qry:querys[u]){
		ans[qry.id]=res-Getans(qry.k);
		//cout<<Getans(qry.k)<<'\n';
	}
	if(!keep){
		for(int j=l[u]; j<=r[u]; ++j) Del(col[j]);
	}
}
int main(){
	memset(cnt,0,sizeof(cnt));
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n>>m; for(int i=1; i<=n; ++i) cin>>tmp[i];
	for(int i=1,u,v; i<n; ++i){
		cin>>u>>v; insert(u,v); insert(v,u);
	}
	dfs1(1,1);
	for(int i=1; i<=m; ++i){
		int u, k; cin>>u>>k;
		querys[u].push_back({i,k});
	}
	solve(1,1,0);
	for(int i=1; i<=m; ++i) cout<<ans[i]<<'\n';
	return 0;
}
/*
10 2
75 72 81 90 62 39 32 88 61 58
2 1
3 2
4 3
5 1
6 4
7 1
8 4
9 6
10 8
2 99
2 68
*/

不太想写了,这些题目的具体就是一些实现问题罢了,所以我就懒得写太多了

posted @ 2025-09-15 16:42  BaiBaiShaFeng  阅读(32)  评论(0)    收藏  举报
Sakana Widget右下角定位