一些还没熟练的算法
虚树
一个简单的建树方法:
-
按 \(dfs\) 序排序关键节点。
-
相邻点的 \(\operatorname{LCA}\) 加入到虚树集合中
-
对集合中的点按原树中的祖先关系建边
我们发现比较恶心的是第 3 步无法快速实现,考虑将点一个一个插入,维护这个 2-3 的过程。
我们用一个单调栈维护点,满足栈底元素到栈顶元素的 \(dfs\) 序单调递增。
当我们新插入一个结点 \(u\) 时,求出它与 \(top\) 的 \(\operatorname{LCA}=x\)。
如果 \(x=top\),那么 \(top\) 是 \(u\) 的父亲,并把 \(u\) 加入到栈中。
如果 \(x\ne top\),说明 \(x-top\) 这条链的虚树结构已经构造完毕,那么一直将栈顶元素弹出并连边 \((top-1, top)\),直到栈顶元素的 \(dfs\) 序小于等于 \(x\) 停止,不断将弹出的元素建边 \((top, top-1)\),那么再把 \(x\) 压进去就可以了(要看此时栈顶是不是 \(x\),因为 \(x\) 在之前可能已经被加过了)。
坑点:最后弹出的是 \(x\) 的儿子,连边是 \((x, top)\),为什么上面是 \(\le\) 而不是 \(<\),因为 \(x\) 是要保留在栈中的,其与 \(top-1\) 的连边后面会连,现在是不用连的。
为什么不加边 \((x, u)\) 和 \((top', x)\) 呢?因为他们会在后面被外链迫使其连边。
最后如果栈里还有元素记得一一 pop 连边。
例题:
[SDOI2011] 消耗战:建出虚树后,设 \(f_i\) 为隔断 \(1\) 与 \(i\) 的子树内的关键点的最小代价,则:
其中 opt 表示该点是否为关键点,若是则为无穷大,否则为 1。
[HNOI2014]世界树:同理,现在需要考虑两点之间的点集怎么分配,我们令每个关键点有一个支配点集 \(S\),设 \(v\in S_u\),当它变为关键点后,所得到的支配点集必然是 \(S_u\) 的子集。
考虑如下增量构造法:在虚树上选取任意一个关键点为新的树根,维护树上每个点所属的关键点支配集的标号,按虚树上深度从小到大把关键点加入(这样我们能保证加入的点至少已经被分配到正确的父亲)。首先当前加入点 \(u\) 的子树必然都被 \(u\) 支配,否则就是和子树外结点的争夺,那么显然就是 \((u, v)\) 路径上的 1/2 分点,数据结构维护即可。
其他: [HEOI2014]大工程 [SDOI2015]寻宝游戏 [HNOI/AHOI2018]毒瘤
其实难的都不是虚树部分。。
点分治
用于处理一类树上路径统计问题,与树上点对距离有关的问题亦可往这里想
考虑这样一个分治操作,对于当前的树,我们只统计经过根的路径贡献,没有经过根的,我们把根结点拿掉,分治交给儿子去做。
显然这个做法在树为链时会爆炸,考虑优化。
我们知道重心的性质是所有子树大小不超过整棵树的 \(\frac{n}{2}\),于是我们珂以在递归下一层分治时先找子树重心,在递归下去。
这样的话递归层数只有 \(\log n\) 层,每层求重心 \(O(n)\),复杂度 \(O((n+f(n))\log n)\),\(f(n)\) 是统计的复杂度。
伪代码:
void dfz(rt){
vis[rt]=-1;//标记为删除点
for(each (rt, v) in G)
if(vis[v]>=0) calc(v);//处理 v 的子树与前面子树的路径贡献
Getsize(rt);//如果能在前面的 calc 就能将 v 的子树大小算出就不必做这一步
for(each (rt, v) in G)
if(vis[v]>=0) findrt(v), dfz(v)
}
明明 findrt 中已经求了一次 siz,为什么要在下一层再 Getsize 呢?
因为在上一层递归中,我们求得并非是以重心为根的 size,因此要重做一遍。
具体可以看 这篇。
显然点分治只能做一次,我们考虑将其动态化,以处理多个问题,
那么便引出了动态点分治————点分树
操作很简单,就是在点分治过程中将上一层的递归重心与下一层的连边,构成一颗新树。
考虑新树有什么特点:
-
树高为 \(\log n\),即点分治递归层数。
-
两个点 \(l, r\) 在点分树上的 \(\operatorname{LCA}\),必然在原树中 \(l, r\) 的路径上。
笛卡尔树
类似与 treap,我们对一个序列建树,对于原序列的编号满足二叉搜索树关系,对于权值满足大/小根堆方式,注意若权值相等,则将序号最小为父亲节点。
一个简单的建树方法,找到当前区间最大/小值,分治去做,找最大值用 ST 表,复杂度 \(O(n\log n)\),有更优秀的建树方式,但没必要学。
因为这玩意都是用来转化的。
比较经典的例题:
UOJ424:映射到笛卡尔树上后,区间 \([l, r]\) 的最大值的点就是笛卡尔树上 \(l,r\) 的 \(\operatorname{LCA}\),那么其本质就是问有多少不同构的笛卡尔树。
进一步考虑怎么把 \(1-m\) 全部包含,想到笛卡尔树的性质是父亲一定严格大于左儿子(相当于值域递减),于是满足 \(1-m\) 包含的转化条件就是这棵笛卡尔树的最长左链长度不超过 \(m-1\),设 \(f_{i, m}\) 为满足 \(i\) 个点、最长链不超过 \(m-1\) 的笛卡尔树的个数:
然后就是我不会的生成函数+多项式了
[NOI2019] 机器人:转化成分别往左儿子和右儿子下走,变成左右链长度在 2 以内,DP 即可
图论分块
设重点为 \(deg_u \ge \sqrt{2m}\) 的点,其余为轻点,由此珂知重点 \(u\) 连向其他重点的边不超过 \(\sqrt{2m}\) 条,因为重点个数不超过 总入度/重点最低度数=\(2m/ \ge \sqrt{2m}=\sqrt{2m}\)。
对于重点修改操作,直接修改重点和与其相连的重点,
对于轻点修改操作,直接暴力修改全部相邻点。
对于重点询问,直接输出维护好的信息。
对于轻点询问,暴力枚举加上重点给它的贡献。
CDQ 分治
线段树分治
网络流
多项式
万能欧几里得
二进制分组
一种强有力的在线转亚离线的思想,对于带(末尾)插入/删除的动态 DS 问题,能在时间复杂度多只 \(\log\) 的情况下,使得每次暴力重构能够艹过去,前提是合并复杂度与序列长度成线性,如凸包合并,主席树单点合并。
考虑类似于多重背包的二进制拆分,每当序列新插入一个值的时候,将其直接插入,随后看前面维护的整块 +1 后是否能成为 2 的整次幂,若能则合并,并且一直合并下去。
这样就能保证最终只有 \(\log n\) 块,那么询问分块即可。
删除时把权值改为 -1.