相当于把树上的链抽出来变成区间进行操作。
重链剖分(本文重点):
本文依照P3384 【模板】轻重链剖分/树链剖分的实现进行讲解。
就是剖分出重链覆盖全树。
重链:除开头的端点为轻儿子外,其他节点全为重儿子的链。
重儿子:一个节点的儿子中子树大小最大的儿子。
要进行树链剖分,先要进行预处理。需要处理出以下信息:
- t o p [ u ] top[u] top[u] u节点所在重链的开头端点的编号。
- d e p [ u ] dep[u] dep[u] u节点的深度。
- f a [ u ] fa[u] fa[u] u节点的父亲节点的编号。
- d f n [ u ] dfn[u] dfn[u] 时间戳,u节点被遍历到的顺序。
- r a n k [ x ] rank[x] rank[x] 时间戳为x的节点的编号。
- s i z e [ u ] size[u] size[u] 以u为根的子树大小。
- s o n [ u ] son[u] son[u] u节点的重儿子的编号。
总共七个(七国之乱
预处理我们分为两个 dfs 来做。因为在重链剖分中优先遍历重儿子(为了使一条重链上的 d f n dfn dfn 连续)
第一次 dfs 处理除 t o p top top、 d f n dfn dfn 和 r a n k rank rank 外的基本信息把重链基本的剖分出来。
void dfs1(int u,int f)
{
fa[u]=f;
size[u]=1;
int tmp=-1;
for(int i=head[u];~i;i=nxt[i])
{
if(to[i]==f) continue;
dep[to[i]]=dep[u]+1;
dfs1(to[i],u);
size[u]+=size[to[i]];
if(size[to[i]]>tmp)
{
tmp=size[to[i]];
son[u]=to[i];
}
}
}
第二次就主要为了把节点信息标完整。
void dfs2(int u,int tp)
{
dfn[u]=++idx;
rk[idx]=u;
top[u]=tp;
if(!son[u]) return;
dfs2(son[u],tp);
for(int i=head[u];~i;i=nxt[i])
{
if(to[i]==son[u]||to[i]==fa[u]) continue;
dfs2(to[i],to[i]);
}
}
然后考虑用线段树维护被用 d f n dfn dfn 从原树中提取出来的区间。
线段树写法没有特殊,按照普通的线段树写即可。
唯一需要注意的是线段树的建树(见注释处)
void build(int i,int l,int r)
{
t[i].l=l;t[i].r=r;t[i].mid=(l+r)>>1;
t[i].d=t[i].add=0;
if(l==r)
{
t[i].d=a[rk[l]]; //因为下标l,r是dfn
return;
}
build(i<<1,l,t[i].mid);
build(i<<1|1,t[i].mid+1,r);
t[i].d=(t[i<<1].d+t[i<<1|1].d)%p;
}
其他部分和普通线段树是一样的。
然后逐一分析题目中的4个操作。
-
1 x y z,表示将树从 x 到 y 结点最短路径上所有节点的值都加上 z。
-
2 x y,表示求树从 x 到 y 结点最短路径上所有节点的值之和。
-
3 x z,表示将以 x 为根节点的子树内所有节点值都加上 z。
-
4 x,表示求以 x 为根节点的子树内所有节点值之和。
操作3,4非常好实现:
void mtree(int x,LL z)
{
z%=p;
t.modify_add(1,dfn[x],dfn[x]+size[x]-1,z);
}
LL qtree(int x)
{
return t.query(1,dfn[x],dfn[x]+size[x]-1)%p;
}
因为区间下标是 d f n dfn dfn 一个节点与其子树构成的区间是连续的。
再看操作1,2。我们分情况讨论,对于在同一重链上的两个点,我们直接对它们的 d f n dfn dfn 区间进行操作即可。
对于不在同一重链上的两点,我们选择它们中所在重链的开头端点深度更深的(这样的目的是为了防止影响到无关的节点),对于它到其链顶进行区间操作,然后跳到它链顶的父亲,超级加辈,重复执行一直到两点在同一条重链上。
实现如下:
void mchain(int x,int y,LL z)
{
z%=p;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) Swap(x,y);
t.modify_add(1,dfn[top[x]],dfn[x],z);
x=fa[top[x]];
}
if(dfn[x]>dfn[y]) Swap(x,y);
t.modify_add(1,dfn[x],dfn[y],z);
}
LL qchain(int x,int y)
{
LL res=0;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) Swap(x,y);
res=(res+t.query(1,dfn[top[x]],dfn[x]))%p;
x=fa[top[x]];
}
if(dfn[x]>dfn[y]) Swap(x,y);
res=(res+t.query(1,dfn[x],dfn[y]))%p;
return res;
}
然后具体套用函数就好了。
对于维护边权的情况,我们只需要在 dfs1 中维护 a 数组,把该点的点权赋值为与父亲相连的边的边权就行了,本质上是边权转成点权。但是记得为根节点赋值。还有一个需要注意的就是在查询和修改链的时候,在同一条重链上的情况,我们在选择的区间应该是 [ d f n [ x ] + 1 , d f n [ y ] ] [dfn[x]+1,dfn[y]] [dfn[x]+1,dfn[y]],这样做可以防止 x 节点到其父节点的边权被算入答案。其他部分与普通树剖一致。
然后关于树链剖分求LCA