前段时间学习了下树链剖分,好久没看了,今天又复习一遍,赶紧写下来,别又忘了。
我们在信息学竞赛中,有时会碰到这么一类题型,在一棵树中,修改两点之间路径上的所有边(或点)上的某个变量(如边的长度,点的权值等等),然后询问单个点(或边)或者两点之间路径上的所有点(或边)的某些性质(如边权之和,最大边最小边等等)。对于这样的题,往往容易往线段树上去靠,但是,单单是用线段树是无法维护每一条链的性质的,所以我们需要一种算法将树链分开来,使得每条链可以和线段树中的一个区间一一对应上。(当然树链剖分远远不止这些简单的应用,也不一定要和线段树有什么关系,总之就是将树链剖分开来吧)。
树链剖分有很多种剖分方法,最常用的应该就是轻重边剖分了吧(在网上大部分介绍的都是这种剖分方法),什么是轻重边剖分呢?
我们首先将树中的边分为两部分,轻边和重边,记size(U)为以U为根的子树的节点的个数,令V为U的儿子中size最大的一个(如有多个最大,只取一个),则我们说边(U,V)为重边,其余的边为轻边(如下图所示红色为重边,蓝色为轻边)。
我们将一棵树的所有边按上述方法分成轻边和重边后,我们可以得到以下几个性质:
1:若(U,V)为轻边,则size(V)<=size(U)/2。
这是显然的。
2:从根到某一点的路径上轻边的个数不会超过O(logN),(N为节点总数)。
这也是很简单,因为假设从跟root到v的路径有k条轻边,它们是 root->...->v1->...->v2->......->vk->...->v,我们设size(v)=num,显然num>=1,则由性质1,我们有size(Vk)>=2,size(Vk-1)>=4......size(v1)>=2^k,显然有2^k<=N,所以k<=log2(N)。
如果我们把一条链中的连续重边连起来,成为重链,则一条链就变成了轻边与重链交替分段的链,且段数是log(N)级别的,则我们可以讲重链放在线段树中维护,轻边可放可不放,为了方便我一般还是放,但是速度就会打一点折扣了。思路就是这么多,接下来就是具体实现了。
我们需要维护一下值:
siz[v]表示以v为根的子树的节点总数。
dep[v]表示v的深度。
son[v]表示与v在同一重链上的v的儿子节点。
fa[v]表示v的父亲节点。
top[v]表示v所在链的顶端节点。
w[v]表示节点v在线段树中的位置。
siz[],son[],fa[],dep[]可以在第一遍dfs中求出来,top[],w[]可在第二遍dfs中求出来。具体过程看代码吧。
struct edge
{
int to;
int next;
}e[maxn<<1];
int box[maxn],cnt,tot;
int siz[maxn],top[maxn],son[maxn],dep[maxn],fa[maxn];
void init()
{
tot=0;
son[0]=dep[0]=0;
memset(box,-1,sizeof(box));
cnt=0;
}
void add(int from,int to)
{
e[cnt].to=to;
e[cnt].next=box[from];
box[from]=cnt++;
}
void dfs(int now,int pre)
{
siz[now]=1;
fa[now]=pre;
son[now]=0;
dep[now]=dep[pre]+1;
int t,v;
for(t=box[now];t+1;t=e[t].next)
{
v=e[t].to;
if(v!=pre)
{
dfs(v,now);
siz[now]+=siz[v];
if(siz[son[now]]<siz[v])
{
son[now]=v;
}
}
}
}
void dfs2(int now,int tp)
{
top[now]=tp;
if(son[now])
dfs2(son[now],top[now]);
int t,v;
for(t=box[now];t+1;t=e[t].next)
{
v=e[t].to;
if(v!=fa[now]&&v!=son[now])
dfs2(v,v);
}
}
以上是剖分过程,关于如何在树链剖分后维护两点间路径的信息,请看这里LCA的树链剖分实现
这里需要注意的是,对于有些题要修改的权值或询问的权值在点上,有的在边上,这在剖分时虽然过程没有变,但在处理的时候是有区别的,具体不同我想在下面两道题里体现。
权值在边上的情况。
http://codeforces.com/problemset/problem/165/D
codeforces 165D Beard Graph
题意:给一棵树,树的每条边有一种颜色,黑色或白色,一开始所有边均为黑色,有两个操作:
操作1:将第i条边变成白色或将第i条边变成黑色。
操作2 :询问u,v两点之间仅经过黑色变的最短距离。
思路:其实这道题可以不用树链剖分,存在更高效的方法,但是一时又想不到更好的例子。
因为是一棵树,所以两点之间的路径是确定的,所以只需要判断路径中是否所有的边均为黑色边即可,全是黑边意味着没有白边,所以我们可以这么做,我们将每条边剖分放入线段树中后,初始时将所有边权设为0,对操作1,如果要将一条边改为黑色,则将线段树赋值为零,否则分值为1,然后对于操作2,我们只要看两点间路径是否权之和为0即可,若为0,返回两点间距离,否则返回0。
上代码:
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#define maxn 100010
using namespace std;
#define mid ((t[p].l+t[p].r)>>1)
#define ls (p<<1)
#define rs (ls|1)
struct tree
{
int l,r;
int sum;
}t[maxn<<2];
void pushup(int p)
{
t[p].sum=t[ls].sum+t[rs].sum;
}
void build(int p,int l,int r)
{
t[p].l=l,t[p].r=r,t[p].sum=0;
if(l==r)
return;
build(ls,l,mid);
build(rs,mid+1,r);
}
void add(int p,int x,int val)
{
if(t[p].l==t[p].r)
{
t[p].sum+=val;
return;
}
if(x<=mid)
add(ls,x,val);
else
add(rs,x,val);
pushup(p);
}
int query(int p,int l,int r)
{
if(t[p].l==l&&t[p].r==r)
{
return t[p].sum;
}
if(l>mid)
return query(rs,l,r);
else if(r<=mid)
return query(ls,l,r);
else
return query(ls,l,mid)+query(rs,mid+1,r);
}
int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn];
struct edge
{
int to;
int next;
}e[maxn<<1];
int box[maxn],cnt,tot;
void init()
{
tot=0;
son[0]=dep[0]=0;
memset(box,-1,sizeof(box));
cnt=0;
}
void add(int from,int to)
{
e[cnt].to=to;
e[cnt].next=box[from];
box[from]=cnt++;
}
void dfs(int now,int pre)
{
siz[now]=1;
fa[now]=pre;
son[now]=0;
dep[now]=dep[pre]+1;
int t,v;
for(t=box[now];t+1;t=e[t].next)
{
v=e[t].to;
if(v!=pre)
{
dfs(v,now);
siz[now]+=siz[v];
if(siz[son[now]]<siz[v])
{
son[now]=v;
}
}
}
}
void dfs2(int now,int tp)
{
w[now]=++tot;
top[now]=tp;
if(son[now])
dfs2(son[now],top[now]);
int t,v;
for(t=box[now];t+1;t=e[t].next)
{
v=e[t].to;
if(v!=fa[now]&&v!=son[now])
dfs2(v,v);
}
}
int solve(int a,int b)
{
int f1=top[a],f2=top[b],dist=0;
while(f1!=f2)
{
if(dep[f1]<dep[f2])
{
swap(f1,f2);
swap(a,b);
}
dist+=w[a]-w[f1]+1;
int tmp=query(1,w[f1],w[a]);
if(tmp)
return -1;
a=fa[f1];
f1=top[a];
}
if(a==b)
return dist;//注意这里
else
{
if(dep[a]>dep[b])
swap(a,b);
int tmp=query(1,w[son[a]],w[b]);//注意这里
if(tmp)
return -1;
return dist+w[b]-w[a];
}
}
int Edge[maxn][2];
int main()
{
int n,q,i,a,b;
scanf("%d",&n);
init();
for(i=1;i<n;i++)
{
scanf("%d%d",&Edge[i][0],&Edge[i][1]);
add(Edge[i][0],Edge[i][1]);
add(Edge[i][1],Edge[i][0]);
}
build(1,1,n);
dfs(1,0);
dfs2(1,1);
scanf("%d",&q);
while(q--)
{
int k;
scanf("%d",&k);
if(k==3)
{
scanf("%d%d",&a,&b);
printf("%d\n",solve(a,b));
}
else
{
scanf("%d",&i);
int tmp;
if(dep[Edge[i][0]]>dep[Edge[i][1]])
tmp=Edge[i][0];
else
tmp=Edge[i][1];
if(k==1)
add(1,w[tmp],-1);
else
add(1,w[tmp],1);
}
}
return 0;
}
权值在点上的情况:
http://acm.hdu.edu.cn/showproblem.php?pid=3966
HDU:3966 Aragorn's Story
题意:题意很明白,给一棵树,将两点之间的路径中的所有点的权值增加或减少一个数,询问特定点当前的权值大小。
思路:思路应该很清晰了,将树剖分后放进线段树中维护。
代码如下:
#pragma comment(linker,"/STACK:100000000,100000000")
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#define maxn 50010
using namespace std;
#define mid ((t[p].l+t[p].r)>>1)
#define ls (p<<1)
#define rs (ls|1)
struct tree
{
int l,r;
int lazy;
}t[maxn<<2];
int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn],num[maxn],tt[maxn];
void pushdown(int p)
{
if(t[p].lazy)
{
t[ls].lazy+=t[p].lazy;
t[rs].lazy+=t[p].lazy;
t[p].lazy=0;
}
}
void build(int p,int l,int r)
{
t[p].l=l,t[p].r=r,t[p].lazy=0;
if(l==r)
{
t[p].lazy=num[tt[l]];
return;
}
build(ls,l,mid);
build(rs,mid+1,r);
}
void add(int p,int l,int r,int val)
{
if(t[p].l==l&&t[p].r==r)
{
t[p].lazy+=val;
return;
}
pushdown(p);
if(r<=mid)
add(ls,l,r,val);
else if(l>mid)
add(rs,l,r,val);
else
{
add(ls,l,mid,val);
add(rs,mid+1,r,val);
}
}
int query(int p,int x)
{
if(t[p].l==t[p].r)
{
return t[p].lazy;
}
pushdown(p);
if(x>mid)
return query(rs,x);
else
return query(ls,x);
}
struct edge
{
int to;
int next;
}e[maxn<<1];
int box[maxn],cnt,tot;
void init()
{
tot=0;
son[0]=dep[0]=0;
memset(box,-1,sizeof(box));
cnt=0;
}
void add(int from,int to)
{
e[cnt].to=to;
e[cnt].next=box[from];
box[from]=cnt++;
}
void dfs(int now,int pre)
{
siz[now]=1;
fa[now]=pre;
son[now]=0;
dep[now]=dep[pre]+1;
int t,v;
for(t=box[now];t+1;t=e[t].next)
{
v=e[t].to;
if(v!=pre)
{
dfs(v,now);
siz[now]+=siz[v];
if(siz[son[now]]<siz[v])
{
son[now]=v;
}
}
}
}
void dfs2(int now,int tp)
{
w[now]=++tot;
tt[tot]=now;
top[now]=tp;
if(son[now])
dfs2(son[now],top[now]);
int t,v;
for(t=box[now];t+1;t=e[t].next)
{
v=e[t].to;
if(v!=fa[now]&&v!=son[now])
dfs2(v,v);
}
}
void solve(int a,int b,int val)
{
int f1=top[a],f2=top[b];
while(f1!=f2)
{
if(dep[f1]<dep[f2])
{
swap(f1,f2);
swap(a,b);
}
add(1,w[f1],w[a],val);
a=fa[f1];
f1=top[a];
}
if(a==b)
{
add(1,w[a],w[a],val);//注意这里
}
else
{
if(dep[a]>dep[b])
swap(a,b);
add(1,w[a],w[b],val);//注意这里
}
}
int main()
{
freopen("dd.txt","r",stdin);
int n,m,q,a,b,c;
char str[2];
while(scanf("%d%d%d",&n,&m,&q)!=EOF)
{
init();
int i;
for(i=1;i<=n;i++)
{
scanf("%d",&num[i]);
}
for(i=1;i<=m;i++)
{
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
}
dfs(1,0);
dfs2(1,1);
build(1,1,n);
while(q--)
{
int node;
scanf("%s",str);
if(str[0]=='Q')
{
scanf("%d",&node);
printf("%d\n",query(1,w[node]));
}
else
{
scanf("%d%d%d", &a,&b,&c);
if(str[0]=='I')
solve(a,b,c);
else
solve(a,b,-c);
}
}
}
return 0;
}
我已将需要注意的地方在代码中标记下来了,
区别就是在修改最后一条链时,也就是a,b在同一条重链中时,我们不妨设dep[a]<=dep[b],这时我们知道a是原来我们要求的v,w两点的LCA。因为我们树链剖分时,将重链放入线段树中时,事实上将点与边一一对应了,每个点对应于其父节点与其连接的边,对于根节点,可设置一个虚拟节点,把它看成根节点的父节点。这样在放入线段树中的操作就可以不变(其实还是为了实现方便)。如果权值在边上,那么我要求v,w两点间的路径时,其LCA所对应的边并不在这条路径里,所以我们要少更新一条边。如果权值在点上,则LCA显然也在v与w之间的路径中,则需要更新LCA。这就是两种题的不同点。
PS:其实树链剖分还有好多应用还有拓展,不过本弱菜还没有学得到,这里只是将最基本的应用总结出来,希望各位神牛不要BS。
PS2:DFS写法容易爆栈,所以还有非递归写法,如BFS写法和模拟栈等等,不过我还没研究出来。。。