树链剖分,是很多树上问题很好的解决方法. 比如说修改树上路径上点的权值.有些人说线段树一定能解决,但是光用线段树是不行的,这里就可以用线段树结合树链剖分解决. 相信很多人还不知道树链剖分是什么算法,不废话了,开始.
先来一道模板题,给定N个节点的一棵树有K次查询,每次查询a和b的最近公共祖先(即LCA) (N<=1e5,K<=1e5)
首先暴力肯定会被卡掉O(N*K); 可以用倍增O(NlogN+KlogN),离线tarjan O(N+K) (常数较大且在强制在线时不行) LCA转RMQ利用dfs序也可以解决 O(2NlogN+K) ,今天我介绍的树链剖分虽然理论时间复杂度不够优秀,但是实际运行时间最优. 倍增216ms,离线tarjan128ms,LCA转RMQ260ms(在K远远大于N时此类问题它更优),而树剖108ms
下面我来介绍树剖的原理,它是把一棵树的所有边分成两种链(轻链和重链),重链的定义是在根节点一定的情况之下,在此节点的儿子节点中,谁下方的子树最大,就把那一条链定义成重链,此节点连接剩下子节点的边都定义为轻链. 这有什么好处呢?请看图.
此图来自poj1330 剖分后就是如下情况,求LCA时遇到重链就一直到顶上,轻链就走一步. 图画的略丑,但意思表达出来了. 做题时,可用几个数组将图的意思表达出来. size[]表示所有点子树的大小. fa[]表示此节点的父亲是谁.top[]表示以此节点为起点通过重链能到达最上面的点,dep[]表示此节点 深度,son[]表示此节点的儿子. 在这里我要说一下,如果一个节点有多个儿子子树个数相同时,那么就随便定义一个为重链就好了 . 链式前向星连边,两次dfs,第一次维护size,fa,dep,son.第二次维护top,之后遵循之前的原则,遇到重链跳到顶端,遇到轻链跳一步.暴力向上跳就行了. 代码如下:
#include<stdio.h>
int dep[100001];
int size[100001];
int son[100001];
int fa[100001];
int top[100001];
int head[100001];
int to[200001];
int next[200001],idx;
bool is[100001];
void addedge(int a,int b)
{
next[++idx]=head[a];
head[a]=idx;
to[idx]=b;
}
void dfs(int p)
{
size[p]=1;
for(int i=head[p];i;i=next[i])
{
if(to[i]!=fa[p])
{
fa[to[i]]=p;
dep[to[i]]=dep[p]+1;
dfs(to[i]);
size[p]+=size[to[i]];
if(size[to[i]]>size[son[p]])
son[p]=to[i];
}
}
}
void dfs2(int p,int t)
{
top[p]=t;
if(son[p])
dfs2(son[p],t);
for(int i=head[p];i;i=next[i])
{
if(to[i]!=fa[p]&&to[i]!=son[p])
dfs2(to[i],to[i]);
}
}
int lca(int x,int y)
{
while(top[x]!=top[y])
{
if(dep[top[x]]>dep[top[y]])
x=fa[top[x]];
else
y=fa[top[y]];
}
if(dep[x]<dep[y])
return x;
else
return y;
}
int main()
{
int Q,n,i,a,b,allfa,x,y;
scanf("%d%d",&n,&Q);
for(int i=1;i<n;i++)
{
scanf("%d%d",&a,&b);
fa[b]=a;
is[b]=true;
addedge(a,b);
addedge(b,a);
}
for(int i=1;i<=n;i++)
if(is[i]==0)
{
allfa=i;
break;
}
dfs(allfa);
dfs2(allfa,allfa);
for(int i=1;i<=Q;i++)
{
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
}
LCA的结果就是最后x和y较浅值.
树链剖分还有许多应用,比如它的dfs序与线段树的结合,再借用一下上张图. 此序列为: 8 4 10 16 3 12 11 2 15 7 1 14 13 5 9