链剖分简记

同步于 cnblog

大概是去年过年接触树剖的吧,写一写见到过的一些东西。

重链剖分

考虑一种划分树形式,我们将一棵树划分为许多条链,每个点链上的后继称为重儿子,是这个点子树最大的儿子,反之其余所有儿子称为轻儿子,连接的边称为轻边

这样做就能够带来许多性质,比如从一个叶子向上走最多只会走至多 \(O(\log n)\) 条轻边,因为发现只能被完全二叉树卡满,而此时深度为 \(O(\log n)\)

LCA

应用上述性质,我们就可以完成求 \(LCA\) 的操作,且时空常数较小,具体来讲,在 \(x,y\) 两点处于相同链时,取深度较小者即为答案,反之选链深度较大者向上跳一跳链即可。

一部分代码实现,树剖预处理及 \(LCA\) 操作。

inline void dfs1(ll x,ll father,ll depth){
    dep[x]=depth,fa[x]=father,siz[x]=1;ll zson=-1;
    for(ll y:g[x]){
        if(y==fa[x]) continue;
        dfs1(y,x,depth+1);siz[x]+=siz[y];
        if(siz[y]>zson) zson=siz[y],son[x]=y;
    }
}
inline void dfs2(ll x,ll nowtop){
    top[x]=nowtop,id[x]=++cnt,w[cnt]=x;
    if(!son[x]) return rb[x]=cnt,void();
    dfs2(son[x],nowtop);
    for(ll y:g[x]){
        if(y==fa[x]||y==son[x]) continue;
        dfs2(y,y);
    }
    rb[x]=cnt; 
}
inline ll LCA(ll x,ll y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	return y;
}

再进一步的,如果已知 \(x\) 及其祖先 \(y\),如何找到 \(y\) 的儿子 \(z\),同时也是 \(x\) 的祖先。

很简单,由于 \(x\) 跳链时可能直接跳到 \(y\) 上,因此记录 \(lst\) 表示 \(x\) 上一个经过的链头,如果直接跳到 \(y\) 了,返回 \(lst\) 即可,否则返回 \(y\) 的后继。

inline ll LCApre(ll x,ll y)
{
	ll lst=x;
	while(top[x]^top[y]) dep[top[x]]<dep[top[y]]?(lst=top[y],y=fa[top[y]]):(lst=top[x],x=fa[top[x]]);
	return x==y?lst:w[id[y]+1];
}

树剖 + ds

一棵树,支持链加,子树加,链查,子树查。

以本题为例,如果把这几个操作放到序列上,那线段树直接搞就好了,但这是在树上,点的标号并不连续。

因此,我们考虑树剖,将一棵树划分的若干条重链,并把每一条重链的点重编号为连续的,就可以对拆分出来的区间直接使用线段树了。

由之前提到的性质,我们最多只会操作 \(\log n\) 个区间,因此复杂度是 \(O(n \log^2 n)\)

record

链上有序信息合并

例题 P10773 [NOISG 2021 Qualification] Truck,GSS7 等。

一些信息可以直接在数据结构上维护的,但是在合并时区分正反方向,因此我们需要同时记录向左向右走的两个答案,在合并链答案的时候也要注意合并顺序。

例题的合并代码,可以当做一个参考:

inline ll ask_chain(ll x,ll y)
{
	SGT ans1={0,0,0,0,0,0},ans2={0,0,0,0,0,0};ll fl=0;
	while(top[x]^top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y),swap(ans1,ans2),fl^=1;
		merge(ans1,ask(1,id[top[x]],id[x]),ans1);
		x=fa[top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y),swap(ans1,ans2),fl^=1;
	merge(ans1,ask(1,id[y]+1,id[x]),ans1);
	if(fl) swap(x,y),swap(ans1,ans2);
	ll ans=0;ad(ans,ans1.val2,ans2.val1,ans1.sumt*ans2.sumd%mod,G*(ans1.sumd+ans2.sumd)%mod);return ans;
}

均摊路径答案

例题 P4211 [LNOI2014] LCA

对于类似求 \(dep\)\(dis\) 的东西,总是会习惯性的认为拆式子,前缀和改式子后维护数据结构,但往往这样的统法在某些题并不奏效。

考虑例题,我们发现,对于求 \(dep\),可以将其改为 \(lca\) 到根的路径权值和,因此,可以将问题改为为路径加,路径查询的 ds 问题,树剖扫描线直接做就好了。

边权转点权

小 trick,发现有些题是询问边权的,但我们并不好处理边的询问,因此,我们将边权下放,转化为点权来做。

但相应的,在链改时最后跳到相同链的修改需要特别处理 \(lca\) 的问题。

示例代码:

inline ll upd(ll x,ll y,ll k)
{
	while(top[x]^top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		modify(id[top[x]],id[x],k);
		x=fa[top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	modify(id[y]+1,id[x],k);
}

链上二分

最典的东西就是求 \(k\) 级祖先,复杂度为 \(O(n\log n)\),理论上劣于长剖。

由于链编号连续,所以只需要找到答案可能所在的链,然后在链上二分寻找答案即可,细节较多,单调性容易分析错误。

树剖换根

例题 P3979 遥远的国度

由于树剖信息是预处理的,所以不好支持更换根的操作。

在例题中,链修改操作与根无关,而子树查询操作可以通过讨论新根与查询点的关系完成。

具体来讲,令新根为 \(t\),查询点为 \(x\),原根子树下的编号为 \([l,r]\),则:

  1. \(t\)\(x\) 的祖先,查询区间仍是 \([l,r]\)
  2. \(t\)\(x\),则查询 \([1,n]\)
  3. \(t\)\(x\) 的孩子,查询 \([1,l) \cup (r,n]\)

record

ddp

一棵树,支持单点修改,查询最大独立集。

静态的 dp 转移方程是简单的,考虑单点修改只会带来链的修改,由于每个点经过轻边只有 \(\log n\) 条,所以我们对每一个点分别记录只选重儿子和可选轻儿子的答案,用 ds 维护每个链的矩阵,每次只需要对链尾做修改,其余转移是固定的,因此每次只会修改 \(O(\log n)\) 条链,复杂度 \(O(n \log^2 n)\),用全局平衡二叉树可以做到 \(O(n \log n)\)

其中只维护重儿子使得修改次数降低的 trick 是经典的。

record

离线图转树

例题 P2542 [AHOI2005] 航线规划

我们暂时不会 LCT,因此不会动态维护某些东西,但是我们可以正难则反。

考虑建出最后的图,并把其转化一棵树,将多余边转化为树上路径修改,然后就变成简单路径查询了。

record

图上多余边转化为链改

跟上一个挺像的,例题 P4180 [BJWC2010] 严格次小生成树

考虑先建出 MST,然后尝试加入新边,其实就是比较边权与路径权值,就能直接求出答案了,可以看做是将多余边转化为树上路径修改查询的 trick。

record

长链剖分

树上 \(k\) 级祖先

在重剖中,我们是有 \(O(n) - O(\log n)\) 查询 \(k\) 级祖先的办法的,但这在 \(n,q\) 不同阶时还是会很慢,那么我们考虑一种更快的做法。

朴素的重剖或许可以通过分块等技巧进一步优化,但复杂度仍然不优,我们从定义重新下手。

我们将重儿子的定义改为 子树深度最大 的儿子,或者说称其为 长儿子,然后我们再预处理树上倍增,就能发现一个性质。

对于任意一个点的 \(k\) 级祖先,其所在长链的长度 \(\geq k\),证明很简单,因为最坏情况其祖先的长链也是延伸至这个点的。考虑有了这个性质,我们只需要对每一条长链链头记录向上向下 \(len\) 级祖先或儿子,就可以做到每次 \(O(1)\) 查询 \(k\) 级祖先了,所以这样做复杂度是 \(O(n\log n) - O(1)\) 的,当然也有更优秀的 \(O(n) - O(1)\) 的高科技做法。

record

优化 dp

鸽鸽鸽鸽

posted @ 2025-05-21 00:02  Wei_Han  阅读(16)  评论(0)    收藏  举报