树上启发式合并(dsu on tree)精巧的暴力
树上启发式合并(dsu on tree)
虽然叫dsu但这和并查集貌似没什么关系
例:
给你一棵树,每个节点有一个颜色,要求出每个子树中数量最多的颜色并输出
(数量相同的情况先不考虑
不重要)
当我们需要在每个子树上统计一些信息的时候,往往会开一个全局的cnt数组,试图 dfs \(O(n)\) 扫一遍,一边加点一边得到答案
但对于一棵树而言显然有问题:当我们统计完其左子树的信息后,必须清空整个cnt数组才能去扫右子树,这样其实就已经变成 \(O(n^2)\) 了
当然我们可以稍微偷工减料一点,因为最后一棵子树统计完后不用清空,我们可以最后遍历最大的那棵子树
最大子树可以通过一遍dfs预处理出子树的size,记录每个点的重儿子得到(类似树剖)
然而就是这一点偷工减料,使得整个算法复杂度直接降至 \(O(nlogn)\)
如果不关心证明的话,你已经学会 dsu on tree 了
证明为什么这样瞎搞就能获得\(nlogn\)的复杂度:
以下通过感性理解的方式说明为什么这东西能优化这么多
回顾一下在每个节点处我们要做什么:
- dfs轻儿子,并消除影响
- dfs重儿子,不消除影响
- 再统计轻子树的影响
前两步的操作一共是 \(O(n)\) 的,就是最朴素的从头到尾扫一遍
现在需要考虑:在每个点处对每个轻子树扫一遍的复杂度
如果一个点和根节点之间一共有 x 条轻边,那么它会被遍历差不多 x+1 次
而轻重链剖分有个很好的性质:走一条轻边时,节点数量至少被砍一半,否则这就不是轻边了
那么从根节点到任意节点经过的轻边数量最多是 \(logn\) 级别的
所以其实很显然了:复杂度就是 \(O(nlogn)\)
再看看极端情况加深理解:
树上问题最容易被出题人的各种链,菊花图,链加菊花图啥的卡掉
如果这棵树长得像链,它将被最后走最大子树这一小贪心优化掉一大半;
如果这棵树长得像菊花图,,那么根节点到任意节点间的轻边数量都将是极少的;
所以你可以相信dsu on tree
代码(这道题的)
int n;
int col[maxn];
int cnt[maxn];
ll ans[maxn];
int siz[maxn], son[maxn];
struct Edge{
int t, nt;
}e[maxn*2];
int hd[maxn], ecnt = 0;
inline void add(int x, int y){
e[++ecnt].t = y;
e[ecnt].nt = hd[x];
hd[x] = ecnt;
}
void dfs1(int p, int fa){
siz[p] = 1;
son[p] = 0;
for(int i=hd[p];i;i=e[i].nt){
int v = e[i].t;
if(v!=fa){
dfs1(v, p);
siz[p] += siz[v];
if(siz[v] > siz[son[p]]) son[p] = v;
}
}
}
ll tot = 0, mxc = 0;
void addcol(int c, int ad){//只计加不计减(减肯定减到0)
cnt[c] += ad;
if(cnt[c] > mxc){
mxc = cnt[c];
tot = c;
}else if(cnt[c] == mxc){
tot += c;
}
}
void cntall(int p, int fa, int d){
for(int i=hd[p];i;i=e[i].nt){
int v = e[i].t;
if(v!=fa){
cntall(v, p, d);
}
}
addcol(col[p], d);
}
void dfs(int p, int fa, int sav){
for(int i=hd[p];i;i=e[i].nt){
int v = e[i].t;
if(v!=fa && v!=son[p]){
dfs(v, p, 0);
}
}
if(son[p]) dfs(son[p], p, 1);
for(int i=hd[p];i;i=e[i].nt){
int v = e[i].t;
if(v!=fa && v!=son[p]){
cntall(v, p, 1);
}
}
//此时所有子节点均已记录
addcol(col[p], 1);
ans[p] = tot;
if(!sav) cntall(p, fa, -1), tot = mxc = 0;
}
void solve(){
cin >> n;
for(int i=1;i<=n;i++) cin >> col[i];
for(int i=1;i<n;i++){
int x, y;
cin >> x >> y;
add(x, y); add(y, x);
}
dfs1(1, -1);
dfs(1, -1, 1);
for(int i=1;i<n;i++) cout << ans[i] << ' ';
cout << ans[n] << '\n';
}
原文地址:https://www.cnblogs.com/tyin/p/15131817.html
- JDK1.7新特性(1):Switch和数字
- 房上的猫:二维数组
- JDK1.7新特性(2):异常和可变长参数处理
- 房上的猫:数组插入算法等难点专开
- Netty(1):第一个netty程序
- 数据迁移部分问题总结(r2第3天)
- 房上的猫:数组
- 【答疑解惑】如何避免程序崩溃之一
- 房上的猫:for循环,跳转语句与循环结构,跳转语句进阶
- 房上的猫:while循环与do-while循环,debug的调试运用
- java调用python代码
- 房上的猫:switch选择结构,与选择结构总结
- java调用ruby代码
- 使用ObjectOutputStream进行socket通信的时候出现固定读到四个字节乱码的问题
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- C语言入门系列之1.C语言概述和上机运行简单C程序
- 开发微信小程序,我为什么放弃 setData,使用 upData
- C语言入门系列之3.顺序程序设计和输入输出
- uniapp提交选中的性别的value值
- LeetCode 1422. 分割字符串的最大得分
- LeetCode 64. 最小路径和
- C语言入门系列之4.分支结构程序-关系、逻辑运算和if、switch语句
- LeetCode 62. 不同路径
- 在裸机上部署Pulsar集群 顶
- JNI回调Java
- 你的函数有多快?使用 performance 监控前端性能
- spring security 密码过期强制修改密码
- Spring Boot入门系列(十五) SpringBoot开发环境热部署的配置
- idea启用springboot项目热部署
- 【LeetCode每日一题】22. Generate Parentheses