树上启发式合并
引入¶
启发式算法是什么呢?
启发式算法是基于人类的经验和直观感觉,对一些算法的优化。
给个例子?
最常见的就是并查集的按秩合并了,有带按秩合并的并查集中,合并的代码是这样的:
1 2 3 4 5 6 | void merge(int x, int y) { int xx = find(x), yy = find(y); if (size[xx] < size[yy]) swap(xx, yy); fa[yy] = xx; size[xx] += size[yy]; } |
在这里,对于两个大小不一样的集合,我们将大小小的并到大的,而不是大的连接小的。
为什么呢?这个集合的大小可以认为是集合的高度(在正常情况下),而我们将集合高度小的并到高度大的显然有助于我们找到父亲
让高度小的树成为高度较大的树的子树,这个优化可以称为启发式合并算法。
算法内容¶
树上启发式合并(dsu on tree)对于某些树上离线问题可以速度大于等于大部分算法且更易于理解和实现的算法。
考虑下面的问题:
给出一棵树,每个节点有颜色,询问一些子树的颜色数量(颜色可重复)。
对于这种问题解决方式大多是运用大量的数据结构(树套树等),如果可以离线,询问的量巨大,是不是有更简单的方法?
树上莫队!
不行,莫队带根号,我要 log
既然支持离线,考虑预处理后
直接暴力预处理的时间复杂度为
可以发现,每个节点的答案是其子树的叠加,考虑利用这个性质处理问题。
我们可以先预处理出每个节点子树的
我们用 check[i]表示颜色
遍历一个节点,我们按以下的步骤进行遍历:
先遍历其非重儿子,获取它的 ans,但 不保留遍历后它的 check ;
遍历它的重儿子, 保留它的 check ;
再次遍历其非重儿子及其父亲,用重儿子的 check 对遍历到的节点进行计算,获取整棵子树的 ans;
上图是一个例子
这样,对于一个节点,我们遍历了一次重子树,两次非重子树,显然是最划算的。
经过这个过程,我们获得了这个节点的子树的所有 ans
为什么不合并第一步和第三步呢?因为 check 数组不能重复使用,否则空间会太大,需要在
显然若一个节点
注意除了重儿子,每次遍历完
复杂度¶
(对于不关心复杂度证明的,可以跳过不看)
我们像树链剖分一样定义重边和轻边(连向重儿子的为重边,其余为轻边)关于重儿子和重边的定义,可以见下图,对于一棵有
根节点到树上任意节点的轻边数不超过
又因为如果一个节点是其父亲的重儿子,则他的子树必定在他的兄弟之中最多,所以任意节点到根的路径上所有重边连接的父节点在计算答案是必定不会遍历到这个节点,所以一个节点的被遍历的次数等于他到根节点路径上的轻边树
图中标红的即为重边,重边连向的子节点为重儿子
大致代码¶
这里是预处理代码
1 2 3 4 5 6 7 8 9 10 11 | void dfs1(int u, int fa) { size[u] = 1; for (int i = head[u]; i; i = tree[i].next) { int v = tree[i].v; if (v != fa) { dfs1(v, u); size[u] += size[v]; if (size[v] > size[son[u]]) son[u] = v; } } } |
下面是求答案的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int dfs2(int u, int fa, bool keep, bool isson) { int tmp = 0; for (int i = head[u]; i; i = tree[i].next) { int v = tree[i].v; if (v != fa && v != son[u]) { dfs2(v, u, 0, 0); } } if (son[u]) tmp += dfs2(son[u], u, 1, 1); for (int i = head[u]; i; i = tree[i].next) { int v = tree[i].v; if (v != fa && v != son[u]) { tmp += dfs2(v, u, 1, 0); } } if (!check[color[u]]) { tmp++; check[color[u]] = 1; } if (!keep || isson) ans[u] = tmp; if (!keep) memset(check, 0, sizeof(check)), tmp = 0; return tmp; } |
代码是我口胡出来的,因为没有经过测评不保证正确。
运用¶
- 某些出题人设置的正解是 dsu on tree 的题
如CF741D。给一棵树,每个节点的权值是'a'到'v'的字母,每次询问要求在一个子树找一条路径,使该路径包含的字符排序后成为回文串。
因为是排列后成为回文串,所以一个字符出现了两次相当于没出现,也就是说,这条路径满足 最多有一个字符出现奇数次 。
正常做法是对每一个节点 dfs,每到一个节点就强行枚举所有字母找到和他异或后结果为 1 的个数<1 的路径,再去最长值,这样
- 可以用 dsu 乱搞~~吊打 std~~水分的题
可以水一些树套树的部分分(没有修改操作),还可以把树上莫队的
练习题¶
题意翻译:树的节点有颜色,一种颜色占领了一个子树,当且仅当没有其他颜色在这个子树中出现得比它多。求占领每个子树的所有颜色之和。
参考资料/扩展阅读¶
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用