(树上)启发式合并
启发式,就是发扬人类智慧来优化一些算法,比如启发式搜索和启发式合并。
启发式搜索就是根据人类直觉,给搜索设定一个估价函数,估价函数更符合条件的优先处理。举个例子,最短路里的估价函数就是最短距离。因为你手模的时候知道要先找最近的路而不是把所有的路都模拟一遍,把这个过程放到程序里实现就是启发式搜索。
然后是启发式合并。这个也很直觉,合并两个东西的时候把小的合并到大的里可以减小复杂度。而且这个看起来很暴力的东西可以把\(O(n^2)\)变成\(O(n\log n)\)。
具体证明一下:现在你有\(n\)个数,你要把它们合并起来。假设你选中两个集合合并,那么合并之后,小的那个集合的大小最少会变成\(2\)倍。而如果全部按照两倍算,我们原先共有\(n\)个集合,所以要合并\(\log n\)次,每次合并的复杂度\(O(n)\),总共\(O(n\log n)\)。
操作很简单,把小的部分的所有元素暴力插入大的部分里就行了。比如并查集,每个集合记录一个\(size\),合并时小的并入大的。再比如平衡树,小的所有的元素暴力插入大的里面,可以把总复杂度从\(O(n^2\log n)\)变成\(O(n\log^2n)\)。
注意:splay的启发式合并复杂度是\(O(n\log n)\)的。 证明请bdfs。
补充:据jijidawang说Splay不管什么顺序合并都是一个 \(\log\) 的。
然后是今天的主题:dsu on tree,即树上启发式合并。大体的思想差不太多,就是将小的子树的信息合并到大的子树上。复杂度证明类似上面的,同样是\(O(n\log n)\)。预处理直接找个重儿子就行了。
上个例题:P4577 领导集团问题(如果您觉得这个就是个普通的启发式合并,和dsu on tree没什么关系的话,那是您说的对)
题意:树上LIS。就是树上求所有从根到叶子的链的LIS中最大的。
首先我们原先\(O(n\log n)\)的二分LIS有一个数组\(ans\),具体的差不多长这个样子:
for(int i=1;i<=n;i++){
if(a[i]>=ans[ans[0]])ans[++ans[0]]=a[i];
else{
int x=lower_bound(ans+1,ans+ans[0]+1,a[i])-ans;
ans[x]=a[i];
}
}
这个原理不再叙述,忘记的请出门左转模板。
我们可以把这个数组扔到树上。具体地,我们每次遍历完当前节点的所有子树之后,所有子树的元素构成了\(ans\)数组。我们此时将该节点的元素按相同的方式插入这个数组,最后根节点处数组大小就是答案。这个过程大概有两种维护方式:
- 动态开点线段树合并,由于每个节点要更新一次,还要合并所有节点,所以复杂度是\(O(n\log n)\)的。当然线段树合并的常数要多大有多大。于是我们有另一种多一个\(\log\)但是跑的更快的做法。
- 由于这个\(ans\)数组是有序的,所以对每个节点开一个multiset维护,合并的时候暴力将子树的所有元素插入当前节点。当然这样把所有的都插入一次显然是\(O(n^2\log n)\)的。我们使用启发式合并,将所有其他子树中的multiset中的元素插入最大的子树,这样就能保证复杂度是\(O(n\log^2n)\)的。
然后是另一个例题:给出一棵\(n\)个节点以\(1\)为根的树,每个节点都有颜色,现在对于每个结点询问其子树里一共出现了多少种不同的颜色。
这个直接统计每个子树中出现颜色然后合并就行了。具体地说,我们首先扫一遍所有的轻儿子计算答案,但是不在颜色数组内保存。然后搜重儿子并将颜色记录进数组。最后扫一遍所有的轻儿子合并答案,复杂度\(O(n\log n)\)。