树上求值无非就这几种常见类型:
一 求子树的值
方法1:DFS做差
条件:可离线+可前缀做差
复杂度:(O(n))
优点:时间短
大致流程:
void DFS(int x,int fa){
update_ans(x);//拿遍历子树前的值更新答案
for(int i=0;i<G[x].size();i++){//遍历子树
int t=G[x][i];
if(t==fa)continue;
DFS(t,x);
}
update_ans(x);//拿遍历子树后的值更新答案
}
方法2:Dsu on tree
条件:可离线
复杂度:(O(nlog(n)))
优点:十分暴力,可以维护不具有单调性的值
要点:维护的容器需要开全局,重儿子要放到最后遍历并且保证其只会被加入一次
大致流程:
例题:[CF]Tree and Queries
void DFS(int x,int fa){//预处理每个节点的重儿子
size[x]=1;
for(int i=0;i<G[x].size();i++){
int t=G[x][i];
if(t==fa)continue;
DFS(t,x);
if(size[t]>size[son[x]])son[x]=t;//更新重儿子
size[x]+=size[t];
}
}
void solve(int x,int fa){
for(int i=0;i<G[x].size();i++){
int t=G[x][i];
if(t==fa||t==son[x])continue;//先遍历轻儿子
solve(t,x);
Erase(t);//暴力把轻儿子的值删掉,以防止遍历其兄弟时造成影响
}
if(son[x])solve(son[x],x);//遍历重儿子,这样可以保证回溯时不用将重儿子的值删去
for(int i=0;i<G[x].size();i++){
int t=G[x][i];
if(t==fa||t==son[x])continue;
Add(t);//暴力把轻儿子的值在加进来,以用来查询该节点的值
}
Get_Ans(x);//查询这个节点
}
方法3:DFS序+线段树/树状数组等区间型数据结构
条件:维护的值存在某种单调性
复杂度: (O(nlog(n)))
优点:可在线操作
要点:每个节点对应的区间编号不要和原编号弄混
大致流程:
void DFS(int x,int fa){//预处理DFS序,把树转成区间,直接维护区间即可
L[x]=++mark;
bel[mark]=x;//这个表示DFS序中的编号所对应的的原编号,有时需要用
for(int i=0;i<G[x].size();i++){
int t=G[x][i];
if(t==fa)continue;
DFS(t,x);
}
R[x]=mark;
}
二 求路径的值
方法1:LCA做差
条件:可前缀做差
复杂度:除找LCA外,遍历复杂度是(O(n))
优点:时间较短,可以搭配上线段树等数据结构
原理与要点:应用了(ans_{x,y}=ans_{root,x}+ans_{root,y}-2 imes ans_{root,lca_{x,y}})的做差性质(root表示根节点)显然,这样只用维护一个节点到根节点的值即可
大致流程:
void DFS(int x,int fa){//预处理,为倍增法求LCA做准备
pre[x][0]=fa;
deep[x]=deep[fa]+1;
for(int i=1;(1<<i)<=deep[x];i++)pre[x][i]=pre[pre[x][i-1]][i-1];//求x向上走2^i步的祖先
for(int i=0;i<G[x].size();i++){
int t=G[x][i];
if(t==fa)continue;
DFS(t,x);
}
}
int LCA(int x,int y){//用倍增法求LCA,其中lg[i]满足2^(lg[i]-1)<=i&&i<2^lg[i]
if(deep[x]<deep[y])swap(x,y);//保证x的深度大于y的深度
while(deep[x]>deep[y])x=pre[x][lg[deep[x]-deep[y]]-1];//跳到同一深度
if(x==y)return x;//相等直接return掉
for(int i=lg[deep[x]]-1;i>=0;i--){//倍增跳跃
if(pre[x][i]==pre[y][i])continue;
x=pre[x][i],y=pre[y][i];
}
return pre[x][0];//在向上走一步
}
void solve(int x,int fa){//再遍历一次,用那个式子求一波答案
}
方法2:树剖+线段树/树状数组等区间型数据结构
条件:维护的值存在某种单调性
复杂度: 不算跳重链的话,为(O(nlog(n)))
优点:可以支持路径区间修改操作,功能强大
要点:码量巨大,注意细节细节细节!!!
大致流程(以线段树为例):
例题:[AHOI2005]航线规划
void DFS_first(int x,int fa){//一遍DFS:预处理每个节点的重儿子,以及其父亲
size[x]=1;
pre[x]=fa;
for(int i=0;i<G[x].size();i++){
int t=G[x][i];
if(t==fa)continue;
DFS_first(t,x);
if(size[t]>size[son[x]])son[x]=t;//更新重儿子
size[x]+=size[t];
}
}
void DFS_second(int x,int fa){//二遍DFS:预处理出树剖序、一个节点能到达一个树剖序连续链的最顶端
dfsn[x]=++mark;//树剖序
top[x]=fa;//fa表示能到达的最顶端
if(son[x])DFS_second(son[x],fa);//优先遍历重儿子,使树剖序连续的链尽可能长
for(int i=0;i<G[x].size();i++){
int t=G[x][i];
if(t==pre[x]||t==son[x])continue;
DFS_second(t,t);//遍历轻儿子,此时dfsn[x]和dfsn[t]不再是连续的,因此要将fa设置成t
}
}
void change(int x,int y){//改变x到y的路径上的值
while(top[x]!=top[y]){//这敲法类似树剖找LCA的过程,当top[x]=top[y]时,说明x,y在一条树剖序连续的链上,就可以退出了
if(deep[top[x]]<deep[top[y]])swap(x,y);//确保top[x]的深度大于top[y],不然可能会重复更新
update_sgt(dfsn[top[x]],dfsn[x]);//在线段树上更新区间
x=pre[top[x]];//跳出这条树剖序连续的链
}
//最后x,y会在一条树剖序连续的链上,依然还需要进行一次更新
if(deep[x]<deep[y])swap(x,y);
if(deep[x]!=deep[y])update_sgt(dfsn[y],dfsn[x]);//如果是更新点的话if(deep[x]!=deep[y])基本上不用打,这得看你维护什么东西
}
int ask(int x,int y){//查询操作与改变操作类似,就不在赘述
int res=0;
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]])swap(x,y);
res+=query_sgt(dfsn[top[x]],dfsn[x]);
x=pre[top[x]];
}
if(deep[x]<deep[y])swap(x,y);
if(deep[x]!=deep[y])res+=query_sgt(dfsn[y],dfsn[x]);
return res;
}
方法3:点分治(淀粉质)
条件:可离线,无根树
复杂度:(O(nlog(n)))
优点:十分暴力,可以维护不具有单调性的值(与Dsu差不多)
要点:每次需遍历子树的重心,对于当前点只用考虑与当前点的情况即可
大致流程:
void Getroot(int x,int fa){//找重心
size[x]=1,W[x]=0;//size表示子树大小,W表示儿子中子树大小最大值
for(int i=0;i<G[x].size();i++){
int t=G[x][i].ed;
if(t==fa||vis[t])continue;//vis表示当前节点是否被遍历到,确保下一个节点没有被遍历到
Getroot(t,x);
size[x]+=size[t];
W[x]=max(W[x],size[t]);
}
W[x]=max(W[x],SIZE-size[x]);//因为这是无根树,所以其父亲也有可能是它儿子(若以x为根的话)
if(W[x]<W[root])root=x;//更新重心,注意root初值应为0,W[0]应为无穷大
}
void DFS_Point(int x){//点分治
UpdateAns(x);//暴力更新答案
vis[x]=true;//标记
for(int i=0;i<G[x].size();i++){
int t=G[x][i].ed;
if(vis[t])continue;
Getroot(t,x);//暴力找重心
DFS_Point(root);//继续分治
}
}