树链剖分

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

问题引入

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

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

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,记录所有的重边

 1 void dfs_1( int u, int f, int d)    //主函数中调用dfs(1,0,1)
 2 {
 3     dep[u] = d;
 4     size[u] = 1;
 5     fa[u] = f;
 6     for(int i = head[u];i != -1;i = edge[i].next)
 7     {
 8         int v = edge[i].to;
 9         if(v == f)
10             continue;
11         dfs_1( v, u, d+1);
12         size[u] += size[v];
13         if(son[u] == -1 || size[son[u]] < size[v])
14             son[u] = v;
15     }
16 }

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

 1 void dfs_2( int u, int tp)     //主函数中调用dfs(1,1)
 2 {
 3     top[u] = tp;
 4     id[u] = ++tmp;
 5     rank[tmp] = u;
 6     if(son[u] != -1)
 7         dfs_2( son[u], tp);
 8     for(int i = head[u];i != -1;i = edge[i].next)
 9     {
10         int v = edge[i].to;
11         if(v != fa[u] && v != son[u])
12             dfs_2( v, v);
13     }
14 }

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

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

如何修改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 的修改会演变成同一条重链上两个节点间的修改。算法证毕。

区间修改操作举例代码:

 1 void change(int u, int v, int value)
 2 {
 3     int tp1 = top[u], tp2 = top[v];
 4     while(tp1 != tp2)
 5     {
 6         if(dep[tp1] < dep[tp2])
 7         {
 8             swap( tp1, tp2);
 9             swap( u, v);
10         }
11         update(1, id[tp1], id[u], value);
12         u = fa[tp1];
13         tp1 = top[u];
14     }
15     if(u == v)
16          return;
17     if(dep[u] > dep[v])
18         swap( u, v);
19     update(1, id[u], id[v], value);
20 }

区间查询操作举例代码:

 1 int find( int u, int v)
 2 {
 3     int tp1 = top[u], tp2 = top[v];
 4     int res = 0;
 5     while(tp1 != tp2)
 6     {
 7         if(dep[tp1] < dep[tp2])
 8         {
 9             swap( tp1, tp2);
10             swap( u, v);
11         }
12         res = max(res, query( 1, id[tp1], id[u]));
13         u = fa[tp1];
14         tp1 = top[u];
15     }
16     if(u == v)
17         return res;
18     if(dep[u] > dep[v])
19         swap( u, v);
20     res = max( res, query(1, id[son[u]], id[v]));
21     return res;
22 }

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

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

举例:

hdu 3966   http://www.cnblogs.com/yaoyueduzhen/p/5311116.html

原文地址:https://www.cnblogs.com/yaoyueduzhen/p/5308532.html