树剖是个神奇的东西~
其实也没有那么神奇~
首先要知道树剖是什么:将一颗树分成若干条链后,对每一个链用数据结构进行维护。
我们最常用的就是开一颗线段树保存所有树链(显然我们要保证有序)
如何分链?dalao们称它叫启发式合并,什么意思呢?
对于一颗以v为根的子树,我们选择它若干儿子中,儿子的儿子数(包括儿子自己)最多的那一个儿子与v相连直到叶子节点,这么一条路径我们称它为重路径,路径上的边我们成为重边,其余的则称轻边。
#define FOR(i,L,R) for(register int i=(L);i<=(R);++i)
int n,m,cnt;
struct Graph{//构图finish
vector<int>G[N];
int p[N],fa[N];
int siz[N],son[N],dep[N];
int top[N];
#define to G[x][i]
inline void dfs2(int x,int sp){
top[x]=sp;//我们记录每一条链的链顶,对于每个轻儿子,他的top等于自己
p[x]=++cnt;//线段树上的映射
if(son[x])dfs2(son[x],sp);
int sz=G[x].size()-1;
FOR(i,0,sz)if(to^fa[x]&&to^son[x])dfs2(to,to);
}
inline void dfs1(int x,int Fa,int depth){
int sz=G[x].size()-1;
FOR(i,0,sz)if(to^Fa){
dfs1(to,fa[to]=x,dep[to]=depth+1);
siz[x]+=siz[to];//更新size值
if(siz[son[x]]<siz[to])son[x]=to;//求重儿子
}
}
inline void init(){
scanf("%d",&n);
FOR(i,1,n-1){
int x,y;scanf("%d%d",&x,&y);
G[x].push_back(y);
G[y].push_back(x);
}FOR(i,1,n)siz[i]=1;//初始化每个siz
}
}g;
我们简称v的轻儿子为ls,重儿子为ws
由轻重儿子的性质易知,size(ls)<size(v)/2,否则它就是ws
对于我们树剖的操作过程,在此我们我们以树上求和为例(修改和查询一模一样,只是换函数名而已):
#define top(a) g.top[a]
#define dep(a) g.dep[a]
#define p(a) g.p[a]
inline void GETSUM(int x,int y){
int t1=top(x),t2=top(y);
int ans=0;
while(t1!=t2){
if(dep(t1)<dep(t2))swap(x,y),swap(t1,t2);//我们只针对深度深的进行操作
ans+=t.Sum(p(t1),p(x));
x=g.fa[t1];//显然这一块链都是被包含在内的,所以我们要跨越此链
t1=top(x);//更新top
}if(dep(x)>dep(y))swap(x,y);//当在一条链上时,我们就只用操作现在的x~y区间
cout<<ans+t.Sum(p(x),p(y))<<"\n";
}
显然我们的操作最多向上爬2log2(n)次,因为轻重儿子的性质决定了向上爬的次数,我们每一次修改是log2(n)的,因此树剖的时间复杂度大概是log2(n)^2,还算优秀
只要会了线段树,树剖就ok,因此线段树才是基础。