启发式合并(dsu),树上启发式合并(dsu on tree)总结
启发式合并(dsu),树上启发式合并(dsu on tree)总结
算法内容
前置知识:启发式合并(dsu)
\qquad 想学习“树上”启发式合并,就要先知道什么是“启发式合并”。
启发式算法是基于人类的经验和直观感觉,对一些算法的优化。 —— oi-wiki
\qquad 启发式合并的典型例子是并查集的启发式合并,即按秩合并。在合并两个并查集的时候,我们可以选择让深度或大小较小的一个并查集并到另一个上面。这种合并方式虽然看起来很暴力,但是时间复杂度可以证明是 Θ ( n log n ) \Theta(n\log n) Θ(nlogn) 的。还有像 set \text{set} set, vector \text{vector} vector 这类的数据结构也是可以直接启发式合并的,时间复杂度证明类似于并查集的按秩合并。
\qquad 启发式合并是一种优雅的暴力,看起来跟暴力差不多,但是实际上时间复杂度是严格正确的。
例题:[HNOI2009] 梦幻布丁
\qquad 题面
\qquad 本题就是一个典型的 set \text{set} set 的启发式合并,直接暴力做即可。实现时有一点点小细节,还有一个至关重要的小 trick \text{trick} trick: S T L STL STL 中的 set , vector \text{set}, \text{vector} set,vector 是可以直接使用 swap \text{swap} swap 函数的,时间复杂度 Θ ( 1 ) \Theta(1) Θ(1)。
\qquad 核心 Code : \text{Code}: Code:
重点:树上启发式合并(dsu on tree)
\qquad 知道了什么是启发式合并后,进一步就该学习树上启发式合并了。
\qquad 树上启发式合并主要是用来解决一些允许离线的子树统计问题、点对统计问题,可以套树形 dp \text{dp} dp,某些路径统计问题也可以解决。树上莫队、线段树合并这类算法能解决的问题有许多树上启发式合并也能解决。而且对于某些询问不统一的问题(即每个节点询问的内容相同,但某些要求不同)也可以解决。
\qquad 其实树上启发式合并最主要是利用了启发式合并的思想。启发式合并的时候,我们可以通过让小的合并到大的上来保证时间复杂度。那么在树上,我们怎样才能保证时间复杂度正确呢?
\qquad 我们以前学过一种名为树链剖分的算法,在这一算法中,我们引入了轻、重儿子的概念,并分析了其性质以及其为什么能保证复杂度。在 dsu on tree \text{dsu on tree} dsu on tree 中,我们也可以借用轻重儿子来保证时间复杂度。具体的,树上启发式合并分为以下几步:
- 递归轻儿子,解决轻儿子中的询问,并清空轻儿子中的信息;
- 递归重儿子,解决本条重链中其他点的询问,不清空重链的信息;
- 将轻儿子和自己的信息加入进来,然后处理自己的询问。如果自己是个轻儿子,那么清空所有信息;否则不清空。
\qquad 这么做看起来,十分甚至九分的暴力。那么就又到了经典环节:我们来分析一下时间复杂度。这一做法看起来暴力,主要在于一个点被访问的次数看起来很多。我们想:一个点被访问到只有两种情况:1、计算当前点答案;2、作为轻子树中的点,加入信息。不难发现,情况 1 1 1 只会进行一次,而根据轻边的性质,可以轻易得知情况二最多只会被统计到 Θ ( log n ) \Theta(\log n) Θ(logn) 次。所以整体复杂度为 Θ ( n log n ) \Theta(n\log n) Θ(nlogn)。
\qquad 树上启发式合并的拓展性极强。而且因为本身时间复杂度为 Θ ( n log n ) \Theta(n\log n) Θ(nlogn),所以有时还可以套一些复杂度 Θ ( log n ) \Theta(\log n) Θ(logn) 的数据结构。
\qquad 对于这类写起来较为繁琐的算法,定一个该算法的板子、整体框架显然是个极好的决定:
例题
#1 Tree Requests
\qquad 题面
\qquad 本题是个很典型的子树统计(虽然有深度限制),而且询问不统一,显然可以用 dsu on tree \text{dsu on tree} dsu on tree 解决。一堆字符能构成回文串有什么条件呢?显然是:出现次数为奇数的字符个数小于等于 1 1 1。那么我们记录一下某一深度,某一字符的出现次数,然后询问的时候枚举 26 26 26 个字符,记录有几个出现次数为奇数即可顺利解决。
\qquad 既然 dsu on tree \text{dsu on tree} dsu on tree 已经定板了,那么我们只需小小修改一下 ins,query \text{ins},\text{query} ins,query 函数即可。
\qquad 核心 Code \text{Code} Code:
#2 Blood Cousins Return
\qquad 题面
\qquad 本题也是典型的询问不统一。但是因为本题查询的是有几个不同的字符串,显然要用一个时间复杂度为 Θ ( log n ) \Theta(\log n) Θ(logn) 的数据结构: map \text{map} map 来存。
\qquad 核心 Code \text{Code} Code:
#3 Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
\qquad 题面
\qquad 同样是回文串,但本题明显偏难,因为它查询的是最长回文串长度。如果查全局最长回文串长度,我们显然可以用点分治写。但是这道题是每一个子树都查。怎么办呢?我们仍然可以借用点分治的思想:在计算时,只计算经过当前子树根节点的路径的最长回文串长度,然后和子树内的取个 max \max max 即可。
\qquad 既然“根”已经定了,那么两点间距离显然可以用 dep x + dep y − 2 × dep r o o t \text{dep}_x + \text{dep}_y - 2\times \text{dep}_{root} depx+depy−2×deproot 表示。那么如何快速查询与自己相配对的回文串的最长长度呢?这就又用到了一步转化:因为一个回文串只允许最多有一个出现次数为奇数的字符,那么若一个字符出现了偶数次,那么我们可以看做它没出现过;同理,若一个字符出现了奇数次,那么我们可以看做它只出现了一次。再次观察题面:字符总数只有 22 22 22 个,那不是显然告诉我们要压成一个二进制数吗?但是,我们把哪一部分压成二进制数呢?压成二进制数之后该存在哪呢?
\qquad 既然想到了二进制数,我们就先不急着去处理上面两个问题,先想想什么状态是合法的。显然当这个二进制数只有一位为 1 1 1,或它本身就是 0 0 0 时是合法的。而我们根据点分治的思想,显然需要让两个不同子树中的点到子树根的路径相拼接,得到一条合法路径。到此,上面两个问题就迎刃而解:我们要把从子树根到子树内每一个点压成一个二进制数,存在这条路径的终点处。那么这两条被拼接的路径需要满足什么要求呢?显然的是,我们如果把它们分别看作两个二进制数,那么这两个二进制数异或起来一定是个合法状态。想到了这一步,我们接着想异或操作有什么性质?两个相同的数异或起来为 0 0 0!虽然会异或的人都知道这一基本性质,但是你不得不承认它很有用。有用在哪呢?我们想:若对于每一个子树根,每次都枚举一遍子树、把每一条路径压成二进制数并储存,时间复杂度显然直接变大变高。但是根据异或的重要性质,我们显然只用存 1 1 1 号点(树根)到每个点的路径,每次一异或就把子树根以上的路径给异或掉了,就能直接计算了。
\qquad 本题还有两个至关重要的小细节:1、既然是点分治的思想,那么就要先计算再添加;2、桶数组初值要赋为极小值。
\qquad 核心 Code \text{Code} Code:
#4 [NOIP2016 提高组] 天天爱跑步
\qquad 题面
\qquad 本题做法很多, dsu on tree \text{dsu on tree} dsu on tree 显然也是不二之选。但是本题乍一看,和 dsu on tree \text{dsu on tree} dsu on tree 一点点关系也没有啊……那我们显然需要将问题认真分析一下。
\qquad 首先,我们考虑对于一个点 x \text{x} x,那些路径可能被 x \text{x} x 统计到?最基本的条件显然是这条路径有一个端点在以 x \text{x} x 为根的子树中。这个条件满足了,接下来就该是跑步时间的限制了。那么我们想,被统计到的路径是不是可以分为两类:1、起点在以 x \text{x} x 为根的子树中(不管终点位置);2、终点在以 x \text{x} x 为根的子树中(不管起点位置)。对于这两种路径,我们分别处理。
\qquad 对于起点在子树内的路径,实际上是很好统计的。因为此时 w x \text{w}_x wx 就相当于 x \text{x} x 与起点的深度差。但是对于终点,怎么统计呢?
\qquad 假设 t \text{t} t 是一个合法的终点, T \text{T} T 是这个终点所在路径的长度,那么这个 t \text{t} t 要满足什么条件呢?如果列出式子,那应该是这样的: w x + ( dep t − dep x ) = T \large \text{w}_x+(\text{dep}_t-\text{dep}_x)=\text{T} wx+(dept−depx)=T,简单移项后发现 w x − dep x = T − dep t \large \text{w}_x-\text{dep}_x=\text{T}-\text{dep}_t wx−depx=T−dept。此时,我们发现等号左边只与 x \text{x} x 有关,等号右边只与 t \text{t} t 有关,那么我们显然可以看成点对统计问题直接开桶解决。
\qquad 起点和终点都解决了,那这题解决了吗?显然没有:有可能这条路径的 lca \text{lca} lca 刚好是 x \text{x} x,那么这条路径便会在起点和终点分别被统计一次。这时我们便需要考虑去重。去重也很好写:我们只需将一条路径在 lca \text{lca} lca 处存一下,在计算完后遍历以 x \text{x} x 为 lca \text{lca} lca 的路径判断一下即可。
\qquad 还有一个至关重要的细节:一条路径肯定不会被 lca \text{lca} lca 之上的节点统计到,所以我们在遍历以 x \text{x} x 为 lca \text{lca} lca 的路径时,要顺便把这些路径在桶中的贡献清空。这样,本题就完美解决了。
\qquad Code \text{Code} Code:
#5 树上统计
\qquad 题面
\qquad 对于本题,第一思路肯定是求解有多少区间跨越了这条边。但是这显然很难求,于是我们考虑正难则反,统计被一条边分开的两个连通块中分别包含了多少个连续区间,然后用总贡献减去这些不经过本条边的区间。那我们的问题就转化为:被一条边分开的两个连通块中分别包含了多少连续区间。
\qquad 对于动态维护连续区间这类问题,我们显然可以通过维护每一段左右端点来轻松做到。具体的,我们开一个桶记录每个位置是否在子树内出现过,然后我们分别统计有多少个连续 1 1 1,有多少个连续 0 0 0 即可。因为随着运算的进行,这个桶维护的信息只增不删,那么维护连续 1 1 1 显然是很好做的:维护每一段连续 1 1 1 的左右端点即可。但是对于连续 0 0 0 怎么搞呢?我们可以考虑开一个 set \text{set} set 来存当前存在的 0 0 0 区间,每次把一个 0 0 0 改为 1 1 1 时,找到包含当前 0 0 0 的区间并进行相应操作即可。
\qquad 核心 Code \text{Code} Code:
其余练习题
\qquad Tree and Queries
\qquad 树上数颜色
\qquad
Dominant Indices
__EOF__

本文链接:https://www.cnblogs.com/best-brain/p/18006554.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)