您的位置:首页 > 其它

树链剖分

2016-03-23 14:29 288 查看
这几天学习了一下树链剖分,个人感觉是线段树在树上的应用,也不能算是一种数据结构吧。比较常见的是对树边的修改和查询问题。

问题引入:

如果题目是在一棵树上进行路径的修改、求极值、求和等问题,大家很容易就会想到用线段树来做,但是这就存在一个问题:线段树存储的是一段连续区间上的信息,,而树的结构是分散的,那么怎么把树的节点或者树边和这个区间联系起来呢?树链剖分就可以解决这个问题。

首先,我们来解读一下算法名称:

1、树链,就是树上的路径。

2、剖分,就是把路径分类为重链和轻链。

树链剖分用一句话概括就是:把一棵树剖分为若干条链,然后利用数据结构(树状数组,SBT,Splay,线段树等等)去维护每一条链以解决问题,算法复杂度为O(logn)。

这里需要几个数组来保存树的信息:

  size[u] 表示以 u 为根的子树的节点数

  dep[u] 表示 u 的深度(根深度为1)

  top[u] 表示 u 所在的链的顶端节点

  fa[u] 表示u的父亲节点

  son[u] 表示与u在同一重链上的u的儿子节点(姑且称为重儿子)

  id[u] 表示u与其父亲节点的连边(姑且称为u的父边)在线段树中的位置

  rank[i] 表示线段树中第 i 个数对应树中的节点编号

涉及到的几个概念:

重儿子:size[v]为u的子节点中size值最大的,那么v就是u的重儿子。
轻儿子:u的其它子节点。
重边:点u与其重儿子的连边。
轻边:点u与其轻儿子的连边。
重链:由重边连成的路径。
轻链:轻边。

剖分后的树有如下性质:

性质1:如果(u,v)为轻边,则size[v] * 2 < size[u];
性质2:从根到某一点的路径上轻链、重链的个数都不大于logn。

算法实现:

一、首先,我们可以对树进行两遍 dfs 求得以上数组的值

第一次dfs:求出 dep, size, fa, son,记录所有的重边

void dfs_1( int u, int f, int d)    //主函数中调用dfs(1,0,1)
{
dep[u] = d;
size[u] = 1;
fa[u] = f;
for(int i = head[u];i != -1;i = edge[i].next)
{
int v = edge[i].to;
if(v == f)
continue;
dfs_1( v, u, d+1);
size[u] += size[v];
if(son[u] == -1 || size[son[u]] < size[v])
son[u] = v;
}
}


第二次dfs:连接重边成重链

void dfs_2( int u, int tp)     //主函数中调用dfs(1,1)
{
top[u] = tp;
id[u] = ++tmp;
rank[tmp] = u;
if(son[u] != -1)
dfs_2( son[u], tp);
for(int i = head[u];i != -1;i = edge[i].next)
{
int v = edge[i].to;
if(v != fa[u] && v != son[u])
dfs_2( v, v);
}
}


二、然后,建立线段树。将树中的权值在线段树中更新,这样建链和建线段树就完成了。具体实现可以看相关题解。

三、最后,就是修改和查询操作。单点的修改和查询很简单,只要将对应线段树中位置的信息修改或查询即可。重点要讲的是区间上的修改和查询,这两个操作写法类似,以修改为例。

如何修改u到v的边权的值呢?这里有两种情况:

(1)如果u与v在同一条重链上,那么就直接修改了

(2)如果u与v不在同一条重链上,那么就一边进行修改,一边将u与v往同一条重链上靠,这样就变成了第一种情况了

这样问题的关键就变成了如何将 u 和 v 往同一条重链上靠。

具体解法如下:

我们记 tp1 = top[u],tp2 = top[v]
(1) 当tp1 != tp2 时:不妨设dep[tp1] >= dep[tp2],那么就更新u到tp1的父边的权值(logn),并使u = fa[tp1]。
(2)当tp1 = tp2时:u与v在同一条重链上,若u与v不是同一点,就更新u到v路径上的边的权值(logn),否则修改完成;
重复上述过程,直到修改完成。

算法正确性证明:

  首先我们需要深入理解一下dfs_2 。可以看出,dfs_2的核心是将树按一条条重链保存到线段树中。在树中同一条重链的节点或边的信息在线段树中是连续的,而且是深度小的保存在前面,深度大的保存在后面,这样线段树中保存的是一段段重链的信息。

  如果需要修改 u 到 v 的信息,如果u和v不在同一条重链上,我们必定需要分别修改u 和 v 到其所在重链的头结点的信息,重复下去最终 u 到 v 的修改会演变成同一条重链上两个节点间的修改。算法证毕。

区间修改操作举例代码:

void change(int u, int v, int value)
{
int tp1 = top[u], tp2 = top[v];
while(tp1 != tp2)
{
if(dep[tp1] < dep[tp2])
{
swap( tp1, tp2);
swap( u, v);
}
update(1, id[tp1], id[u], value);
u = fa[tp1];
tp1 = top[u];
}
if(u == v)
return;
if(dep[u] > dep[v])
swap( u, v);
update(1, id[u], id[v], value);
}


区间查询操作举例代码:

int find( int u, int v)
{
int tp1 = top[u], tp2 = top[v];
int res = 0;
while(tp1 != tp2)
{
if(dep[tp1] < dep[tp2])
{
swap( tp1, tp2);
swap( u, v);
}
res = max(res, query( 1, id[tp1], id[u]));
u = fa[tp1];
tp1 = top[u];
}
if(u == v)
return res;
if(dep[u] > dep[v])
swap( u, v);
res = max( res, query(1, id[son[u]], id[v]));
return res;
}


以上都是针对树边操作的树链剖分,也有对树的节点进行修改查询的题目,方法类似,只需要在理解的时候改变一下 id 数组的含义:id[ u ]表示 u 节点在线段树中的位置。

当然,如果是对节点的修改和查询,我们就可以用树状数组而不是线段树来保存信息,这样代码会简单很多。

举例:

hdu 3966 /article/5857307.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: