当前位置: 首页 > news >正文

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
*/

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

http://www.wxhsa.cn/company.asp?id=4791

相关文章:

  • 西电微机原理-第一章 序论:微型计算机概述
  • Liunx 硬盘扩容
  • 船舶航向控制算法
  • pyside6 1
  • 基于WSL下载Hadoop和HBASE
  • 应用多、交付快,研发运维怎么管?看云效+SAE 如何一站式破局
  • revit二次开发之 钢筋功能详细分析
  • java-wxj02
  • 停止win10自动升级操作
  • vue3 - elementPlus
  • GAS_Aura-Target Data
  • windows 把恢复分区调整到 c 盘前面
  • wso2~对已发布api的元信息管理
  • 利用Myo臂环采集肌电信号和角速度来实现实时手势识别
  • 实用指南:leetcode 966. 元音拼写检查器 中等
  • 三轴传感开发新纪元:exvib扩展库让精准检测触手可及!
  • List与Dictionary区别
  • OpenStack Cinder 架构
  • 完整教程:IC(输入捕获)
  • HiMarket 正式开源,为企业落地开箱即用的 AI 开放平台
  • 如何统计DrawMeshInstancedIndirect绘制物体的Triangle数据
  • VK1S68C点钟LED驱动控制专用芯片高抗干扰数显驱动IC 可支持134的点阵LED显示面板
  • 基于MATLAB的海洋中尺度涡旋诊断
  • 从混乱到有序:Tita 项目一体化管理的全场景赋能
  • SpringBoot入门指南:让Java开发变得像搭积木一样简单 - 教程
  • 汇编语言[王爽]-13 int指令【中断实现loop、jmp】
  • Supabase云同步架构:Flutter应用的数据同步策略
  • 汇编语言[王爽]-12 内中断
  • 【SPIE出版】第五届先进制造技术与电子信息国际学术会议(AMTEI 2025)
  • 2025.9.15 考试总结