启发式合并(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:
//opt=1
if(x == y) continue;
int fx = x, fy = y;
if(col[fx].size() < col[fy].size()) {
for(it1 = col[fx].begin(); it1 != col[fx].end(); it1 ++) {
int v = *it1;
if(it1 != col[fx].begin()) it2 = it1, it2 --;//如果it1不是开头迭代器,就找到上一个迭代器,存在it2中
if((it1 == col[fx].begin() || (it1 != col[fx].begin() && *it2 + 1 != *it1)) && col[fx].find(v - 1) == col[fx].end() && col[fy].find(v - 1) != col[fy].end()) per --;//当it1与前一个迭代器所指的值不相邻的时候再判断,因为如果相邻,那么他们本来就是一段,修改完还是一段,没有判断的必要,处理不好还可能误判
if(it1 != col[fx].end()) it2 = it1, it2 ++;
if((it1 == -- col[fx].end() || (it1 != col[fx].end() && *it1 + 1 != *it2)) && col[fx].find(v + 1) == col[fx].end() && col[fy].find(v + 1) != col[fy].end()) per --;
c[v] = fy, col[fy].insert(v);
}
col[fx].clear();
}
else {
for(it1 = col[fy].begin(); it1 != col[fy].end(); it1 ++) {
int v = *it1;
if(it1 != col[fy].begin()) it2 = it1, it2 --;
if((it1 == col[fy].begin() || (it1 != col[fy].begin() && *it2 + 1 != *it1)) && col[fy].find(v - 1) == col[fy].end() && col[fx].find(v - 1) != col[fx].end()) per --;
if(it1 != col[fy].end()) it2 = it1, it2 ++;
if((it1 == -- col[fy].end() || (it1 != col[fy].end() && *it1 + 1 != *it2)) && col[fy].find(v + 1) == col[fy].end() && col[fx].find(v + 1) != col[fx].end()) per --;
c[v] = fx, col[fx].insert(v);
}
col[fy].clear();
swap(col[fx], col[fy]);
重点:树上启发式合并(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 对于这类写起来较为繁琐的算法,定一个该算法的板子、整体框架显然是个极好的决定:
/*
define:
dfn[x]:x的dfn序(大多数用不到)
L[x]/R[x]:以x为根的子树的dfn序范围
sze[x]:以x为根的子树大小
son[x]:x的重儿子
*/
//-----------------------------
void dfs_pre(int x) {
//预处理出有用信息
sze[x] = 1, dfn[x] = ++ num, L[x] = num, V[num] = x;
for(int i = head[x]; i; i = edge[i].lst) {
int v = edge[i].to;
dfs_pre(v);
sze[x] += sze[v];
if(sze[v