跳转至

树论-树链剖分

树链剖分是一种用于处理树上路径查询的算法,它将树分解为多条链,使得任意两点间的路径可以表示为若干段链的组合。

树链剖分分为 重链剖分长链剖分

重链剖分

定义

重儿子:其儿子中子树大小最大的儿子。如有多个子树大小最大的儿子,只取一个儿子作为重儿子。

轻儿子:其儿子中不是重儿子的儿子。

重边:重儿子与其父亲的边

轻边:轻儿子与其父亲的边

重链:许多条首尾相接的重边

过程

\(f_x\)\(x\) 的父亲

\(dep_x\)\(x\) 的深度

\(si_x\) : 以 \(x\) 为根的子树大小

\(son_x\)\(x\) 的重儿子

第一次dfs处理出来以上信息


\(dfn_x\)\(x\) 的dfs序

\(idfn_x\) : dfs序为 \(x\) 的节点编号

\(top_x\)\(x\) 所在的重链的顶部节点(深度最小的点)

第二次dfs处理出来以上信息

代码

第一次dfs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void dfs(int s,int fa) {
    f[s]=fa,dep[s]=dep[fa]+1,si[s]=1;
    for(int i=0; i<v[s].size(); i++) {
        int y=v[s][i];
        if(y==fa)continue;
        dfs(y,s);
        si[s]+=si[y];
        if(si[y]>si[son[s]])son[s]=y;
    }
}
第二次dfs
1
2
3
4
5
6
7
8
9
void hld(int s,int fa,int topp) {
    dfn[s]=++dfscnt,idfn[dfscnt]=s,top[s]=topp;
    if(son[s])hld(son[s],s,topp);
    for(int i=0; i<v[s].size(); i++) {
        int y=v[s][i];
        if(y==fa||y==son[s])continue;
        hld(y,s,y);
    }
}

性质

1.一棵子树内的dfs序是连续的。

2.整棵树的根节点到树内任意一点的路径可以被拆分成不超过 \(O(\log n)\) 条重链。

3.重链上的dfs序是连续的。

应用

维护LCA(最近公共祖先)

两个点不断向上跳重链,当两个点跳到同一条重链上时,深度较小的点为LCA。

代码
1
2
3
4
5
6
7
int lca(int x,int y) {
    while(top[x]!=top[y]) {
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        x=f[top[x]];
    }
    return dep[x]<dep[y]?x:y;
}

维护点权信息

维护子树的信息

根据性质1可知,任意一棵子树内的dfs序是连续的,设 \(x\) 为这棵子树的根,那么 \(dfn_x\)\(dfn_x+si_x-1\) 这段区间就是子树所有节点的dfs序。

可以利用数据结构来维护区间信息。

维护路径的信息

可以把这条路径 \((x,y)\) 拆开,拆成路径 \((lca(x,y),x)\) 与路径 \((lca(x,y),y)\)

也就是这两个点不断向上跳重链,根据性质3可知,重链上的dfs序是连续的,跳重链的过程可以转化成区间问题。

当两个点跳到同一个重链上时,根据性质3可知,在同一个重链上时可以转化成区间问题。

可以利用数据结构来维护区间信息。

代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int sum(int x,int y) {
    int res=0;
    while(top[x]!=top[y]) {
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        //处理 dfn[top[x]] 到 dfn[x] 的区间信息
        x=f[top[x]];
    }
    if(dep[x]<dep[y])swap(x,y);
    //处理 dfn[y] 到 dfn[x] 的区间信息
    return res;
}

维护边权信息

每个节点的父亲只有一个,所以将父亲到儿子的边权下放到儿子上成为儿子的点权。

根据性质-dfs序是连续的,那么将上面的维护点权信息更改一下就行了。

例题

P3379 【模板】最近公共祖先(LCA)

方法之一:树剖lca

P3384 【模板】重链剖分/树链剖分

模版

题单

P7746 [COCI2011-2012#3] PLAĆE

点权板子

P3038 [USACO11DEC] Grass Planting G

边权板子

P2146 [NOI2015] 软件包管理器

点权板子

P3833 [SHOI2012] 魔法树

点权板子

P4114 Qtree1

边权板子

P2590 [ZJOI2008] 树的统计

点权板子

P4315 月下“毛景树”

点权板子

P3178 [HAOI2015] 树上操作

点权板子

P1505 [国家集训队] 旅游

线段树维护边权

P4116 Qtree3

线段树维护最小dfs序

P4092 [HEOI2016/TJOI2016] 树

线段树维护最大dfs序

P2486 [SDOI2011] 染色

线段树维护区间两个端点颜色以及区间颜色段数。

合并区间时注意贡献。

统计答案时注意贡献。

长链剖分

定义

重儿子:其儿子中子树深度最大的儿子。如有多个子树深度最大的儿子,只取一个儿子作为重儿子。

轻儿子:其儿子中不是重儿子的儿子。

重边:重儿子与其父亲的边

轻边:轻儿子与其父亲的边

重链:许多条首尾相接的重边

性质

1.任意一点 \(x\)\(k\) 级祖先 \(y\) 所在的长链长度大于等于 \(k\)

2.整棵树的根节点到树内任意一点的路径可以被拆分成不超过 \(O(\sqrt n)\) 条重链。

应用

维护树上 K 级祖先(LA)

时间复杂度为 \(O(n \log n)\)\(O(1)\)

预处理

1.每个点所在链的顶点和深度

2.利用树上倍增预处理出每个点的 \(2^n\) 级祖先。

3.对于每条链,其长度为 \(len\) ,预处理出这条链的顶点向上的 \(len\) 个祖先和向下的 \(len\) 个儿子。

4.预处理出 \(1\)\(n\) 在二进制下的最高位 \(g_i\)

查询

1.将询问的节点 \(x\) 跳到 \(x\)\(2^{g_i}\) 级祖先。

2.剩下还有 \(k-2^{g_i}\) 级,可以发现 \(k-2^{g_i}<2^{g_i}\) ,因为此时 \(x\) 所在的长链长度一定 \(\geq2^{g_i}\) ,那么此时 \(x\) 可以先跳到 \(x\) 所在链的顶点。

3.如果还没跳到,那么可以用预处理3中的数组继续向上跳;如果跳过去了,那么可以用预处理3中的数组向下跳。

优化DP

例题

P5903 【模板】树上 K 级祖先

板子

题单

P5904 [POI2014] HOT-Hotels 加强版

长链剖分优化DP

未完待续!