dsu on tree & 线段树合并学习笔记
1 dsu on tree
1.1 优雅的暴力
莫队是优雅的暴力,dsu on tree 也是。
事实上 dsu on tree 和 莫队 甚至还有一些相同的地方,当然也有很多不同的地方。
1.2 不优雅的暴力A
考虑这样的暴力:
- 暴力枚举子树上的点并统计答案
时间复杂度:\(O(Tn)\)。
这显然是不优雅的,因为有些子树的答案明明之前算过了,但是却又算了一遍。
1.3 不优雅的暴力B
考虑另一个暴力:
- 递归所有儿子
- 合并所有子树信息
时间复杂度:\(O(nm+T)\),\(m\) 为合并的时间复杂度。有些时候,\(m\) 较大,信息过于零散,因此也不优雅。
1.4 优雅的 dsu on tree
注意到我们不能不预处理信息,也不能预处理所有信息。因此,我们考虑边搜索边储存一些临时信息。
先放出代码:
void dfs(int x,bool f)
{
for(int y:edge[x]) dfs(y,(y==hson[x]));
update_tree(x);
ans[x]=getans();
if(!f) clear_tree(x);
}
int main()
{
build_tree();
init_query();
dfs(1,true);
for(int i=1; i<=qcnt; ++i) output();
}
诶,这份代码里多了好多东西啊,hson
是啥啊,update_tree
又是啥啊?
1.5 dsu on tree 的奥秘
hson
是一个节点的重儿子。如果您不知道重儿子是什么,请先学习轻重链剖分。update_tree
和clear_tree
都是暴力!它们分别负责统计一棵子树中去掉根结点重儿子的子树后剩余节点的答案。
也就是说,对于一个节点,我们这么处理:
- 递归轻儿子,不保留子树贡献
- 递归重儿子,保留子树贡献
- 枚举子树中不在重儿子子树中的点的贡献
- 处理这个节点的询问
由于某些轻重链剖分的性质,复杂度为 \(O(n\log n+T)\)。
1.6 何时该用 dsu on tree
- 没有修改,允许离线。
- 查询子树内信息。
- 较难在短时间内合并多棵子树的信息。
- 可以在短时间内加入或删除一个点的信息。
2 线段树合并
线段树合并也是一种暴力。
2.1 暴力
如何合并两棵线段树 \(A\) 和 \(B\):
- 如果当前节点两棵线段树都没有,返回空子树。
- 如果对于一个节点,\(A\) 没有,\(B\) 有,返回 \(B\),反之亦然。
- 如果已经递归到叶子节点,更新信息并返回。
- 递归左儿子。
- 递归右儿子。
- 更新信息。
啊?这不是单次 \(O(n\log n)\) 的吗?怎么 \(10^5\) 次合并还能跑啊?
的确,如果我们的二叉树全是满的,线段树合并时间复杂度是错的。
但是,如果我们在树上依次合并 \(n\) 棵线段树,并且所有线段树一共只有 \(k\) 个节点,我们会得到一个 \(\text{O}(k)\) 的做法。
我们考虑均摊分析一下,合并两棵线段树的复杂度是 \(\text{O}(|A\cap B|)\) 的,但是合并后总节点数会减少 \(|A\cap B|\) 个。
由于插入 \(n\) 个信息带来的节点数是 \(n\log n\) 级别,因此大多数时候我们认为线段树合并的复杂度是 \(\text{O}(n\log n)\)
2.2 何时该用线段树合并
- 所有线段树的总节点数不超过 \(n\log n\)。
- 信息可以合并
- 信息合并后不会分开或再次合并(例如 dfs 时一个点在递归完子树后合并,之后就不会再用到它子树的信息了)
3 例题
大多数题目两者都能做。
模板题相信大家都能轻松 AC /qq
- CF600E
- CF570D
- CF208E
- CF246E
- CF1009F
- P4556
应用题(咕咕咕)