长链剖分学习笔记

长链剖分学习笔记

简介

长链剖分也是一种树链剖分,平时我们说树链剖分,一般都是直接默认为轻重链剖分。
轻重链剖分的优秀性质在于从任意一个点开始,向上跳跃,跳过的重链数量不会超过\(log\)级别。
这样子可以很优秀的解决两点之间链的问题。
对于解决一些子树的信息问题,我们可以用\(dsu\ on\ tree\)的思路,保证了每个点向上修改的次数不超过\(log\)次,也可以很方便的解决一些问题。而长链剖分则是通过修改剖分链的方式,通过维护一些信息,可以在更有优秀的时间中解决一部分问题。
长链剖分十分类似于轻重链剖分,但是我们稍加修改,将每次选择子树大小最大的儿子作为重儿子变成了选择子树深度最大的那个儿子作为重儿子。然后将所有点和它的重儿子之间的边认为是重边,如果我们把他们在树中全部加粗,那么原树就被分割成了若干条链。因为很多东西都和轻重链剖分是相同的,所以这一部分我写的很简单。甚至于连两者之间的代码都是非常的相似的。

void dfs1(int u,int ff)
{
    md[u]=dep[u]=dep[ff]+1;fa[u]=ff;
    for(int i=h[u];i;i=e[i].next)
    {
        int v=e[i].v;if(v==ff)continue;
        dfs1(v,u);
        if(md[v]>md[hson[u]])hson[u]=v,md[u]=md[v];
    }
}
void dfs2(int u,int tp)
{
    top[u]=tp;len[u]=md[u]-dep[top[u]]+1;
    if(hson[u])dfs2(hson[u],tp);
    for(int i=h[u];i;i=e[i].next)
        if(e[i].v!=fa[u]&&e[i].v!=hson[u])
            dfs2(e[i].v,e[i].v);
}

稍微介绍一下每个数组表示的含义分别是什么。
\(dep\)是深度,\(fa\)是父亲节点,\(md\)\(maxdep\)也就是子树中的最大深度
\(hson\)是重儿子,\(top\)是这条重链深度最小的点,也就是重链的顶点,\(len\)是重链的长度。
代码应该还是比较清楚地,所以就不在过多的解释了。
如果没有学过轻重链剖分,可以先去学习一下,大概直接百度树链剖分就好了。

简单的性质

性质不多,长链剖分的重点在于题目。但是所有题目的复杂度都和性质相关。

性质一

所有链长度的和是\(O(n)\)级别的。

证明:

所有点在且仅在一条重链之中,永远只会被计算一次,因为链长的总和是\(O(n)\)级别的。

性质二

任意一个点的\(k\)次祖先\(y\)所在的长链的长度大于等于\(k\)

证明:
假如\(y\)所在的长链的长度小于\(k\),那么它所在的链一定不是重链,因为\(y-x\) 这条链显然更优,那么\(y\)所在的重链长度至少为\(k\),性质成立。
否则\(y\)所以在长链长度大于等于\(k\),性质成立。

性质三

任何一个点向上跳跃重链的次数不会超过\(\sqrt n\)

证明:
如果一个点\(x\)从一条重链跳到了另外一条重链上,那么跳跃到的这条重链的长度不会小于之前的重链长度。
那么在最坏的情况下,重链长度分别为\(1,2,3,...,\sqrt n\),也就是最多跳跃\(\sqrt n\)次。
从这点上就可以看出,如果用长链剖分来解决两点之间的链以及\(LCA\)问题,复杂度是不优于树链剖分的。

一些应用

一、\(O(nlogn)-O(1)\)计算\(k\)次祖先

\(k\)次祖先我们有几种方法,可以树链剖分之后跳重链,这样是\(O(n)-O(logn)\)
还可以提前预处理好倍增数组,这样是\(O(nlogn)-O(logn)\)的。
我之前还自己\(yy\)了一种辣鸡做法,先树链剖分再倍增,似乎可以做到\(O(nlogn)-O(loglogn)\)
还可以对于每个点,暴力维护\([1,\sqrt n]\)次祖先,这样子是\(O(n\sqrt n)-O(\sqrt n)\)
当然,如果支持离线的话,可以做到\(O(n+q)\),只需要\(dfs\)的时候维护一个栈就好了。
然而这些都不够优秀,我们来考虑一下长链剖分的性质,看能否优化上述东西。

我们再看看\(O(n\sqrt n)-O(\sqrt n)\)的做法,它可以结合倍增,这样的话,它的本质就是优化掉了很小的几个二进制位。再回头看看上面的第二条性质:\(k\)次祖先所在的重链长度不小于\(k\)。利用这个性质,我们就可以得到一个很秒的方法了。我们把\(k\)折半,假设是\(r\),不难发现\(r\)次祖先所在的重链长度不短于\(r\)。如果我们提前维护出每条重链从上往下的每一个点,以及对于每个重链的顶端,维护它的重链长度个祖先,这样子对于每次找到\(r\)次祖先之后,我们就可以通过一些判断,找到\(k\)次祖先的位置,而最后这一次寻找不难发现可以利用上面预处理出来的重链和祖先\(O(1)\)的计算。那么现在的复杂度瓶颈又回到了找\(r\)次祖先。我们直接找\(r\)次祖先真的优秀吗?如果利用倍增寻找的话,仍然是\(O(logn)\)的复杂度。但是我们发现\(r\)只需要满足\(r>k/2\)就可以向上面那么做了,因此我们令\(r\)\(k\)的最高二进制位,也就是\(r=highbit(k)\),这样子就可以倍增预处理出来\(r\)倍祖先,然后\(O(1)\)找到\(k\)次祖先了。

