Loading

【学习笔记】长链剖分

Page Views Count

概述

在常规树链剖分中把重儿子设成 \(siz\) 最大的儿子,这样从根跳重链时子树大小至少减半,因此只需要 \(O(\log n)\) 次即可到达任何节点。

考虑把关键字由 \(siz\) 改成子树内最大的深度 \(dep\),这样的剖分方法称为长链剖分。

void dfs1(int u,int fa,int d){
    dep[u]=d,mxdep[u]=dep[u];
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue;
        dfs1(v,u,d+1);
        if(mxdep[v]>mxdep[u]) mxdep[u]=mxdep[v],son[u]=v;
    }
}

长链剖分具有一些性质:

  • 从一个节点跳向链顶的父亲,所在的链长度一定增加,证明显然。

  • 从一个节点跳链顶,\(O(\sqrt{n})\) 次可以到达树根,结合上一性质可证,最坏情况是 \(1+2+\cdots+\sqrt{n}\)。但这个性质不常用。

优化 DP

大致思想

当 DP 的状态形如 \(f_{u,d}\),只与子树 \(u\) 和子树内到 \(u\) 的距离 \(d\) 有关时,可以考虑长链剖分优化至 \(O(n)\)

具体方法类似树上启发式合并,每次继承重儿子信息,轻儿子暴力合并。

复杂度证明:短链向长链合并时,长链长度一定不会增加,因此相当于把短链上的信息直接删去了。而一条链只会合并一次,每个节点只出现在一条链上,因此合并复杂度是 \(O(n)\) 的。

具体实现

难点在于如何继承重儿子的信息。

在绝大多数题目中,是一个 \(f_{v,d}\to f_{u,d+1}\) 的过程,也就是由父亲到儿子,距离增加 \(1\)。可以使用指针实现,先 DFS 预处理,对于根和每个轻儿子开子树内最大距离大小的空间,对于重儿子将指针设为父亲的指针位置加 \(1\)

void dfs2(int u,int f){
    if(u==1){
        dp[u]=p,p+=mxdep[u]-dep[u]+1;
    }
    if(!son[u]) return;
    dp[son[u]]=dp[u]+1;
    dfs2(son[u],u);
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==f||v==son[u]) continue;
        dp[v]=p,p+=mxdep[v]-dep[v]+1; 
        dfs2(v,u);
    }
}

在一般情况下,每个数组大小就是 \(n\)

也可以用 vector 实现,但是每次加入距离更小的,实际上是倒着存储,需要特判一些边界等等,且常数较大,不推荐。

在正式 DP 时,要利用好只能枚举短链上节点到 \(u\) 的距离。对每棵子树单独计数的题目,直接继承即可,且对于最值或求和之类的题目,会影响到的只有短链的范畴;对于多棵子树合并计数的题目,注意到一定有短链存在,因此还是以枚举短链上距离为依托。

例题

CodeForces-1009F Dominant Indices *2300

朴素的统计子树内距离对应节点个数。

重儿子可以直接继承,轻儿子暴力合并即可。

Luogu-P5904 POI 2014 HOT-Hotels 加强版

这类的三元组有两种:\(\mathrm{LCA}\) 是或不是重心的。

先考虑朴素 DP 怎么做,我们希望问题在子树内解决,所以要在 \(\mathrm{LCA}\) 处统计答案。

对于第一种情况,设 \(f_{u,d}\)\(u\) 子树内距离为 \(d\) 的节点个数,\(g_{u,d}\)\(u\) 子树内到 \(u\) 距离均为 \(d\)\(\mathrm{LCA}\)\(u\) 的点对数。

枚举每棵子树以及距离,统计答案的过程:

\[g_{u,d+1}\times f_{v,d}\to \mathrm{ans} \]

\[f_{u,d+1}\times f_{v,d}\to g_{u,d+1} \]

\[f_{v,d}\to f_{u,d+1} \]

对于第二种情况,设目标三元组 \((u,v,w)\),有三部分组成:\(\mathrm{LCA}(u,v)\)\(u,v\) 距离相等,均为 \(d_1\)\(\mathrm{LCA}(u,v,w)\)\(w\) 的距离 \(d_2\) 与到 \(\mathrm{LCA}(u,v)\) 的距离 \(d_3\) 无边集交且满足 \(d_1=d_2+d_3\)

因为在 \(\mathrm{LCA}(u,v,w)\) 处统计答案,发现 \(d_3=d_1-d_2\),因此 \(d_1-d_2\) 相等的位置本质没有区别,设 \(h_{u,d}\)\(u\) 子树内满足这样情况且 \(d_1-d_2=d\) 的点对数,那么转移也类似上面了。

考虑怎么优化。

\(g\) 是只对一棵子树有效的,不需要继承,\(f\) 比较朴素的继承即可,而 \(h\) 比较不同,考虑到 \(v\)\(u\) 后,\(d_1\) 不变而 \(d_2\) 增加 \(1\),因此继承是形如 \(h_{v,d}\to h_{u,d-1}\),与正常的继承不同。这就需要我们预处理指针时,把重儿子的指针设为父亲的指针减 \(1\),从而空间要开 \(2\) 倍。

统计答案也需要精细处理,一个简单的想法是先计算轻子树之间的答案,这部分照搬暴力即可,同时要用一个临时数组记录一下当前统计到的 \(f,g,h\)。计算完之后,可以和重儿子继承来的 \(f,h\) 再合并得到另一部分的答案。

Luogu-P3899 湖南集训 更为厉害

容易发现 \(a,b,c\) 在一条链上,在 \(a\) 处计算答案,讨论 \(b\) 的位置。

如果 \(b\)\(a\) 的上方,那么 \(b,c\) 相当于独立,方案数是 \(\max(dep_a,k)\times (siz_a-1)\)

如果 \(b\)\(a\) 的下方,那么 \(b\) 的个数实际上与 \(c\)\(a\) 的距离有关,具体是 \(\sum_{c\in \mathrm{son}(a)}\min(dep_c-dep_a-1,k)\),即 \((a,c)\) 路径上点的个数和可以取的点的个数的较小值。

第一种情况可以随便计算,第二种情况是要求一个加权和,因此我们考虑如何对于每个子树内距离 \(d\)\(\sum d-1\)。朴素设 \(f_{u,d}\) 为个数,\(g_{u,d}\) 为加权和,继承时 \(f\) 正常,由于 \(d\) 的增加,\(g_{u,d+1}\) 实际上是由 \(df_{v,d}=(d-1)f_{v,d}+f_{v,d}=g_{v,d}+f_{v,d}\) 贡献来的,这样不能直接继承。

尝试把 \(d\) 拆成可以直接继承的类型,那就改成 \(dep_v-dep_u\),这样对于不同的 \(u\),加权和变成了 \(dep_v\times f_{u,d}-dep_u\times f_{u,d}\),后者系数对于 \(u\) 而言是常量,前者在 \(v\)\(u\) 的过程中系数不发生改变,可以直接继承。

容易发现可以后缀和优化。

其实第二种情况可以选择对每个 \(b\) 计数而不是对每个 \(c\) 计数,这样就是求深度范围内的所有节点子树大小,主席树二维数点。

总结

长链剖分优化 DP 大致有以下技巧:

  • 答案由两棵子树贡献得到,先暴力计算轻子树之间的,再统一算所有轻子树和重子树之间的。

  • 在需要求和优化时,采用后缀和而不是前缀和,因为前者不需要修改长链上深度较大节点的值,保证了复杂度。

  • 修改状态定义为便于继承的结果,多数采用了 \(d=dep_v-dep_u\) 的方法。

参考资料

posted @ 2023-04-22 22:11  SoyTony  阅读(129)  评论(0编辑  收藏  举报