启发式合并(dsu),树上启发式合并(dsu on tree)总结

启发式合并(dsu),树上启发式合并(dsu on tree)总结
摘要由CSDN通过智能技术生成

算法内容

前置知识:启发式合并(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 中,我们也可以借用轻重儿子来保证时间复杂度。具体的,树上启发式合并分为以下几步:

  1. 递归轻儿子,解决轻儿子中的询问,并清空轻儿子中的信息;
  2. 递归重儿子,解决本条重链中其他点的询问,不清空重链的信息
  3. 将轻儿子和自己的信息加入进来,然后处理自己的询问。如果自己是个轻儿子,那么清空所有信息;否则不清空。

\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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值