zoukankan      html  css  js  c++  java
  • 树链剖分

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

    问题引入

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

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

    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

  • 相关阅读:
    排序算法:冒泡排序
    排序算法: 插入排序
    排序算法:折半插入排序
    排序算法:快速排序
    浅谈C/C++回调函数(Callback)& 函数指针
    仿函数
    回溯法:求和
    动态规划:数字和为sum的方法数
    字典序
    剑指offer15 二进制中1的个数
  • 原文地址:https://www.cnblogs.com/yaoyueduzhen/p/5308532.html
Copyright © 2011-2022 走看看