dsu on tree学习笔记
前言
一次模拟赛的\(T3\):传送门
只会\(O(n^2)\)的我就\(gg\)了,并且对于题解提供的\(\text{dsu on tree}\)的做法一脸懵逼。
看网上的其他大佬写的笔记,我自己画图看了一天才看懂(我太蒻了),于是就有了这篇学习笔记。
概念篇/基础运用
算法简介
现在考虑这样一类树上统计问题:
无修改操作,询问允许离线
对子树信息进行统计(链上的信息在某些条件下也可以统计)
树上莫队?点分治?
\(\text{dsu on tree}\)可以把它们吊起来打!
\(\text{dsu on tree}\)运用树剖中的轻重链剖分,将轻边子树信息累加到重链上进行统计,拥有\(O(nlogn)\)的优秀复杂度,常数还贼TM小,你值得拥有!
//虽说是dsu on tree,但某个毒瘤@noip说这是静态链分治
//还有其他的数据结构神du仙liu说它可以被看成是静态的树剖(因为其在树上有强大的统计信息的能力,但不能支持修改操作),与正常的树链剖分相对
//所以我同时保留这几种说法,希望数据结构神du仙liu们不要喷我这个juruo
算法实现
遍历所有轻儿子,递归结束时消除它们的贡献
遍历所有重儿子,保留它的贡献
再计算当前子树中所有轻子树的贡献
更新答案
如果当前点是轻儿子,消除当前子树的贡献
那么这里有人可能就要问了,为什么不保留求出的所有答案呢?这样复杂度就更优了啊
如果这样的话,当你处理完一颗子树的信息时,再递归去求解另一颗子树时,
已有的答案就会与当前子树信息相混淆,就会产生错误答案。
所以,从这我们看出,一个节点只能选择一个子节点来保留答案
其它的都要去暴力求解
那么选择哪一个节点能使复杂度最优呢?
显然,我们要尽量均衡答案被保留的子树和不被保存的子树的大小
这是不是就很像树链剖分划分轻重儿子了呢?
人工图解
因为窝太蒻了一开始没怎么理解它,所以有了图解这个环节23333。
- 比如现在有一个已经剖好的树(粗边为重边,带红点的是重儿子):
- 首先,我们先一直跳轻儿子跳到这个位置:
- 记录它的答案,并撤销影响,一直往轻儿子上跳
- 然后发现下一步只能跳到一个重儿子上,就记录他的答案并保存(下文图中被染色的点即为目前保存了答案的点)
- 接着回溯到父节点上,往下计算答案
- 因为重儿子保存了答案被标记,往下暴力计算的时候只会经过轻边及轻儿子(即\(6 \rightarrow 12\)这条边和\(12\)号节点)
同理,\(2\)号点也可进行类似操作,因为它的重儿子\(6\)号节点已保存了这颗子树的答案,只需上传即可,
不用再从\(6\)这个位置再往下走统计答案,唯一会暴力统计答案的只有它的轻儿子\(5\)号节点
- 然后继续处理根节点另一个轻儿子\(3\),一直到叶子节点收集信息
- 最后,对根节点的重儿子进行统计,如图,先对箭头所指的两个轻儿子进行计算
- 接着对每一个重儿子不断保存答案,对轻儿子则暴力统计信息,将答案不断上传
- 然后,对于根节点的处理同上即可
大致代码:
inline void calc(int x,int fa,int val)
{
......................
/*
针对不同的问题
采取各种操作
*/
for(rg int i=0;i<(int)G[x].size();++i)
{
int v=G[x][i];
if(vis[v] || v==fa) continue;
calc(v,x,val);
}
}
inline void dfs(int x,int fa,int keep)//keep表示当前是否为重儿子
{
for(int i=0;i<(int)G[x].size();++i)
{
int v=G[x][i].v;
if(v==fa || v==son[x]) continue;
dfs(v,x,0);
}
if(son[x]) dfs(son[x],x,1),vis[son[x]]=true;//标记重儿子
calc(x,fa,1);vis[son[x]]=false;//计算贡献
ans[x]=....;//记录答案
if(!keep) calc(x,fa,-1);//不是重儿子,撤销其影响
}
如果是维护路径上的信息,大概还可以这么写:(如果有错,请大佬指出)
ps:关于\(\text{dsu on tree}\)对路径上信息进行维护的精彩应用,可以看最后\(3\)道例题
inline void dfs(int x,int fa)
{
siz[x]=1,dep[x]=dep[fa]+1,nid[rev[x]=++idx]=x;
//再次借助树剖的思想,子树内节点顺序转为线性
for(rg int i=0;i<(int)G[x].size();++i)
{
int v=G[x][i].v,w=G[x][i].w;
if(v==fa) continue;
dfs(v,x),siz[x]+=siz[v];
if(!son[x] || siz[v]>siz[son[x]]) son[x]=v;
}
}
inline void calc(int x,int val)
{//对x这一节点进行单独处理
if(val>0) //计算贡献
else //撤销影响
}
inline void dfs2(int x,int fa,int keep)
{
for(rg int i=0;i<(int)G[x].size();++i)
{
int v=G[x][i].v;
if(v==fa || v==son[x]) continue;
dfs2(v,x,0);
}
if(son[x]) dfs2(son[x],x,1);
for(rg int i=0;i<(int)G[x].size();++i)
{
int v=G[x][i].v;
if(v==fa || v==son[x]) continue;
for(rg int j=0;j<siz[v];++j)
{
int vv=nid[rev[v]+j];
..........
//更新答案
}
for(rg int j=0;j<siz[v];++j) calc(nid[rev[v]+j],1);
}
calc(x,1);
..........//更新答案
if(!keep) for(rg int i=0;i<siz[x];++i) calc(nid[rev[x]+i],-1);
}
复杂度证明
不感兴趣的大佬可以跳过这一段。(蒟蒻自己乱\(yy\)的证明,如果有错请大佬指出)
显然,根据上面的图解,一个点只有在它到根节点的路径上遇到一条轻边的时候,自己的信息才会被祖先节点暴力统计一遍
而根据树剖相关理论,每个点到根的路径上有\(logn\)条轻边和\(logn\)条重链
即一个点的信息只会上传\(logn\)次
如果一个点的信息修改是\(O(1)\)的,那么总复杂度就是\(O(nlogn)\)
几道可爱的例题
例题\(1\):$$\color{#66ccff}{\texttt{-> 树上数颜色 <-}}$$
此题来自洛咕日报第\(65\)篇作者\(\text{codesonic}\)。
我们可以维护一个全局数组\(cnt\),代表正在被计算的子树的每种颜色的数量
每次计算子树贡献的时候,把节点信息往里面加就行了,如果一个颜色第一次出现,则颜色种类数\(top++\)
对于需要撤销影响的子树,把信息从里面丢出来即可,如果被删除的颜色只有这一个,则颜色种类数\(top--\)
\(Code\)
例题\(2\):$$\color{#66ccff}{\texttt{-> CF600E Lomsat gelral <-}}$$
公认的\(\text{dsu on tree}\)模板题,相比于上题只是增加了对每种数量的颜色和的统计。
我们可以维护\(cnt\)数组,表示某个颜色出现的次数;再维护一个\(sum\)数组,表示当前子树出现了\(x\)次的颜色的编号和
对节点信息统计时,先把它在\(sum\)数组里的贡献删掉,更新了\(cnt\)数组后再添回去
然后别忘了开\(long \, long\)(血的教训)
\(Code\)
应用篇/各种灵活运用
CF570D Tree Requests
$$\color{orange}{\texttt{-> 原题传送门 <-}}$$
窝太菜了,不会二进制优化,只会\(O(26*nlogn)\)
首先,因为要形成回文串、又可以对字符进行任意排列,所以最多只能有一种字母的出现次数为奇数
然后我们维护一个\(cnt\)数组,统计每个深度所有字母的出现次数:
cnt[dep[x]][s[x]-'a']+=val;
- 最后再\(check\)一下就好了
\(Code\)
CF246E Blood Cousins Return
$$\color{orange}{\texttt{-> 原题传送门 <-}}$$
首先用\(map\)把给的所有名字哈希成\(1\)到\(n\)的数字
题目就可以转化为求出每个深度有多少不同的数
同样,对每个深度开个\(set\)去重并统计
然后就是套板子的事情了
\(Code\)
CF208E Blood Cousins
$$\color{orange}{\texttt{-> 原题传送门 <-}}$$
显然原问题可以转化为求该点的\(k\)级祖先有多少个\(k\)级儿子(如果没有\(k\)级祖先,答案就是0)
而一个点\(x\)的\(k\)级儿子即为在以\(x\)为根节点的子树中有多少点的\(dep\)为\(dep[x] + k\)
把所有询问读进来,求出相关的点的\(k\)级祖先(可以离线\(O(n)\)处理,也可以倍增\(O(nlogn)\)搞;如果时空限制比较紧,就采取前者吧)
然后因为是统计节点数,所以开一个普通的\(cnt\)数组维护即可。最后答案别忘了\(-1\),因为算了自己
扔一个加强版的(\(N \le 10^6\),\(128MB,1s\)):\(\color{#66ccff}{\texttt{-> 传送门 <-}}\)
友情提醒:上面这道良心题不仅卡空间,还卡时间(如果你用dsu on tree)
\(Code\)
IOI2011 Race
$$\color{orange}{\texttt{-> 原题传送门 <-}}$$
点分治的题怎么能用点分治呢?而且这还是dsu on tree学习笔记
首先,这道题是对链的信息进行统计,就不能再像对子树的统计方法去搞♂了,所以需要一些奇技淫巧
思路与点分治一样,对于每个节点\(x\),统计经过\(x\)的路径的信息
注意到这道题链上的信息是可加减的,所以我们可以不保存\(x\)的子孙\(\rightarrow x\)的信息,而是保存每个节点到根节点的信息,在统计的时候在减去\(x \rightarrow\)根节点的信息
然后我们考虑如何统计,我们可以在每个节点维护一个桶\(cnt\),记录从这个点\(x\)往下走的所有路径中,能形成的每种路径权值和以及其所需要的最少的边的数量:
对于\(v_{ij}\),计算出其到\(x\)的距离\(dis\)及深度差\(d\)(可以看成路径上的节点数),并用\(d\) \(+\) \(cnt[\)k−dis\(]\)来更新答案。
然后用刚才得到的\(dis\)对应的\(d\)来更新\(cnt[dis]\)的值。
这样就相当于,用每个\(v_{ij}\)到\(x\)的链,与之前桶中所保存某条链的路径权值和之和恰为\(k\)的拼成一条路径,并更新答案。然后,再把它也加入桶中
再套上\(\text{dsu on tree}\)的板子,每个节点保存它的重儿子的 桶的信息即可
虽然是\(O(nlog^2n)\)的,但常数小,咱不慌
但是窝太菜了,用\(map\)作桶不开\(O2\)会\(T \, 3\)个点(毕竟用了\(STL\),还有两只\(log\)),有空再重写一遍233
貌似用\(unodered_{}map\)不开\(O2\)也卡得过去。。
\(Code\)
NOIP2016 天天爱跑步
$$\color{orange}{\texttt{-> 原题传送门 <-}}$$
- 首先,我们可以把\(S \Rightarrow T\)这条路径拆成\(S \rightarrow lca(S,T)\) 和 \(lca(S,T) \rightarrow T\)两段来考虑
考虑在第一段路径上一点\(u\)能观测到该玩家的条件是:\(dep[S] - dep[u] = w[u]\)
同理,在第二段路径上一点\(u\)能观测到该玩家的条件是:\(dep[T] - dep[u] = dis(S,T) - w[u]\),即\(dep[S] - 2 \times dep[lca(S,T)] = w[u] - dep[u]\)
然后可以用差分的思想,对每个节点开两个桶\(up\)、\(down\)进行统计
在\(S\)的\(up\)中插入\(dep[S]\)
在\(T\)的\(down\)中插入\(dep[S] - 2 \times dep[lca(S,T)]\)
因为\(lca(S,T)\)会对\(S \rightarrow T\)和\(T \rightarrow S\)都进行统计,所以在其\(up\)中删除\(dep[S]\)
同理,在\(fa[lca(S,T)]\)的\(down\)中删除\(dep[S] - 2 \times dep[lca(S,T)]\)
然后用\(\text{dsu on tree}\)统计即可,答案为\(up[w[u]+dep[u]] + down[w[u] - dep[u]]\)
注意到\(w[u] - dep[u]\)可能小于零,为了避免负数下标、又不想套\(map\),我们可以使用如下\(trick\)
int up[N],CNT[N<<1],*down=&CNT[N];
//把donw[0]指向CNT[N],这样就可以给负数和正数都分配大小为N的空间
跑的虽然没有普通的差分快,不过吊打线段树合并还是绰绰有余的
\(Code\)
[Vani有约会]雨天的尾巴
$$\color{orange}{\texttt{-> 原题传送门 <-}}$$
跟天天爱跑步差不多,就不画图了(~懒)
同上题,用差分的思想,对每个节点的增加和删除开两个桶统计
同时,这题要维护每个点出现的最多物品的种类,直接开个线段树维护就好了
\(O(nlog^2n)\),常数应该和树剖差不多,不过因为每个点都要进行增加删除两个操作,常数大了一倍,而且还用了线段树,所以\(\cdots\)
不过依然比部分线段树合并跑的快2333
\(Code\)
由以上三题,我们可以看出,在一定条件下,\(\text{dsu on tree}\)也是可以在链上搞♂事情的
比如\(Race\)满足链上信息可加减性,后两道题可以用差分将链上的修改/询问转化为点上的修改/询问
但\(\text{dsu on tree}\)可以应用的条件肯定不止以上两种,因为窝太蒻了,只见识了这些题,以后看到其他类型的也会补上来
射手座之日
$$\color{orange}{\texttt{-> 提交地址 <-}}$$
现在终于可以回过头来解决这个题了
留给大家思考吧,要代码的话可以私信我
虽然有很多大佬会线段树合并或虚树上\(dp\)秒切这道题,不过还是希望用\(dsu \; AC\)
参考资料/总结
参考资料
总结
以后还会不定期地添加\(\text{dsu on tree}\)的相关题目~
如果有需要,我会把最后那道题的代码贴出来
dsu on tree学习笔记的更多相关文章
- dsu on tree 学习笔记
这是一个黑科技,考虑树链剖分后,每个点只会在轻重链之间转化\(log\)次. 考虑暴力是怎么写的,每次枚举一个点,再暴力把子树全部扫一边. \(dsu\ on\ tree.\)的思想就是保留重儿子不清 ...
- 珂朵莉树(Chtholly Tree)学习笔记
珂朵莉树(Chtholly Tree)学习笔记 珂朵莉树原理 其原理在于运用一颗树(set,treap,splay......)其中要求所有元素有序,并且支持基本的操作(删除,添加,查找......) ...
- Codeforces 600E. Lomsat gelral(Dsu on tree学习)
题目链接:http://codeforces.com/problemset/problem/600/E n个点的有根树,以1为根,每个点有一种颜色.我们称一种颜色占领了一个子树当且仅当没有其他颜色在这 ...
- Link Cut Tree学习笔记
从这里开始 动态树问题和Link Cut Tree 一些定义 access操作 换根操作 link和cut操作 时间复杂度证明 Link Cut Tree维护链上信息 Link Cut Tree维护子 ...
- 矩阵树定理(Matrix Tree)学习笔记
如果不谈证明,稍微有点线代基础的人都可以在两分钟内学完所有相关内容.. 行列式随便找本线代书看一下基本性质就好了. 学习资源: https://www.cnblogs.com/candy99/p/64 ...
- k-d tree 学习笔记
以下是一些奇怪的链接有兴趣的可以看看: https://blog.sengxian.com/algorithms/k-dimensional-tree http://zgjkt.blog.uoj.ac ...
- splay tree 学习笔记
首先感谢litble的精彩讲解,原文博客: litble的小天地 在学完二叉平衡树后,发现这是只是一个不稳定的垃圾玩意,真正实用的应有Treap.AVL.Splay这样的查找树.于是最近刚学了学了点S ...
- LSM Tree 学习笔记——本质是将随机的写放在内存里形成有序的小memtable,然后定期合并成大的table flush到磁盘
The Sorted String Table (SSTable) is one of the most popular outputs for storing, processing, and ex ...
- LSM Tree 学习笔记——MemTable通常用 SkipList 来实现
最近发现很多数据库都使用了 LSM Tree 的存储模型,包括 LevelDB,HBase,Google BigTable,Cassandra,InfluxDB 等.之前还没有留意这么设计的原因,最近 ...
随机推荐
- Atcoder&CodeForces杂题11.6
Preface NOIP前突然不知道做什么,感觉思维有点江僵化,就在vjudge上随便组了6道ABC D+CF Div2 C/D做,发现比赛质量还不错,知识点涉及广,难度有梯度,码量稍小,思维较多. ...
- mac os安装mtr
MTR是Linux平台上一款非常好用的网络诊断工具,集成了traceroute.ping.nslookup的功能,用于诊断网络状态非常有用 现使用的方法是下载pkg包手动安装 mtr的pkg下载地址 ...
- js入门之内置数组对象 Array
一. 数组 1. 创建数组的两种方式 1. 数组字面量 var array = [] 2. 数组的构造函数创建数组 var array = new Array(); 2. 如何判断一个变量是否是数组 ...
- JAVA笔记整理(二),下载安装JDK
Windows平台 1.登录Oracle官方网站(http://www.oracle.com/index.html),找到下载 2.选择要下载的版本,点击JDK DOWNLOAD 3.下载文件,先勾选 ...
- vue-quill-editor + iview 实现富文本编辑器及图片上传
1.npm 安装 vue-quill-editor npm install vue-quill-editor 2.再main.js中引入 import VueQuillEditor from 'vue ...
- spark coalesce和repartition的区别和使用场景
区别: repartition底层调用的是coalesce方法,默认shuffle def repartition(numPartitions: Int)(implicit ord: Ordering ...
- Python将字符串转换成字典
1. ast包 import ast user_info = '{"name" : "南湖", "gender" : "male& ...
- Nginx的平滑升级记录---适用于编译安装的Nginx
一.查看自己的Nginx的版本号 [root@localhost sbin]# cd /usr/local/nginx/sbin/ [root@localhost sbin]# ls nginx [r ...
- python打造批量关键词排名查询工具
自己做站点的时候,都看看收录和关键词排名什么的,所以打造的这个批量关键词查询工具. #encoding:utf-8 import urllib,re,random,time,sys,StringIO, ...
- POJ1185 炮兵阵地 和 POJ2411 Mondriaan's Dream
炮兵阵地 Language:Default 炮兵阵地 Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 34008 Accepted ...