树链剖分
树链剖分是一种用于高效处理树上路径问题的算法,结合线段树等数据结构,可以将树上的问题转化为区间问题,从而实现高效的查询和修改操作。
0. 应用场景
树链剖分主要用于解决以下几类树上的问题:
- 路径查询与修改:例如,求解树上两点之间的路径和、路径上的最大值/最小值等。
- 子树查询与修改:例如,求解某个子树的总和、子树内满足某种条件的节点数等。
- 最近公共祖先(LCA):虽然 LCA 问题可以通过其他算法(如倍增法)解决,但树链剖分也可以顺便实现 LCA 的快速查询。
1. 基本定义
- 树链:树上的一条简单路径。
- 重链:由若干个“重儿子”连接形成的链。
- 重儿子:在某个节点的所有子节点中,子树大小最大的子节点。
- 轻儿子:除了重儿子之外的其他子节点。
- 重边:连接父节点和重儿子的边。
- 轻边:连接父节点和轻儿子的边。
- 树->序列:通过 DFS 序将树上的节点映射到一个连续的序列上。
2. 原理
树链剖分的核心思想是将树剖分成若干条“重链”和“轻链”,使得每条链在 DFS 序中形成一段连续的区间。这样,树上的路径问题就可以通过线段树等数据结构高效地转化为区间问题求解。具体来说:
- 重链的长度可以很长,但轻链的长度相对较短。
- 从根节点到任意节点的路径最多经过 \(O(log n)\) 条轻链。
- 因此,树链剖分可以将路径操作的时间复杂度优化到 $ O(log^2 n)$。
3. 实现
以下是树链剖分的基本实现步骤:
(1) DFS1:计算子树大小、重儿子、深度
- 输入:当前节点 (u) 和其父节点 (fa)。
- 过程:
- 初始化当前节点的子树大小为 1(自身),更新当前节点的深度。
- 遍历当前节点的所有子节点 (v):
- 如果 (v) 是父节点 (fa),跳过。
- 递归调用 DFS1,计算子节点 (v) 的子树大小。
- 更新当前节点的子树大小为子节点的子树大小之和。
- 如果子节点 (v) 的子树大小大于当前的重儿子的子树大小,则将 (v) 设为当前节点的重儿子。
(2) DFS2:剖分树链
- 输入:当前节点 (u)、当前链的起始节点 (top)。
- 过程:
- 给当前节点 (u) 记录其链的起始节点 (top)。
- 如果当前节点 (u) 有重儿子,递归处理重儿子,保持当前链的连续性。
- 递归处理轻儿子,为每个轻儿子创建新的链。
(3) LCA 函数
- 利用链的起始节点 (top),可以快速求解 LCA。
- 方法:
- 如果两个节点 (u) 和 (v) 在同一条链上,直接比较深度,深度较小的节点是 LCA。
- 否则,将深度较大的节点跳到其链的起始节点,重复上述过程。
(4) 线段树操作
- 在树链剖分的基础上,结合线段树实现对路径或子树的查询和修改。
- 路径操作:
- 将路径拆分为若干条重链和轻链。
- 对每条链进行线段树操作。
- 子树操作:
- 利用 DFS 序的连续性,将子树问题转化为区间问题。
4. 示例代码(C++)
以下是树链剖分的简单实现示例:
P3384
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10;
int n,m,root,mod;
int h[N],e[N<<1],ne[N<<1],idx;
int dep[N],top[N],id[N],cnt,fa[N],son[N],sz[N],w[N],nw[N];
struct node
{
int add,sum;
int l,r;
}tr[N<<4];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs1(int u,int f,int depth)
{
dep[u]=depth,fa[u]=f;
sz[u]=1;
for(int i=h[u];~i;i=ne[i])
{
int v=e[i];
if(v==f) continue;
dfs1(v,u,depth+1);
sz[u]+=sz[v];
if(sz[v]>sz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int t)
{
top[u]=t;
id[u]=++cnt,nw[cnt]=w[u];
if(!son[u]) return ;
dfs2(son[u],t);
for(int i=h[u];~i;i=ne[i])
{
int v=e[i];
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
void pushup(int u)
{
tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%mod;
}
void pushdown(int u)
{
if(tr[u].add)
{
tr[u<<1].add+=tr[u].add,tr[u<<1|1].add+=tr[u].add;
(tr[u<<1].sum+=tr[u].add*(tr[u<<1].r-tr[u<<1].l+1))%=mod;
(tr[u<<1|1].sum+=tr[u].add*(tr[u<<1|1].r-tr[u<<1|1].l+1))%=mod;
tr[u].add=0;
}
}
void build(int u,int l,int r)
{
tr[u].l=l,tr[u].r=r;
if(l==r)
{
tr[u].add=0,tr[u].sum=nw[l]%mod;
return ;
}
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
void modify(int u,int l,int r,int k)
{
if(tr[u].l>=l&&tr[u].r<=r)
{
tr[u].add+=k,tr[u].sum=(tr[u].sum+k*(tr[u].r-tr[u].l+1))%mod;
return ;
}
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,k);
if(r>mid) modify(u<<1|1,l,r,k);
pushup(u);
}
void modify_path(int u,int v,int k)//修改u->v路径上的点
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
modify(1,id[top[u]],id[u],k);
u=fa[top[u]];
}
if(dep[u]<dep[v]) swap(u,v);
modify(1,id[v],id[u],k);
}
int query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r)
{
return tr[u].sum%mod;
}
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
int res=0;
if(l<=mid) res=(res+query(u<<1,l,r))%mod;
if(r>mid) res=(res+query(u<<1|1,l,r))%mod;
return res;
}
int query_path(int u,int v)
{
int res=0;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res=(res+query(1,id[top[u]],id[u]))%mod;
u=fa[top[u]];
}
if(dep[u]<dep[v]) swap(u,v);
res=(res+query(1,id[v],id[u]))%mod;
return res;
}
void modify_tree(int u,int k)
{
modify(1,id[u],id[u]+sz[u]-1,k);
}
int query_tree(int u)
{
return query(1,id[u],id[u]+sz[u]-1);
}
signed main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>m>>root>>mod;
for(int i=1;i<=n;i++) cin>>w[i],w[i]%=mod;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(u,v),add(v,u);
}
dfs1(root,-1,1);
dfs2(root,root);
build(1,1,n);
for(int i=1;i<=m;i++)
{
int op,x,y,k;
cin>>op>>x;
if(op==1)
{
cin>>y>>k;
modify_path(x,y,k);
}else if(op==2)
{
cin>>y;
cout<<query_path(x,y)<<'\n';
}else if(op==3)
{
cin>>k;
modify_tree(x,k);
}else {
cout<<query_tree(x)<<'\n';
}
}
}
P3379
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+10;
int n,m,root,mod;
int h[N],e[N<<1],ne[N<<1],idx;
int dep[N],top[N],id[N],cnt,fa[N],son[N],sz[N];
struct node
{
int add,sum;
int l,r;
}tr[N<<4];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs1(int u,int f,int depth)
{
dep[u]=depth,fa[u]=f;
sz[u]=1;
for(int i=h[u];~i;i=ne[i])
{
int v=e[i];
if(v==f) continue;
dfs1(v,u,depth+1);
sz[u]+=sz[v];
if(sz[v]>sz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int t)
{
top[u]=t;
if(!son[u]) return ;
dfs2(son[u],t);
for(int i=h[u];~i;i=ne[i])
{
int v=e[i];
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
signed main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>m>>root;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++)
{
int u,v;
cin>>u>>v;
add(u,v),add(v,u);
}
dfs1(root,-1,1);
dfs2(root,root);
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) v=fa[top[v]];
else u=fa[top[u]];
}
if(dep[u]<dep[v]) cout<<u<<'\n';
else cout<<v<<'\n';
}
}
5. 总结
树链剖分是一种强大的算法,结合线段树等数据结构,可以高效地解决树上的路径和子树问题。它的核心在于将树剖分成若干条链,并利用 DFS 序将问题转化为区间问题。
浙公网安备 33010602011771号