搜索

树上启发式合并 - 个人总结


发布时间: 2022-11-24 18:57:04    浏览次数:47 次

树上启发式合并(dsu on tree)用来处理这样一类题目:询问支持离线,并且询问与子树有关。它可以很方便地在O(nlogn) 内完成答案的统计。
我们基于这样一个简单的问题来讨论dsu on tree。U41492 树上数颜色 - 洛谷

给定一棵节点具有颜色的树,询问每棵子树中有多少种不同的颜色

(1)树上莫队:这个十分简单,我们不多赘述。时间复杂度为\(O(n\sqrt n)\)

以下介绍两种启发式合并的方法:

(2)树上桶合并:我们把每个节点子树中的颜色编号存于一个set桶中,显然我们只需获取桶的大小即可(我不确定size()函数是否会导致复杂度升级,最好还是维护sz数组)。在桶往上传时,我们总是把小桶合并到大桶,于是可以保证每个元素被移出桶的次数最多只有logn。这样做的复杂度为\(O(nlogn)\),考虑到set的操作是logn的,于是总复杂度为\(O(nlogn^2)\),能勉强应付1e6的数据。

(补充证明:对较小桶而言,合并后的新桶大小至少为自己的两倍,换而言之,对一个元素执行取出和压入操作,新桶大小必然大于等于旧桶的两倍,这意味着一个元素最多被执行logn次该操作。)

(3)树上启发式合并:

先论暴力算法。我们维护一个计数数组cnt,记录某颜色当前的数量,当cnt[x]由0变1时++ans。对每个节点,重置ans和cnt,然后遍历以它为根的子树,最后把ans记录在该点上。

而树上启发式合并基于的就是对\(O(n^2)\)的优化。对于一个结点u,我们不一定非要在被清空的cnt和ans上重新开始,因为u的儿子子树的信息同样是自己的一部分,所以可以在其某个儿子遗留的未重置的cnt和ans上接着维护ans。我们指定这个儿子为重儿子,能使总复杂度达到O(nlogn)。

证明:每个节点除了最原始的遍历,在它到根节点的路径上,每有一条轻边就意味着要被多遍历一次。由树链剖分的引理可知,从根到任一节点的链上最多只有logn条轻边,于是每个节点最多被遍历logn+1次。

算法步骤:

1 预处理每个点的重儿子

2-1 进入dfs函数,函数首先向非重儿子方向递归,完成这些节点的处理,这主要是为了自下而上出结果。dfs函数默认不清除数据,因此要手动清除。

2-2 接着才正式开始启发式合并。具体见如下参考模板。

参考模板:

void dfs0( int u, int fa ) { //遍历整个树,找每个点的重儿子
	siz[u] = 1;
	for( int v : G[u]) {
		if( v == fa ) continue;
		dfs0( v, u );
		siz[u] += siz[v];
		if( siz[v] > siz[son[u]] )
			son[u] = v;
	}
}

int ans; //记录答案的全局变量,跟随全局数组一起重置

void calc( int u, int fa, bool flag ) {//遍历以u为根的子树,处理每个节点的数据
	if( flag ) { //flag=0回退 flag=1添加
		//统计数据
	}	
	else {
		//按原路清理数据(通常是彻底清除,直接赋0、inf或-inf)
	}
	for( int v : G[u] )
		if( v != fa ) calc( v, u, flag );
}

void dfs( int u, int fa ) { //默认不清理遗留数据
	for( int v : G[u] ) {
		if( v == fa || v == son[u] ) continue;
		dfs( v, u );//先算轻儿子的答案
		calc( v, u, 0 );//计算完轻儿子的答案后 要把儿子的痕迹擦干净 为下一个儿子准备
        ans=0;//或+-inf
		//...可能还有其他的操作
	} //以下才是启发式合并的正式开始
	if( son[u] ) dfs( son[u], u );//重儿子的贡献仍然保留 不回退
    //别忘了把u本身加入数据
    //如果重儿子遗留的ans不是我们需要的,还得重置一下
	for( int v : G[u] ) {
		if( v == fa || v == son[u] ) continue;
		calc( v, u, 1 );//开始重新添加每个轻儿子的贡献 为后面计算自己准备
	}
	//...一堆操作,比如将ans计入res[u],或者处理挂在u上的询问
}

个人总结:

(1)保证每个节点只被dfs一次,被calc不超过logn次

(2)清除数据不能用memset,而按其怎么加进来去原路把数据清除,这样防止了复杂度退化。

(3)calc函数的功能是遍历子树记录子孙节点数据,如果这些节点的数据是给定的或者有办法预处理,还可以通过提前生成dfs序,来代替遍历子树。

(4)树上启发式合并的典型运用Problem - D - Codeforces神题,多细品)。 以0/1表示22个字母分别出现奇数次还是偶数次,我们所维护的数组叫做len,len[s]表示从根到某一节点形成的01串s的最大深度(启发:节点记录的数据要么与本点绑定,要么就相对于根节点),节点的s和深度都是可以预处理好的(这样刚好能由两点01串的异或得到两点的路径信息)。对于当前节点u,我们的目标是找到经过u的最长合法路径。对u的每棵轻儿子子树,要进行三次calc,第一次是遍历子树中的节点x,从现有的len集合中找到所有与x形成合法匹配的01串s,用dep[x]+len[s]-2*dep[u]来更新ans,第二次是把子树中所有节点x的01串s更新到len集合,也就是len[s]=max(len[s],dep[x])。第三次是清空len,注意要重置为-inf而不是0。这道题目显然无法采用莫队,因为第一次calc和第二次calc是严格分开的。

免责声明 树上启发式合并 - 个人总结,资源类别:文本, 浏览次数:47 次, 文件大小:-- , 由本站蜘蛛搜索收录2022-11-24 06:57:04。此页面由程序自动采集,只作交流和学习使用,本站不储存任何资源文件,如有侵权内容请联系我们举报删除, 感谢您对本站的支持。 原文链接:https://www.cnblogs.com/blover/p/16915744.html