树上问题(长期更新)

一点idea

常见的分类方式

对于树上的路径,可以分为子树内的路径和子树外的路径,且这样的两条路径一定无交(这是充要的)。

虚树

在树上做一些东西的时候(废话),我们发现有很多树上的点是没有用的。

虚树就是对树上信息高度概括的技术,在虚树上,我们只保留最为有用的信息,且保持祖先后代关系不变。

具体而言,我们称一些点为关键点,即只有这些点会对问题的答案产生影响。

那么我们发现,关键点的LCA也是关键点。

当我们钦定树上的k个点作为关键点后,建出来的虚树最多有2k个点(感性理解)。

有一个事实:虚树里的点加多了也不会影响答案。这很显然。

那么为了方便,在建立虚树之前,我们把原树的树根1先加入虚树。

单调栈建立虚树

我们用栈维护虚树上的一条链,保证其中dfn单调递增。

起初,1在其中。

设现在的栈顶为tp

我们尝试加入一个点u

首先,找到l=LCA(u,tp)

  1. l=tp时,u就在这条链上,将u入栈即可。

  2. 否则,我们关注栈中次大结点(即栈顶下方的第一个元素)的dfndfnl的大小关系。当dfnl比其小时,将tp与栈中次大节点连边并退栈,重复此过程。跳出循环后,若l就是次大节点,那么把栈顶与l连边并退栈,否则将栈顶与l连边并退栈,然后将l入栈。最后将u入栈。

最后栈中还剩一条链,拿出来建边即可。

常数很小,若只考虑连边,是O(k)的,k为关键点的数量。

细节很多。使用链式前向星时,对于每个入栈的点,清空它的表头head

注意如果虚边需要边权的话,要另外去找一下。

消耗战 的虚树build
void build(){
	sort(h+1,h+k+1,cmp);
	tt=0;
	tot=1;
	stk[1]=1;
	head[1]=0;
	for(int i=1;i<=k;++i){
		if(h[i]==1) continue;
		int l=lca(stk[tot],h[i]);
		if(l!=stk[tot]){
			while(dfn[l]<dfn[stk[tot-1]]) add(stk[tot-1],stk[tot],qlinemin(stk[tot-1],stk[tot])),tot--;
			if(dfn[l]!=dfn[stk[tot-1]]) head[l]=0,add(l,stk[tot],qlinemin(stk[tot],l)),stk[tot]=l;
			else add(l,stk[tot],qlinemin(stk[tot],l)),tot--;
		}
		head[h[i]]=0;
		stk[++tot]=h[i];
	}
	for(int i=1;i<tot;++i) add(stk[i],stk[i+1],qlinemin(stk[i],stk[i+1]));	
}

二次排序+LCA连边建立虚树

首先,将关键点按dfn排序,并将任意相邻两个关键点求出LCA,将关键点和LCA都扔到数组A中。

由于dfn的性质,此时A中已经包含了所有虚树中的点(感性理解)。那么现在就是要按原树中的祖先后代关系建边。

我们把A再次按dfn排序,此时第一个点一定是虚树的根(但不一定是原树的树根1,这一点与单调栈做法不一样,要注意一下)。我们对任意两个A中相邻的点AiAi+1,连边(LCA(Ai,Ai+1),Ai+1)

第一个点是虚树的根,所以后面的LCA中肯定有一个是它,所以不管它了。

为什么这样是对的?

  1. LCA(Ai,Ai+1)=Ai时,由于已经按dfn排序,AiAi+1之间肯定没有其他关键点,所以直接连边就好。

  2. LCA(Ai,Ai+1)Ai时,同样由于已经按dfn排序,同上分析。

所以是对的。

常数较大。

使用时的细节

我们一般可以通过观察数据范围来判断是否在考察虚树(玄学)。

题目的数据范围里一般会有,最后的复杂度很可能是O(klogn)的。

虚树里的清空不能暴力去清,只能在进行计算/建树时顺带清空,这样才能保证复杂度。

经典结论

虚树上所有边的边权之和等于:

将关键点按DFS序排序,计算相邻两个关键点之间的距离之和,还要加上首尾两个关键点之间的距离,然后这坨东西除以2

树上合并类问题

dsu on tree

  1. 先遍历轻儿子计算答案,不保留影响。

  2. 遍历重儿子计算答案,保留影响。

  3. 再遍历轻儿子,加入其对u的贡献,计算u的答案。

这一部分通过类似重链剖分的分析,是O(nlogn)的。加上算贡献的复杂度还要再乘进去。

好写,适合乱搞,或者利用小常数冲过一些题。

树分治

点分治

可以处理大规模树上路径信息问题。

每次选择一个节点作为根节点rt,树上路径要么经过rt要么不经过rt。先处理rt作为链的一端,子树中的点作为另一端的链的贡献,那么经过rt的路径的信息可以通过这样的链合并得到。

然后对于每个子树递归下去处理。递归下去之前要清空当前这层对答案数组的影响,这需要记录撤销哪些操作,而不能暴力清空。

每次递归以重心作为根最优,因为子树大小减半,是O(nlogn)的。可能因为算贡献的复杂度还要乘上一坨。

重选根节点后要重新计算子树大小,否则会出锅。

边分治

和点分治是类似的,每次选一条边将树分为大小尽量相等的两部分。

但是菊花图可以卡这个,于是要将原树转二叉树。

一种类似线段树,对点x,若儿子个数不超过二,就直接连,否则新建两个点,将x的儿子分成两半扔给这两个新点,并且将虚点向x连边。空间复杂度O(nlogn)

一种是用一个lst记录x,要连一个儿子时,将lst向其连边,然后新建一个点y,再将lsty连边并lsty。空间复杂度O(n)

点分树

对原树进行重构保证树高稳定logn,可解决与原树形态无关的待修改问题。

用点分治找分治中心的方式重构,将每一层的分治中心与上一层的分治中心连边。

有一点性质:

  1. 两点在点分树上的LCA一定在原树上两点间的路径上。

  2. 原树上的一个连通块一定在点分树上以这个连通块中的点为根的一个子树内。

还原树

一般是交互。可以询问一些神秘信息,要求还原整棵树的结构。不会。

posted @   RandomShuffle  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示