Sweety

Practice makes perfect

导航

树链剖分(待整理)

Posted on 2017-05-01 22:11  蓝空  阅读(183)  评论(0编辑  收藏  举报

转自here

概述:

“在一棵树上进行路径的修改、求极值、求和”乍一看只要线段树就能轻松解决,实际上,仅凭线段树是不能搞定它的。我们需要用到一种貌似高级的复杂算法——树链剖分。


    

树链,就是树上的路径。剖分,就是把路径分类为重链和轻链。
记siz[v]表示以v为根的子树的节点数,dep[v]表示v的深度(根深度为1),top[v]表示v所在的链的顶端节点,fa[v]表示v的父亲,son[v]表示与v在同一重链上的v的儿子节点(姑且称为重儿子),w[v]表示v与其父亲节点的连边(姑且称为v的父边)在线段树中的位置。只要把这些东西求出来,就能用logn的时间完成原问题中的操作。


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


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




算法实现:
我们可以用两个dfs来求出fa、dep、siz、son、top、w。
dfs_1:把fa、dep、siz、son求出来,比较简单,略过。
dfs_2:⒈对于v,当son[v]存在(即v不是叶子节点)时,显然有top[son[v]] = top[v]。线段树中,v的重边应当在v的父边的后面,记w[son[v]] = totw+1,totw表示最后加入的一条边在线段树中的位置。此时,为了使一条重链各边在线段树中连续分布,应当进行dfs_2(son[v]);
       ⒉对于v的各个轻儿子u,显然有top[u] = u,并且w[u] = totw+1,进行dfs_2过程。
       这就求出了top和w。
将树中各边的权值在线段树中更新,建链和建线段树的过程就完成了。


修改操作:例如将u到v的路径上每条边的权值都加上某值x。
一般人需要先求LCA,然后慢慢修改u、v到公共祖先的边。而高手就不需要了。
记f1 = top[u],f2 = top[v]。
当f1 <> f2时:不妨设dep[f1] >= dep[f2],那么就更新u到f1的父边的权值(logn),并使u = fa[f1]。
当f1 = f2时:u与v在同一条重链上,若u与v不是同一点,就更新u到v路径上的边的权值(logn),否则修改完成;
重复上述过程,直到修改完成。


求和、求极值操作:类似修改操作,但是不更新边权,而是对其求和、求极值。

    就这样,原问题就解决了。鉴于鄙人语言表达能力有限,咱画图来看看:[转载]树链剖分

    如右图所示,较粗的为重边,较细的为轻边。节点编号旁边有个红色点的表明该节点是其所在链的顶端节点。边旁的蓝色数字表示该边在线段树中的位置。图中1-4-9-13-14为一条重链。

 当要修改11到10的路径时。
 第一次迭代:u = 11,v = 10,f1 = 2,f2 = 10。此时dep[f1] < dep[f2],因此修改线段树中的5号点,v = 4, f2 = 1;
 第二次迭代:dep[f1] > dep[f2],修改线段树中10--11号点。u = 2,f1 = 2;
 第三次迭代:dep[f1] > dep[f2],修改线段树中9号点。u = 1,f1 = 1;
 第四次迭代:f1 = f2且u = v,修改结束。



题目:spoj375、USACO December Contest Gold Divison, "grassplant"。

**spoj375据说不“缩行”情况下最短的程序是140+行,我的是128行。


附spoj375程序(C++):

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <string.h>
using namespace std;
const int maxn = 10010;
struct Tedge
{ int b, next; } e[maxn * 2];
int tree[maxn];
int zzz, n, z, edge, root, a, b, c;
int d[maxn][3];
int first[maxn], dep[maxn], w[maxn], fa[maxn], top[maxn], son[maxn], siz[maxn];
char ch[10];

void insert(int a, int b, int c)
{
     e[++edge].b = b;
     e[edge].next = first[a];
     first[a] = edge;
}

void dfs(int v)
{
     siz[v] = 1; son[v] = 0;
     for (int i = first[v]; i > 0; i = e[i].next)
         if (e[i].b != fa[v])
         {
             fa[e[i].b] = v;
             dep[e[i].b] = dep[v]+1;
             dfs(e[i].b);
             if (siz[e[i].b] > siz[son[v]]) son[v] = e[i].b;
             siz[v] += siz[e[i].b];
         }
}

void build_tree(int v, int tp)
{
     w[v] = ++ z; top[v] = tp;
     if (son[v] != 0) build_tree(son[v], top[v]);
     for (int i = first[v]; i > 0; i = e[i].next)
         if (e[i].b != son[v] && e[i].b != fa[v])
             build_tree(e[i].b, e[i].b);
}

void update(int root, int lo, int hi, int loc, int x)
{
     if (loc > hi || lo > loc) return;
     if (lo == hi)
     { tree[root] = x; return; }
     int mid = (lo + hi) / 2, ls = root * 2, rs = ls + 1;
     update(ls, lo, mid, loc, x);
     update(rs, mid+1, hi, loc, x);
     tree[root] = max(tree[ls], tree[rs]);
}

int maxi(int root, int lo, int hi, int l, int r)
{
     if (l > hi || r < lo) return 0;
     if (l <= lo && hi <= r) return tree[root];
     int mid = (lo + hi) / 2, ls = root * 2, rs = ls + 1;
     return max(maxi(ls, lo, mid, l, r), maxi(rs, mid+1, hi, l, r));
}

inline int find(int va, int vb)
{
     int f1 = top[va], f2 = top[vb], tmp = 0;
     while (f1 != f2)
     {
           if (dep[f1] < dep[f2])
           { swap(f1, f2); swap(va, vb); }
           tmp = max(tmp, maxi(1, 1, z, w[f1], w[va]));
           va = fa[f1]; f1 = top[va];
     }
     if (va == vb) return tmp;
     if (dep[va] > dep[vb]) swap(va, vb);
     return max(tmp, maxi(1, 1, z, w[son[va]], w[vb]));  //
}

void init()
{
     scanf("%d", &n);
     root = (n + 1) / 2;
     fa[root] = z = dep[root] = edge = 0;
     memset(siz, 0, sizeof(siz));
     memset(first, 0, sizeof(first));
     memset(tree, 0, sizeof(tree));
     for (int i = 1; i < n; i++)
     {
         scanf("%d%d%d", &a, &b, &c);
         d[i][0] = a; d[i][1] = b; d[i][2] = c;
         insert(a, b, c);
         insert(b, a, c);
     }
     dfs(root);
     build_tree(root, root);    //
     for (int i = 1; i < n; i++)
     {
         if (dep[d[i][0]] > dep[d[i][1]]) swap(d[i][0], d[i][1]);
         update(1, 1, z, w[d[i][1]], d[i][2]);
     }
}

inline void read()
{
     ch[0] = ' ';
     while (ch[0] < 'C' || ch[0] > 'Q') scanf("%s", &ch);
}

void work()
{
     for (read(); ch[0] != 'D'; read())
     {
         scanf("%d%d", &a, &b);
         if (ch[0] == 'Q') printf("%dn", find(a, b));
                      else update(1, 1, z, w[d[a][1]], b);
     }
}

int main()
{
    for (scanf("%d", &zzz); zzz > 0; zzz--)
    {
        init();
        work();
    }
    return 0;
}


转自kuangbin

最近一段时间决定先好好搞一下数据结构了,首先搞了下树链剖分;

专题训练:http://acm.hust.edu.cn/vjudge/contest/view.action?cid=28982#overview

树链剖分并不是一个复杂的算法或者数据结构,只是能把一棵树拆成链来处理而已,换一种说法,树链剖分只是xxx数据结构/算法在树上的推广,或者说,树链剖分只是把树hash到了几段连续的区间上

学习链接:

入门:http://blog.sina.com.cn/s/blog_7a1746820100wp67.html  (看了这个讲解,再把题目A掉就有点理解了)

https://quartergeek.com/summary-of-heavy-light-decomposition/


如下是辅助行说明:

here

树链剖分用一句话概括就是:把一棵树剖分为若干条链,然后利用数据结构(树状数组,SBT,Splay,线段树等等)去维护每一

条链,复杂度为O(logn)

 

 

那么,树链剖分的第一步当然是对树进行轻重边的划分。

定义size(x)为以x为根的子树节点个数,令v为u的儿子中size值最大的节点,那么(u,v)就是重边,其余边为轻边。

 

当然,关于这个它有两个重要的性质:

(1)轻边(u,v)中,size(v)<=size(u/2)

(2)从根到某一点的路径上,不超过logn条轻边和不超过logn条重路径。

 

 

当然,剖分过程分为两次dfs,或者bfs也可以。

 

如果是两次dfs,那么第一次dfs就是找重边,也就是记录下所有的重边。

然后第二次dfs就是连接重边形成重链,具体过程就是:以根节点为起点,沿着重边向下拓展,拉成重链,不在当前重链上的节

点,都以该节点为起点向下重新拉一条重链。

 

 

剖分完毕后,每条重链相当于一段区间,然后用数据结构去维护,把所有重链首尾相接,放到数据结构上,然后维护整体。

 

在这里,当然有很多数组,现在我来分别介绍它们的作用:

 

siz[]数组,用来保存以x为根的子树节点个数

top[]数组,用来保存当前节点的所在链的顶端节点

son[]数组,用来保存重儿子

dep[]数组,用来保存当前节点的深度

fa[]数组,用来保存当前节点的父亲

tid[]数组,用来保存树中每个节点剖分后的新编号

rank[]数组,用来保存当前节点在线段树中的位置



仔细想想 自己第一次听说这个这个数据结构大概有两年半的时间了 然而一直不会.

不过现在再回头来看 发现其实也不是很麻烦

首先 在学树链剖分之前最好先把LCALCA 树形DPDP 以及dfsdfs序 这三个知识点学了

如果这三个知识点没掌握好的话 树链剖分难以理解也是当然的

-------------------------------------------------------------------------------------------

树链剖分通常用于处理树的形态不变 但点权/边权需要修改查询的题目

在选定一个点作为根后 我们来对这棵树进行操作

 

第一步

 

从根开始进行一遍遍历((通常用dfs)dfs) 这一遍遍历需要统计以下信息

父亲节点fafa 当前节点深度dd 子树大小szsz 这些显然是在学习前置知识时已经用得比较熟练了的

然后 在这一遍遍历中 我们需要再求当前节点的重儿子sonson

重儿子的定义是 当前节点的子节点中 子树大小最大的那个 ((如果有多个任取一个))

其余的就是轻儿子了

 

另外所有节点与其重/轻儿子的连边称为重/轻边

把连续的重边定义为重链

我们会发现这样一个性质

从任一节点向根节点走 走过的重链和轻边的条数都是loglog级别以内的

 

证明如下:

由于任一轻儿子对应的子树大小要小于父节点所对应子树大小的一半

因此从一个轻儿子沿轻边向上走到父节点后 所对应的子树大小至少变为两倍以上

经过的轻边条数自然是不超过log2Nlog2N

然后由于重链都是间断的 ((连续的可以合成一条))

所以经过的重链的条数是不超过轻边条数+1+1

因此经过重链的条数也是loglog级别的

综合可知原命题得证

 

从轻边向上走显然每条轻边都可以做到O(1)O(1)向上走

而从重链向上走要做到每条重链只用O(1)O(1)就必须额外做一些处理

 

第二步

 

利用第一遍遍历得到的信息 我们再进行一遍遍历((需用dfs)dfs)

对于每个重儿子 要求出沿重链向上走走到顶端的点的位置toptop

这个toptop显然是和父节点的一样的

对于每个轻儿子 由于向上走的重链不存在 我们可以令toptop为自身

现在从重链向上走都只需要O(1)O(1)

不过修改的复杂度仍然不靠谱

对于两点之间的修改和询问操作 轻边我们可以暴力直接修改询问 重链则必须结合一些数据结构进行以保证效率

 

这个时候 我们或许会回想起学dfsdfs序的时候 我们将树上的点根据dfs序映射到了一维数组上

从而可以利用线段树等数据结构对在dfsdfs序上连续的一段区间进行修改和询问

因此 为了能够用线段树等数据结构进行维护 我们必须将同一条重链上的点映射到一个连续的区间

这个操作并不复杂 我们只需在对每个点dfsdfs时先遍历到它的重儿子 最后重链上的点映射到一维数组里便是连续的

 

做完这两个步骤后 树链剖分的核心部分就结束了

不过注意两个点向上走的时候 不能走得超过这两个点的LCALCA

这里的具体操作最好独立思考一下 对比倍增LCALCA的写法会更好理解

 

看到这里 大家大概也能反应到树链剖分的复杂度是O(NlogN+Qlog2N)