oi-wiki
Version:
wiki for OI / ACM-ICPC
274 lines (217 loc) • 8.41 kB
Markdown
在学习本部分时,请先学习 **[线段树](/ds/segment/)** 的相关内容。
## 树链剖分的思想及能解决的问题
一棵静态(形状固定的)树,要求进行几种操作:
1. 修改 **单个节点 / 树上两点之间的路径 / 一个节点的子树上** 的所有点的值。
2. 查询 **单个节点 / 树上两点之间的路径 / 一个节点的子树上** 节点的值的 **和 / 极值 / 其他(具有较强的合并性)** 。
如果树的形态是一条链,那么我们只需要维护一个线段树,修改或查询线段树的值。
因为这是一棵树,我们将这个树剖分成多个链,并用线段树修改或查询答案,这就是树链剖分的思想。
如果树是动态的,需要使用 **LCT** 来解决。
由于树链剖分的思想十分暴力,所以被 OIers 戏称为 **“优雅的暴力”** 。
## 例题 [luogu P2590 \[ZJOI2008\] 树的统计](https://www.luogu.org/problemnew/show/P2590)
题目大意:对一棵有 $n$ 个节点的静态树,进行三种操作共 $q$ 次:
1. 修改单个节点的值;
2. 查询 $u$ 到 $v$ 的路径上的最大值;
3. 查询 $u$ 到 $v$ 的路径上的权值和。
题目保证 $1\le n\le 30000,0\le q\le 200000$
## 一些定义
$fa(x)$ 表示节点 $x$ 在树上的父亲。
$dep(x)$ 表示节点 $x$ 在树上的深度。
$siz(x)$ 表示节点 $x$ 的子树的节点个数。
$son(x)$ 表示节点 $x$ 的 **重儿子**,即所有儿子中子树大小最大的一个。
定义 **重边** 表示连接两个重儿子的边。
定义 **重路径** 表示重边连成的一条链。
$top(x)$ 表示节点 $x$ 所在 **重路径** 的顶部节点(深度最小)。
$tid(x)$ 表示节点 $x$ 的 **时间戳** ,也是其在线段树中的编号。
$rnk(x)$ 表示时间戳所对应的节点编号,有 $rnk(tid(x))=x$。
我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 $fa(x),dep(x),siz(x),son(x)$,第二次 DFS 求出 $top(x),tid(x),rnk(x)$。
给出一种代码实现:
```cpp
void dfs1(int o,int fat)
{
son[o]=-1;siz[o]=1;
for(int j=h[o];j;j=nxt[j])if(!dep[p[j]])
{
dep[p[j]]=dep[o]+1;
fa[p[j]]=o;
dfs1(p[j],o);
siz[o]+=siz[p[j]];
if(son[o]==-1||siz[p[j]]>siz[son[o]])son[o]=p[j];
}
}
void dfs2(int o,int t)
{
top[o]=t;
cnt++;tid[o]=cnt;rnk[cnt]=o;
if(son[o]==-1)return;
dfs2(son[o],t);
for(int j=h[o];j;j=nxt[j])if(p[j]!=son[o]&&p[j]!=fa[o])dfs2(p[j],p[j]);
}
```
这样构成的线段树有这样一个性质,这是原树的一个 DFS 序,一个节点的子树在线段树中是相连的, **所有重链在线段树上也是相连的** 。
## 解法
根据题面以及以上的性质,你的线段树需要维护三种操作:
1. 单点修改;
2. 区间查询最大值;
3. 区间查询和。
单点修改很容易实现。
修改一个节点的子树也很容易实现。
问题是如何修改 / 查询两个节点之间的路径。
考虑我们是如何用 **倍增法求解 LCA** 的。首先我们 **将两个节点提到同一高度,然后将两个节点一起向上跳** 。对于树链剖分也可以使用这样的思想。
在向上跳的过程中,如果当前节点在重链上,借助线段树向上跳到重链顶端,如果当前节点不在重链上,向上跳一个节点。如此直到两节点相同。沿途更新 / 查询区间信息。
给出一种代码实现:
```cpp
// st 是线段树结构体
int querymax(int x,int y)
{
int ret=-inf,fx=top[x],fy=top[y];
while(fx!=fy)
{
if(dep[fx]>=dep[fy])ret=max(ret,st.query1(1,1,n,tid[fx],tid[x])),x=fa[fx];
else ret=max(ret,st.query1(1,1,n,tid[fy],tid[y])),y=fa[fy];
fx=top[x];fy=top[y];
}
if(x!=y)
{
if(tid[x]<tid[y])ret=max(ret,st.query1(1,1,n,tid[x],tid[y]));
else ret=max(ret,st.query1(1,1,n,tid[y],tid[x]));
}
else ret=max(ret,st.query1(1,1,n,tid[x],tid[y]));
return ret;
}
```
## 完整代码
鉴于树链剖分的题目细节较多,容易打错,给出一种代码实现,以供参考。
```cpp
#include<cstdio>
#include<cstring>
#include<algorithm>
#define lc o<<1
#define rc o<<1|1
using namespace std;
const int maxn=60010;
const int inf=2e9;
int n,a,b,w[maxn],q,u,v;
int cur,h[maxn],nxt[maxn],p[maxn];
int siz[maxn],top[maxn],son[maxn],dep[maxn],fa[maxn],tid[maxn],rnk[maxn],cnt;
char op[10];
inline void add_edge(int x,int y){cur++;nxt[cur]=h[x];h[x]=cur;p[cur]=y;}
struct SegTree
{
int sum[maxn*4],maxx[maxn*4];
void build(int o,int l,int r)
{
if(l==r){sum[o]=maxx[o]=w[rnk[l]];return;}
int mid=(l+r)>>1;
build(lc,l,mid);
build(rc,mid+1,r);
sum[o]=sum[lc]+sum[rc];
maxx[o]=max(maxx[lc],maxx[rc]);
}
int query1(int o,int l,int r,int ql,int qr)//max
{
if(l>qr||r<ql)return -inf;
if(ql<=l&&r<=qr)return maxx[o];
int mid=(l+r)>>1;
return max(query1(lc,l,mid,ql,qr),query1(rc,mid+1,r,ql,qr));
}
int query2(int o,int l,int r,int ql,int qr)//sum
{
if(l>qr||r<ql)return 0;
if(ql<=l&&r<=qr)return sum[o];
int mid=(l+r)>>1;
return query2(lc,l,mid,ql,qr)+query2(rc,mid+1,r,ql,qr);
}
void update(int o,int l,int r,int x,int t)
{
if(l==r){maxx[o]=sum[o]=t;return;}
int mid=(l+r)>>1;
if(x<=mid)update(lc,l,mid,x,t);
else update(rc,mid+1,r,x,t);
sum[o]=sum[lc]+sum[rc];
maxx[o]=max(maxx[lc],maxx[rc]);
}
}st;
void dfs1(int o,int fat)
{
son[o]=-1;siz[o]=1;
for(int j=h[o];j;j=nxt[j])if(!dep[p[j]])
{
dep[p[j]]=dep[o]+1;
fa[p[j]]=o;
dfs1(p[j],o);
siz[o]+=siz[p[j]];
if(son[o]==-1||siz[p[j]]>siz[son[o]])son[o]=p[j];
}
}
void dfs2(int o,int t)
{
top[o]=t;cnt++;tid[o]=cnt;rnk[cnt]=o;
if(son[o]==-1)return;
dfs2(son[o],t);
for(int j=h[o];j;j=nxt[j])if(p[j]!=son[o]&&p[j]!=fa[o])dfs2(p[j],p[j]);
}
int querymax(int x,int y)
{
int ret=-inf,fx=top[x],fy=top[y];
while(fx!=fy)
{
if(dep[fx]>=dep[fy])ret=max(ret,st.query1(1,1,n,tid[fx],tid[x])),x=fa[fx];
else ret=max(ret,st.query1(1,1,n,tid[fy],tid[y])),y=fa[fy];
fx=top[x];fy=top[y];
}
if(x!=y)
{
if(tid[x]<tid[y])ret=max(ret,st.query1(1,1,n,tid[x],tid[y]));
else ret=max(ret,st.query1(1,1,n,tid[y],tid[x]));
}
else ret=max(ret,st.query1(1,1,n,tid[x],tid[y]));
return ret;
}
int querysum(int x,int y)
{
int ret=0,fx=top[x],fy=top[y];
while(fx!=fy)
{
if(dep[fx]>=dep[fy])ret+=st.query2(1,1,n,tid[fx],tid[x]),x=fa[fx];
else ret+=st.query2(1,1,n,tid[fy],tid[y]),y=fa[fy];
fx=top[x];fy=top[y];
}
if(x!=y)
{
if(tid[x]<tid[y])ret+=st.query2(1,1,n,tid[x],tid[y]);
else ret+=st.query2(1,1,n,tid[y],tid[x]);
}
else ret+=st.query2(1,1,n,tid[x],tid[y]);
return ret;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)scanf("%d%d",&a,&b),add_edge(a,b),add_edge(b,a);
for(int i=1;i<=n;i++)scanf("%d",w+i);
dep[1]=1;dfs1(1,-1);dfs2(1,1);
st.build(1,1,n);
scanf("%d",&q);
while(q--)
{
scanf("%s%d%d",op,&u,&v);
if(!strcmp(op,"CHANGE"))st.update(1,1,n,tid[u],v);
if(!strcmp(op,"QMAX"))printf("%d\n",querymax(u,v));
if(!strcmp(op,"QSUM"))printf("%d\n",querysum(u,v));
}
return 0;
}
```
## 时间复杂度证明
以上算法的时间复杂度为 $O(q \log^2 n)$,下证:
可以证明,如果 $u$ 是 $v$ 的父亲,且 $v$ 不是 $u$ 的重儿子,有 $siz(v)\le \frac{1}{2} siz(u)$。
因为如果 $siz(v)> \frac{1}{2} siz(u)$,那么 $siz(v)$ 大于其他 $u$ 的儿子的 $siz$ 的和,就有 $siz(v)$ 大于其他 $u$ 的儿子的 $siz$。这样 $v$ 一定是 $u$ 的重儿子,与题设不符。
由此可知,每次查询操作,我们顺着 **非重链** 向上跳,其子树节点个数一定至少 **乘以 2**。如果我们顺着重链向上跳,其子树节点个数也会乘以 2,最大为 $n$。
由于一个点的子树的节点个数最多为 $n$ ,所以我们最多只需要向上跳 $O(\log n)$ 次。再乘上线段树查询的时间复杂度 $O(\log n)$ 和操作数 $O(q)$,最后的时间复杂度为 $O(q \log^2 n)$。证毕。
## 练习
[luogu P3258 \[JLOI2014\] 松鼠的新家](https://www.luogu.org/problemnew/show/P3258) (当然可以用树上差分)
[luogu P3178 \[HAOI2015\] 树上操作](https://www.luogu.org/problemnew/show/P3178)
[luogu P3384 【模板】树链剖分](https://www.luogu.org/problemnew/show/P3384)
[luogu P2146 \[NOI2015\] 软件包管理器](https://www.luogu.org/problemnew/show/P2146)
[luogu P2486 \[SDOI2011\] 染色](https://www.luogu.org/problemnew/show/P2486)
[luogu P3313 \[SDOI2014\] 旅行](https://www.luogu.org/problemnew/show/P3313)