树链剖分学习笔记
树链剖分的思想及能解决的问题
树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。
具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。
树链剖分(树剖/链剖)有多种形式,如 重链剖分,长链剖分 和用于 Link/cut Tree 的剖分(有时被称作「实链剖分」),大多数情况下(没有特别说明时),「树链剖分」都指「重链剖分」。
重链剖分可以将树上的任意一条路径划分成不超过 $O(\log n)$ 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。
重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。
如:
- 修改 树上两点之间的路径上 所有点的值。
- 查询 树上两点之间的路径上 节点权值的 和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)。
除了配合数据结构来维护树上路径信息,树剖还可以用来 $O(\log n)$(且常数较小)地求 LCA。在某些题目中,还可以利用其性质来灵活地运用树剖。
重链剖分
我们给出一些定义:
定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
定义 轻子节点 表示剩余的所有子结点。
从这个结点到重子节点的边为 重边。
到其他轻子节点的边为 轻边。
若干条首尾衔接的重边构成 重链。
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。
如图:
实现
树剖的实现分两个 DFS 的过程。伪代码如下:
第一个 DFS 记录每个结点的父节点(father)、深度(deep)、子树大小(size)、重子节点(hson)。
$$
\begin{array}{l} \text{TREE-BUILD }(u,dep) \ \begin{array}{ll} 1 & u.hson\gets 0 \ 2 & u.hson.size\gets 0 \ 3 & u.deep\gets dep \ 4 & u.size\gets 1 \ 5 & \textbf{for }\text{each }u\text{'s son }v \ 6 & \qquad u.size\gets u.size + \text{TREE-BUILD }(v,dep+1) \ 7 & \qquad v.father\gets u \ 8 & \qquad \textbf{if }v.size> u.hson.size \ 9 & \qquad \qquad u.hson\gets v \ 10 & \textbf{return } u.size \end{array} \end{array}
$$
第二个 DFS 记录所在链的链顶(top,应初始化为结点本身)、重边优先遍历时的 DFS 序(dfn)、DFS 序对应的节点编号(rank)。
$$
\begin{array}{l} \text{TREE-DECOMPOSITION }(u,top) \ \begin{array}{ll} 1 & u.top\gets top \ 2 & tot\gets tot+1\ 3 & u.dfn\gets tot \ 4 & rank(tot)\gets u \ 5 & \textbf{if }u.hson\text{ is not }0 \ 6 & \qquad \text{TREE-DECOMPOSITION }(u.hson,top) \ 7 & \qquad \textbf{for }\text{each }u\text{'s son }v \ 8 & \qquad \qquad \textbf{if }v\text{ is not }u.hson \ 9 & \qquad \qquad \qquad \text{TREE-DECOMPOSITION }(v,v) \end{array} \end{array}
$$
以上为代码实现。
我们先给出一些定义:
- $fa(x)$ 表示节点 $x$ 在树上的父亲。
- $dep(x)$ 表示节点 $x$ 在树上的深度。
- $siz(x)$ 表示节点 $x$ 的子树的节点个数(包括自己)。
- $son(x)$ 表示节点 $x$ 的 重儿子。
- $top(x)$ 表示节点 $x$ 所在 重链 的顶部节点(深度最小)。
- $dfn(x)$ 表示节点 $x$ 的 DFS 序,也是其在线段树中的编号。
- $rnk(x)$ 表示 DFS 序所对应的节点编号,有 $rnk(dfn(x))=x$。
我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 $fa(x)$,$dep(x)$,$siz(x)$,$son(x)$,第二次 DFS 求出 $top(x)$,$dfn(x)$,$rnk(x)$。
/*定义部分*/
ll n, a, b, w[Maxn];
vector<ll> v[Maxn];
ll fa[Maxn], dep[Maxn], siz[Maxn], son[Maxn];
ll top[Maxn], dfncnt=0, dfn[Maxn], rnk[Maxn];
/*定义部分*/
void dfs1(ll x){
son[x]=-1, siz[x]=1; // son为-1表示为叶子节点,siz包括本身
for(ll i=0;i<(ll)v[x].size();i++){
ll tmp=v[x][i];
if(!dep[tmp]){
dep[tmp]=dep[x]+1, fa[tmp]=x;
dfs1(tmp);
siz[x]+=siz[tmp];
son[x]=((son[x]==-1 || siz[tmp]>siz[son[x]]) ? tmp : son[x]); // 求重子节点
}
}
} void dfs2(ll x, ll Top){
top[x]=Top, dfn[x]=++dfncnt; // dfn
rnk[dfncnt]=x;
if(son[x]==-1){ return ; } // 叶子节点
dfs2(son[x], Top); // 更新重链上的点的信息
for(ll i=0;i<(int)v[x].size();i++){
ll tmp=v[x][i];
if(tmp!=son[x] && tmp!=fa[x]){ // 既不为重子节点,也不为父亲节点
dfs2(tmp, tmp); // 新的重链
}
}
}
重链剖分的性质
树上每个节点都属于且仅属于一条重链。
重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)。
所有的重链将整棵树 完全剖分。
在剖分时 重边优先遍历,最后树的 DFS 序上,重链内的 DFS 序是连续的。按 DFN 排序后的序列即为剖分后的链。
一颗子树内的 DFS 序是连续的。
可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。
因此,对于树上的任意一条路径,把它拆分成从 LCA 分别向两边往下走,分别最多走 $O(\log n)$ 次,因此,树上的每条路径都可以被拆分成不超过 $O(\log n)$ 条重链。
常见应用
路径上维护
用树链剖分求树上两点路径权值和,伪代码如下:
$$
\begin{array}{l} \text{TREE-PATH-SUM }(u,v) \ \begin{array}{ll} 1 & tot\gets 0 \ 2 & \textbf{while }u.top\text{ is not }v.top \ 3 & \qquad \textbf{if }u.top.deep< v.top.deep \ 4 & \qquad \qquad \text{SWAP}(u, v) \ 5 & \qquad tot\gets tot + \text{sum of values between }u\text{ and }u.top \ 6 & \qquad u\gets u.top.father \ 7 & tot\gets tot + \text{sum of values between }u\text{ and }v \ 8 & \textbf{return } tot \end{array} \end{array}
$$
链上的 DFS 序是连续的,可以使用线段树、树状数组维护。
每次选择深度较大的链往上跳,直到两点在同一条链上。
同样的跳链结构适用于维护、统计路径上的其他信息。
子树维护
有时会要求,维护子树上的信息,譬如将以 $x$ 为根的子树的所有结点的权值增加 $v$。
在 DFS 搜索的时候,子树中的结点的 DFS 序是连续的。
每一个结点记录 bottom 表示所在子树连续区间末端的结点。
这样就把子树信息转化为连续的一段区间信息。
求最近公共祖先
不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。
向上跳重链时需要先跳所在重链顶端深度较大的那个。
参考代码:
ll LCA(ll x, ll y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]]){
x=fa[x];
} else{
y=fa[y];
}
}
return (dep[x]>dep[y] ? y : x);
}
例题
「ZJOI2008」树的统计
题目大意
对一棵有 $n$ 个节点,节点带权值的静态树,进行三种操作共 $q$ 次:
- 修改单个节点的权值;
- 查询 $u$ 到 $v$ 的路径上的最大权值;
- 查询 $u$ 到 $v$ 的路径上的权值之和。
保证 $ 1\le n\le 30000$,$0\le q\le 200000$。
解法
根据题面以及以上的性质,你的线段树需要维护三种操作:
- 单点修改;
- 区间查询最大值;
- 区间查询和。
单点修改很容易实现。
由于子树的 DFS 序连续(无论是否树剖都是如此),修改一个节点的子树只用修改这一段连续的 DFS 序区间。
问题是如何修改/查询两个节点之间的路径。
考虑我们是如何用 倍增法求解 LCA 的。首先我们 将两个节点提到同一高度,然后将两个节点一起向上跳。对于树链剖分也可以使用这样的思想。
在向上跳的过程中,如果当前节点在重链上,向上跳到重链顶端,如果当前节点不在重链上,向上跳一个节点。如此直到两节点相同。沿途更新/查询区间信息。
对于每个询问,最多经过 $O(\log n)$ 条重链,每条重链上线段树的复杂度为 $O(\log n)$,因此总时间复杂度为 $O(n\log n+q\log^2 n)$。实际上重链个数很难达到 $O(\log n)$(可以用完全二叉树卡满),所以树剖在一般情况下常数较小。
给出一种代码实现:
ll Q_max(ll x, ll y){
ll maxl=-INF;
while(top[x]!=top[y]){
if(dep[top[x]]>=dep[top[y]]){ // 看哪一个更靠下 == 看哪一个更深 == 看哪一个 dep 值大
maxl=max(maxl, Q.Max(1, dfn[top[x]], dfn[x])), x=fa[top[x]]; // 一定是f[top[x]]
} else{
maxl=max(maxl, Q.Max(1, dfn[top[y]], dfn[y])), y=fa[top[y]]; // 一定是f[top[y]]
}
}
if(dfn[x]<dfn[y]){
maxl=max(maxl, Q.Max(1, dfn[x], dfn[y]));
} else{
maxl=max(maxl, Q.Max(1, dfn[y], dfn[x]));
}
return maxl;
}
ll Q_sum(ll x, ll y){
ll sum=0;
while(top[x]!=top[y]){
if(dep[top[x]]>=dep[top[y]]){
sum+=Q.Sum(1, dfn[top[x]], dfn[x]), x=fa[top[x]]; // 一定是f[top[x]]
} else{
sum+=Q.Sum(1, dfn[top[y]], dfn[y]), y=fa[top[y]]; // 一定是f[top[y]]
}
}
if(dfn[x]<dfn[y]){
sum+=Q.Sum(1, dfn[x], dfn[y]);
} else{
sum+=Q.Sum(1, dfn[y], dfn[x]);
}
return sum;
} // 整体同 Q_max 函数
#include<bits/stdc++.h>
#define ll long long
#define Maxn 30005
#define INF 2147483647
using namespace std;
ll n, a, b, w[Maxn];
ll q, x, y;
string opt;
vector<ll> v[Maxn];
ll fa[Maxn], dep[Maxn], siz[Maxn], son[Maxn];
ll top[Maxn], dfncnt=0, dfn[Maxn], rnk[Maxn];
void dfs1(ll x){
son[x]=-1, siz[x]=1; // son为-1表示为叶子节点,siz包括本身
for(ll i=0;i<(ll)v[x].size();i++){
ll tmp=v[x][i];
if(!dep[tmp]){
dep[tmp]=dep[x]+1, fa[tmp]=x;
dfs1(tmp);
siz[x]+=siz[tmp];
son[x]=((son[x]==-1 || siz[tmp]>siz[son[x]]) ? tmp : son[x]); // 求重子节点
}
}
} void dfs2(ll x, ll Top){
top[x]=Top, dfn[x]=++dfncnt; // dfn
rnk[dfncnt]=x;
if(son[x]==-1){ return ; } // 叶子节点
dfs2(son[x], Top); // 更新重链上的点的信息
for(ll i=0;i<(int)v[x].size();i++){
ll tmp=v[x][i];
if(tmp!=son[x] && tmp!=fa[x]){ // 既不为重子节点,也不为父亲节点
dfs2(tmp, tmp); // 新的重链
}
}
}
//线段树的节点编号是dfn
struct Seg_tree{
#define lid (id<<1)
#define rid (id<<1|1)
struct SG_TR{
ll l, r, maxl, sum;
} tr[Maxn<<2];
void build(ll id, ll l, ll r){ // 建树
tr[id].l=l, tr[id].r=r;
if(l==r){
tr[id].maxl=tr[id].sum=w[rnk[l]];
return ;
}
ll mid=(l+r)>>1;
build(lid, l, mid), build(rid, mid+1, r);
tr[id].sum=tr[lid].sum+tr[rid].sum;
tr[id].maxl=max(tr[lid].maxl, tr[rid].maxl);
}
void update(ll id, ll x, ll val){ // 更新
if(tr[id].l==tr[id].r){
if(tr[id].l==x){
tr[id].maxl=tr[id].sum=val;
}
return ;
}
ll mid=(tr[id].l+tr[id].r)>>1;
if(x<=mid){ update(lid, x, val); }
else if(x>mid){ update(rid, x, val); }
tr[id].sum=tr[lid].sum+tr[rid].sum;
tr[id].maxl=max(tr[lid].maxl, tr[rid].maxl);
}
ll Max(ll id, ll l, ll r){ // 求区间最大
if(tr[id].l==l && tr[id].r==r){
return tr[id].maxl;
}
ll mid=(tr[id].l+tr[id].r)>>1;
if(r<=mid){ return Max(lid, l, r); }
else if(l>mid){ return Max(rid, l, r); }
else{ return max(Max(lid, l, mid), Max(rid, mid+1, r)); }
}
ll Sum(ll id, ll l, ll r){ // 求区间和
if(tr[id].l==l && tr[id].r==r){
return tr[id].sum;
}
ll mid=(tr[id].l+tr[id].r)>>1;
if(r<=mid){ return Sum(lid, l, r); }
else if(l>mid){ return Sum(rid, l, r); }
else{ return Sum(id, l, mid)+Sum(rid, mid+1, r); }
}
} Q; // 结构体封装线段树
ll Q_max(ll x, ll y){
ll maxl=-INF;
while(top[x]!=top[y]){
if(dep[top[x]]>=dep[top[y]]){ // 看哪一个更靠下 == 看哪一个更深 == 看哪一个 dep 值大
maxl=max(maxl, Q.Max(1, dfn[top[x]], dfn[x])), x=fa[top[x]]; // 一定是f[top[x]]
} else{
maxl=max(maxl, Q.Max(1, dfn[top[y]], dfn[y])), y=fa[top[y]]; // 一定是f[top[y]]
}
}
if(dfn[x]<dfn[y]){
maxl=max(maxl, Q.Max(1, dfn[x], dfn[y]));
} else{
maxl=max(maxl, Q.Max(1, dfn[y], dfn[x]));
}
return maxl;
}
ll Q_sum(ll x, ll y){
ll sum=0;
while(top[x]!=top[y]){
if(dep[top[x]]>=dep[top[y]]){
sum+=Q.Sum(1, dfn[top[x]], dfn[x]), x=fa[top[x]]; // 一定是f[top[x]]
} else{
sum+=Q.Sum(1, dfn[top[y]], dfn[y]), y=fa[top[y]]; // 一定是f[top[y]]
}
}
if(dfn[x]<dfn[y]){
sum+=Q.Sum(1, dfn[x], dfn[y]);
} else{
sum+=Q.Sum(1, dfn[y], dfn[x]);
}
return sum;
} // 整体同 Q_max 函数
int main()
{
cin>>n;
for(ll i=1;i<n;i++){
cin>>a>>b;
v[a].push_back(b);
v[b].push_back(a);
} for(ll i=1;i<=n;i++){
cin>>w[i];
}
dep[1]=1, dfs1(1), dfs2(1, 1), Q.build(1, 1, n);
cin>>q;
for(ll i=1;i<=q;i++){
cin>>opt>>x>>y;
if(opt[1]=='H'){ // CHANGE
Q.update(1, dfn[x], y);
} else if(opt[1]=='M'){ // MAX
cout<<Q_max(x, y)<<endl;
} else if(opt[1]=='S'){ // SUM
cout<<Q_sum(x, y)<<endl;
}
}
return 0;
}
标签:剖分,top,tr,笔记,树链,dfn,节点,重链,ll
From: https://www.cnblogs.com/BLM-dolphin/p/18035671