树上问题杂类

Prufer 序列

基础知识

一个 \(n\) 个节点的唯一对应着一串长为 \(n-2\) 的序列,因此一个 \(n\) 阶完全图的有标号生成树个数为 \(n^{n-2}\)

无根树转 prufer: 不断将无根树中编号最小的叶子节点删除,并将与之相连的节点放入序列中,直到只剩 \(2\) 个节点。具体方法是,维护一个指针 \(p\)\(1\) 开始扫描,如果 \(p\) 是叶子节点,就将 \(fa_p\) 放入序列中,并将 \(fa_p\) 的度数减一,判断 \(fa_p\) 是否为叶子节点,如果是就在 \(fa_p<p\) 的情况下继续重复上述操作。否则就 \(p \to p+1\),继续扫描。

prufer转无根树:每次找到点集中最小的没有在 prufer 序列中出现的数,与 prufer 序列中的第一个数连边,并将二者分别从点集和序列中删除。

性质: \(i\) 在 prufer 序列中的出现次数即为 \(deg_i-1\)

P2290 [HNOI2004] 树的计数

通过 prufer 序列上述的性质转化为可重集全排列。

\[\frac{(n-2)!}{\prod(d_i-1)!} \]

P2624 [HNOI2008] 明明的烦恼

记住如果使用 Prufer 序列不用多想其他树上的事,只需要无脑把数字填入序列即可。

本题先对有约束的点使用上一题中的公填入序列,再对剩下的空位置使用一次 prufer 序列。

P4430 小猴打架

形成的树有 \(n^{n-2}\) 种,同时 \(n-1\) 条边的连接还有顺序,所以还要乘上一个 \((n-1)!\)

CF156D Clues

把小点对应到所在联通块的大点内,设联通块个数为 \(t\),其 Prufer 序列对应的长度就是 \(t-2\) 了,其中每一位可以填 \(n\) 个数中的一个,所以方案数是 \(n^{t-2}\)。但是你从序列生成一颗树的过程中,Prufer 序列已经钦定了一部分边,但是还有一条边是该大点从点集内被取出和 Prufer 序列中的点连的边,且每个大点恰好被从点集中取出一次,每次可以有 \(sz_i\) 种方案选择其中的一个小点的,所以还需要乘以 \(\prod sz_i\)

总的方案数是 \(n^{t-2}\prod sz_i\)

技巧性杂题

  1. 树上问题可以转化为数点
  2. 树上差分

P5666 [CSP-S2019] 树的重心

反向考虑贡献,即考虑每个点作为重心的出现次数。
利用重心的数字意义数点。小技巧:这里用重心作为根,统计方便。
一条边去掉可能使得某个点 \(x\) 变成重心,当且仅当去掉的边不在该子树内,且该边去掉后形成的另一个树大小 \(S\) 满足 \(n-2s_x \le S \le n-2g_x\) 即可,其中 \(s_x\)\(x\) 子树大小,\(g_x\)\(x\) 子树最大的儿子节点数。对于每个点树状数组数点可以解决。这里有一些小细节。

  1. 如果去除子树内边的影响?重新开一个树状数组 \(c_2\) 动态计算,然后进入 \(u\) 前后的差就是子树需要减去的贡献。
  2. 去掉某边之后的 \(S\)\(n-s_x\) 还是 \(s_x\)?这时候需要如果边连着 \(u\) 那么进入 \(u\) 之后,边对应的 \(S\)\(n-s_x\),如果边与目前统计的点在不同子树内就是 \(s_x\)。我们发现大部分情况都是 \(s_x\),于是提前给树状数组 \(c_1\) 赋值 \(s_x\),然后进入 \(x\) 之后再修改为 \(n-s_x\)
  3. 如果处理根节点?只需要满足 \(s_{maxson} \le n-S\) 就行了,如果选取边在 \(maxson\) 之中,那么应该选次大与其比较。

P1600 [NOIP2016 提高组] 天天爱跑步

将观察者的询问挂在树上。

利用树上差分进行加减,同时为了统计出的个数为该子树内所产生的,可以在累加全局桶进入该子树前后的差。

长链剖分

链剖分规定:直链,链底端为叶子节点,每个点恰好包含于一条链中。

根据最深子树/最大子树分为长链剖分和重链剖分。

从根到叶子,经过的轻边个数 \(\sqrt n\) 个。

可以用于处理深度相关 DP,

P10641 BZOJ3252 攻略

长链剖分经典应用,选择树上长度极值的路径。

一条长链就是从叶子上来的一个条路径,我们选前 \(k\) 大长链即可。

P5903 【模板】树上 K 级祖先

对于树进行长链剖分,记录每个点所在链的链顶和深度。

进行树上倍增预处理 \(2^k\) 级祖先,对于每条链的链顶,预处理其长度个数个孙子和祖先。

对于 \(k\) 级祖先,先求出 \(k\) 的二进制最高位 \(z\),跳 \(2^z\) 步之后,再跳到链顶,根据需要跳孙子或者祖先。因为上面已经预处理过了,所以可以 \(O(1)\) 求。而且 \(k\) 肯定是小于这条链的长度,因为长链为子树内的最长链。

特判 \(k=0\),这个时候求二进制最高位会出问题。

CF1009F Dominant Indices

长链剖分优化 DP。

\(f_{i,j}\) 表示 \(i\) 子树内深度为 \(j\) 的点的个数,暴力合并是 \(O(n^2)\) 的。

考虑类似于 DSU ON TREE 的思路,继承重儿子的数组信息。轻儿子暴力合并,这样子每条长链只被合并了一次,时间复杂度是 \(O(n)\) 的。

可以用 std::vector 来实现,但是我们只能在末尾添加元素,不符合每次合并下标加一的要求,于是我们倒置 DP 数组,越靠后的位置代表的深度越小,这样子每次只要在末尾添加元素就行了。

树上启发式合并

很多时候都是对于每个树上每个节点及其子树求答案。
流程:计算轻儿子自己的答案(算完之后清楚),计算重儿子自己的答案(并保存中途数据),遍历轻儿子加入数据统计中,将自己加入数据统计,统计该点答案,如果该点是自己父亲的轻儿子就撤销统计。

CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

将每种颜色出现次数的奇偶性为二进制值,记 \(val_u\) 表示从 \(u\) 到根的异或和,树上差分思想,lca 处的贡献抵消,于是一条路径的贡献就是端点的异或和。

由于要求的是深度最大,于是我们维护 \(Mx_v\) 表示异或和为 \(v\) 的最大深度。按照 dsu on tree 的套路维护即可。

posted @ 2024-11-23 12:36  Mirasycle  阅读(51)  评论(1)    收藏  举报