树链剖分是将一棵树按照特殊的dfs序划分成链
从而使树上任意一条链最多被划分为log(n)段
同时保持的dfs序对子树操作的便利
主要用的方法是轻重链剖分:
定义size[x]为以x 为根子树的大小
son[ x ] 为 x 的儿子中size最大的节点 称为重儿子
那么x与son[ x ] 相连的边就是一条重边 其余边为轻边
重边连在一起就是重链 轻链就是轻边
这样我们就可以让尽可能多的点在dfs序中连续,这样我们修改时才会尽可能地发挥线段树区间维护的优势。
一些性质:
- 在轻边(u ,v)中 size[ u ] < size[ u ] / 2
- 从根到某一点的路径上 不超过logn条
需要维护的有:
size[ ] son [ ] 定义如上
dfn[ ] 每个点dfs序编号
dep[ ] 记录每个点深度
fa[ ] 记录每个点的father (用于以后维护dfs序上不连续区间之间的跳跃)
top[ ] 记录每个点所在链上的dep最小的点(深度最小的点)
( 用于以后维护dfs序上连续的区间)
w [ ] 重新编号的每个点的权值 用线段树维护
通过两遍dfs实现:
第一遍dfs求出size son dep fa
第二遍dfs确定top dfn w
具体代码实现如下:
细节见注释
void dfs1(int x)
{
size[x]=1;
for(register int i=0;i<link[x].size();i++)
{
if(cur!=fa[x])
{
dep[cur]=dep[x]+1;
fa[cur]=x;//这两个一定写在访问子节点之前
dfs1(cur);
size[x]+=size[cur];
if(size[cur]>size[son[x]])son[x]=cur; //更新重儿子
}
}
}
void dfs2(int x,int t)
{
p++;
dfl[x]=p;
top[x]=t;
a[p]=w[x];
if(son[x])dfs2(son[x],t);
for(int i=0;i<link[x].size();i++)
if(cur!=fa[x]&&cur!=son[x])
dfs2(cur,cur);
dfr[x]=p; //这里区间右端点可以不用记录 因为dfr=dfn+size-1
}
这样就可以把整棵树的dfs序放在数据结构上维护
修改以及询问操作就要利用到刚刚记录的top&fa
举个栗子
假设要将(11,10)上每个点的权值加1
令x=11 , y=10
我们可以知道top[x]到x在dfs序上是连续的 top[y]到y也是一样
默认优先更新top值最大的点
那么我们可以直接更新线段树上区间(dfn[ top[y] ] , dfn[ y ])
然后y=fa[ y ]
接下来更新的是(dfn[ top[x] ] , dfn[ x ]) then x= fa[ x ]
。。。
以此类推直到top [ x ] ==top [ y ] 这时表明x y在一条链上了
那么最后更新一次(dfn [ x ] , dfn [ y ] ) (先把x深度调到 < y)
就可以收工啦
询问操作也是类似的
具体实现:
void cal(int x,int y,int z)
{
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) x^=y^=x^=y;
modi(1,n,dfl[top[x]],dfl[x],1,z);//注意要加dfn 线段树上维护是通过dfs序的编号
x=fa[top[x]];
}
if(dep[x]>dep[y]) x^=y^=x^=y;
modi(1,n,dfl[x],dfl[y],1,z);
}
int qsum(int x,int y)
{
int ans=0;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) x^=y^=x^=y;
ans+=query(1,n,dfl[top[x]],dfl[x],1);
ans%=P;
x=fa[top[x]];
}
if(dep[x]>dep[y]) x^=y^=x^=y;
ans+=query(1,n,dfl[x],dfl[y],1);
ans%=P;
return ans;
}
因为剖分后依然是dfs序
那么关于x子树的修改就依然可以直接修改(dfn[ x ], dfn[x]+size[x]-1)
具体就不再赘述
值得一提的是
对于带权图可以把权值下放到边连接的深度大点上
就又变成了上面那样啦
敲代码细心噢!
稍微不注意就会找不到错
收工啦