————————————————18.4.18更新
有时我们会遇到这样的问题:在一棵树上,每次询问两点间路径上的和或者是最值。但我们用搜索时,时间就会到O(n),这样根本就完不成算法。但树上剖分就可以缩短修改的时间。
树上剖分的算法简介
我们定义每个有儿子的节点有一个重儿子(great son),重儿子的选择方式有很多种,但使算法时间最少的重儿子选择方式是选择所有儿子中的子树的节点数最多的那个儿子。(经验证明博主蒟蒻不知)
上图就是一个树剖的例子。接下来我们将每条路径上的重儿子用重链链接,按重链顺序dFS,为每个节点定义一个新的id值(dfs序)。同时对于每个节点定义一个top值为它所在的重链上深度最小的点的编号。下面上图,理解一下:
接下来,我们惊奇地发现:重链上的编号是连续的!所以树上所有的信息可以储存在一个线段树里维护。修改时,我们在树上用top缩短路径,就可以降低时间复杂度了。
树剖一些相关信息
平摊复杂度修改查询O(log2(n)),初始化复杂度O(n)。
树剖可以求lca,路径最值,和。缺点也很明显,不能动态维护树,这时就要使用LCT了。但是静态时树剖的常数更小更优秀。
树剖算法实现
树剖需要用到两次dfs:第一次是初始化树,第二次是初始化重链的top值和id值
vector <LL> ma[100010]; //树边存储
void init(LL x){ //第一次dfs,x表示搜索到x号点
LL maxx=0,bian=0; // maxx表示该点儿子中最大的size值
vector <LL> ::iterator it;
dep[x]=dep[fa[x]]+1;
size[x]=1; //该点的size值先初始化
for(it=ma[x].begin();it!=ma[x].end();it++)if(*it!=fa[x]){
fa[*it]=x;init(*it);size[x]+=size[*it]; //先处理它的儿子,再计算出该点的size值
if(maxx<size[*it]){
maxx=size[*it];bian=*it; //更新重儿子
}
}
gs[x]=bian; //重儿子赋值
}
void bfs(LL x,LL t) { //第二次dfs ,x表示找到第x号点,t表示该点的top值
vector <LL> ::iterator it;
if(!x)return ;
top[x]=t;
id[x]=++cnt; //id为该点的dfs序
fanid[id[x]]=x; //更新该id的反映射
bfs(gs[x],t);//先更新重儿子的top值,它的top值为当前点的top值
for(it=ma[x].begin(); it!=ma[x].end(); it++)if(*it!=fa[x]&&*it!=gs[x])bfs(*it,*it);//它其他儿子的top值为其它节点的编号
}
修改:某节点到另一节点的路径所有点权加a
void mo(LL x,LL y,LL a){ // 修改x点到y点路径上所有点的点权,使它们增加a
while(top[x]!=top[y]){ //将x,y先跳至同一重链
if(dep[top[x]]<dep[top[y]])swap(x,y); //先将x与y选择一个跳至更大的深度
modify(1,id[top[x]],id[x]); //修改路径上的所有点权
x=fa[top[x]]; //x跳至它top的爸爸
}
if(dep[x]<dep[y])swap(x,y);//跳至同一重链后,它们的路径也要修改 ,选择深度更大的
modify(a,id[y],id[x]); //修改它们路径上的所有点权
}
这里给大家留一个思考:如何修改某节点的子树上所有节点的点权呢?提示:bfs序,看前面的图找找区间规律吧
查询:某节点到另一节点的路径所有点权和
LL loadsum(LL x,LL y) { //查询 x点到y点路径上所有点的点权和
LL sum=0;
while(top[x]!=top[y]) {
if(dep[top[y]]>dep[top[x]])swap(x,y);
sum+=getsum(1,id[top[x]],id[x]); //加上路径上所有点权和
x=fa[top[x]];
} //其实与之前修改操作的跳点操作一模一样
if(dep[x]>dep[y])swap(x,y);
sum+=getsum(1,id[x],id[y]); //加上它们路径上所有点权和
return sum;
}