补充一些复杂度以及空间的细节:根据性质一,对于每个重链的顶点,维护整条链以及它小于链长次祖先,因为总的点数是\(O(n)\)级别的,所以这里的时空负载度都是\(O(n)\)\(highbit\)显然可以\(O(n)\)预处理,这样子时空复杂度还是\(O(n)\)。倍增的时空复杂度是\(O(nlogn)\),这个方法的复杂度瓶颈在此。

综上所述,我们得到了一个预处理\(O(nlogn)\),单次询问\(O(1)\)的方法。

代码&题目链接

二、快速计算可合并的以深度为下标的子树信息

这个思路十分类似于\(dsu\ on\ tree\),我们长链剖分之后,每次不重新计算,全部继承重儿子的值,然后再把其他的所有轻儿子的贡献额外的算进来。

直接这样子说十分的不清晰,我们找到题目来说。

BZOJ4543 Hotel加强版

题目&代码链接

因为会在这里写比较详细的题解,所以上面写得很\(Simple\)
我们先考虑一个\(O(n^2)\)\(dp\),也就是原题的做法。
我们考虑一下,三个点两两的距离相同是什么情况,

1.存在一个三个点公共的\(LCA\),所以我们在\(LCA\)统计答案即可。

2.存在一个点,使得这个点到另外两个子树中距离它为\(d\)的点以及这个点的\(d\)次祖先。

所以,设\(f[i][j]\)表示以\(i\)为根的子树中,距离当前点为\(j\)的点数。
\(g[i][j]\)表示以\(i\)为根的子树中,两个点到\(LCA\)的距离为\(d\),并且他们的\(LCA\)\(i\)的距离为\(d-j\)的点对数。

考虑合并的时候的转移:
\(ans+=g[i][0],ans+=g[i][j]*f[son][j-1],f[i][j]+=f[son][j-1],g[i][j]+=g[son][j+1]\)
转移的正确性比较显然,不在多讲了,并不是这里的重点。
这样子的复杂度是\(O(n^2)\)的。

我们观察一下转移的时候有这样两步:\(f[i][j]+=f[son][j-1],g[i][j]+=g[son][j+1]\)
如果我们钦定一个儿子的话,那么这个数组是可以直接赋值的,并不需要再重复计算。
所以我们用指针来写,也就是:\(f[i]=f[son]-1,g[i]=g[son]+1\)
如果整棵树是链我们发现复杂度可以做到\(O(n)\),既然如此,我们推广到树。
我们进行长链剖分,每次钦定从重儿子直接转移,那么我们还需要从轻儿子进行转移。
不难证明所有轻儿子都是一条重链的顶部,转移时的复杂度是重链长度。
那么,复杂度拆分成两个部分:直接从重儿子转移\(O(1)\),从轻儿子转移\(O(\sum len)\)
发现每个点有且仅有一个父亲,因此一条重链算且仅被一个点暴力转移,而每次转移复杂度是链长。
所以全局复杂度是\(\sum\)链长,也就是\(O(n)\),因此总复杂度就是\(O(n)\)

这样子写下来,发现长链剖分之后,我们的复杂度变为了线性。
但是注意到复杂度证明中的一点:转移和链长相关。
而链长和什么相关呢?深度。所以说对于这一类与深度相关的、可以快速合并的信息,使用长链剖分可以优化到一个非常完美的复杂度。如果需要维护的与深度无关的信息的话,或许\(dsu\ on\ tree\)是一个更好的选择。

这里再额外补充一道题目,似乎都比较简单:(尽然找不到别的题目了)

【BZOJ3653】谈笑风生

【CF1009F】Dominant Indices

【COGS2652】秘术「天文密葬法」

题解啥的直接戳链接吧。

三、维护一些奇怪的贪心

BZOJ3252 攻略

题面&题解&代码

这题比较神仙,对于长链剖分的运用也是很妙的,思路可以借鉴一下。

这里不再过细的讨论这部分内容。

Ending

根据题目数量的分布,也不难看出长链剖分我们用的最多的仍然是维护和深度相关的信息。因为求\(k\)次祖先预处理的复杂度达到了\(O(nlogn)\),成为了复杂度瓶颈,只有当询问次数非常大的时候,才能体现出长链剖分\(O(1)\)回答的优越性。同理,维护一类贪心题,似乎题目也比较固定,并且可以用别的方法解决。只有在维护和深度相关的可以快速合并的信息的时候,时间复杂度可以做到\(O(n)\)级别,而包括轻重链剖分在内的一些算法一般都只能做到\(O(nlogn)\)级别,利用长链剖分可以做到更大的数据范围。

咕咕咕,这篇文章就到这里了,主要是\(yyb\)太弱了,难题写不动,就只能写写这些比较简答的题目。

posted @ 2018-08-15 07:58 小蒟蒻yyb 阅读(...) 评论(...) 编辑 收藏