• 浅谈树链剖分 F&Q


      这是一篇迟来的博客,由于我懒得写文章,本篇以两个问题阐述笔者对树链剖分的初步理解。

    Q1:树链剖分解决什么问题?

      树链剖分,就是把一棵树剖分成若干连续的链,将这些链里的数据映射在线性数组上维护。比方说我们想要维护树上任意两点间的lca,或者支持一段路径或一棵子树的修改和查询,都可以用树链剖分来解决。

    Q2:树链剖分是什么原理?

      以经典的重链剖分为例,列举笔者认为是树剖核心的两个基本数据结构性质:

      1、树剖序:树剖所维护的几个基本信息如dfn(时间戳)、size(子树大小)、fa(树上父亲),在这里不过多赘述。这里只分析基于son(重儿子)所得出的树剖序的本质。

      我们在打时间戳遍历原树的时候,实际上做了遍特殊的dfs,这个dfs以优先遍历重儿子为原则,对应的dfs序实际上就是树剖序。换言之,树剖序是一种特殊的dfs序,它首先符合dfs序的特征,如“u的子树在树剖序里是一段连续区间”这样的性质支持我们可以用区间数据结构(线段树)来维护子树的信息。

      同时,树剖序有自己的独特性质。相信能看到这篇博客的同学都对所谓“重链”有了了解:每条重链都是由叶子结点沿返祖边指向祖先方向的一条树链,它上面除了最浅的一个点以外的所有结点都是对应祖先的重儿子。两条重链之间通过一条轻边衔接。既然我们优先遍历重儿子,每段重链在树剖序上都是一段连续的区间:而每个节点要么是重儿子(在重链上),要么是某条重链的起点;那么每个节点都一定且仅属于某条重链。所以我们按树剖序得到的线性数组就是由若干条重链拼成的,我们可以在其上用维护区间的方法便可以维护整棵树的信息。

      2、重链的优越性

      那么,树链剖分的高效性何在?首先,我们记录每个点所在重链的链头,从某一点转移到链头的复杂度是O(1)的。修改、查询每段树链用线段树log级维护。而我们需要沿轻边跳跃多少次就可以覆盖整条路径呢?

      引理:任一点到根节点的轻边最多有log条。

      这实际上是个很显然的性质:最坏的情况就是每条边都是轻边,每次从轻儿子v跳到u,由于size[v] < size[son[u]],子树的大小便至少会增加一倍,那么最多会增加log次,这就是我们要按轻重剖分的原因。

      于是树链剖分每次询问都1最坏时间为O(logn*logn),实际上很难有极端数据来卡到这个限度,一般还是很优秀的。(笔者用树链剖分打出的lca板子居然跑得和倍增一样快)

      至此树剖的整体思路已经明了,我们在剖分出的线性区间内架起一棵线段树来维护树上信息即可。下面附上我的树剖代码,码风应该比较清爽,其中线段树的写法也是从大佬那里抄来的精华。希望对各位学习树剖的同学有所帮助。

    1. #include <iostream>  
    2. #include <cctype>  
    3. #include <cstdio>  
    4. #define maxn 100010  
    5. #define BUG putchar('*')  
    6. using namespace std;  
    7. template <typename T>  
    8. void read(T &x) {  
    9.     x = 0;  
    10.     int f = 1;  
    11.     char ch = getchar();  
    12.     while (!isdigit(ch)) {  
    13.         if (ch == '-')  
    14.             f = -1;  
    15.         ch = getchar();  
    16.     }  
    17.     while (isdigit(ch)) {  
    18.         x = x * 10 + (ch ^ 48);  
    19.         ch = getchar();  
    20.     }  
    21.     x *= f;  
    22.     return;  
    23. }  
    24. int mod;  
    25. int head[maxn], top;  
    26. int n, m, root;  
    27. int val[maxn], w[maxn];  //共计三个数据数组:原始值、树剖序对应值和线段树
    28. struct E {  
    29.     int to, nxt;  
    30. } edge[maxn << 1];  
    31. inline void insert(int u, int v) {  
    32.     edge[++top] = (E) {v, head[u]};  
    33.     head[u] = top;  
    34. }  
    35. namespace segement_tree {   //线段树部分
    36.     #define lc (nd<<1)  
    37.     #define rc ((nd<<1)|1)  
    38.     struct node {  
    39.         int val, len;  
    40.         inline friend node operator + (node a, node b) {  
    41.             return (node) {(a.val + b.val) % mod, a.len + b.len};  
    42.         }  
    43.     } data[maxn << 2];  
    44.     int tag[maxn << 2];   //lazy_tag只维护简单的加减
    45.     inline node operator * (node a, int b) {  
    46.         return (node) {a.val + (b * a.len) % mod, a.len};  
    47.     }  
    48.     inline void put_tag(int nd, int del) {  
    49.         data[nd] = data[nd] * del;  
    50.         tag[nd] = tag[nd] + del;  
    51.     }  
    52.     inline void update(int nd) {  
    53.         data[nd] = data[lc] + data[rc];  
    54.     }  
    55.     inline void push_down(int nd) {   //下推,同时释放当前节点的tag信息
    56.         put_tag(lc, tag[nd]);  
    57.         put_tag(rc, tag[nd]);  
    58.         tag[nd] = 0;  
    59.     }  
    60.     void build(int nd, int l, int r) {  
    61.         if (l == r) {  
    62.             data[nd] = (node) {w[l], 1};  
    63.             return;  
    64.         }  
    65.         int mid = (l + r) >> 1;  
    66.         build(lc, l, mid);  
    67.         build(rc, mid + 1, r);  
    68.         update(nd);  
    69.     }  
    70.     void modify(int nd, int l, int r, int ql, int qr, int del) {  
    71.         if (l >= ql && r <= qr) {  
    72.             put_tag(nd, del);  
    73.             return;  
    74.         } else if (l > qr || r < ql)  
    75.             return;  
    76.         push_down(nd);  
    77.         int mid = (l + r) >> 1;  
    78.         modify(lc, l, mid, ql ,qr, del);  
    79.         modify(rc, mid + 1, r, ql, qr, del);  
    80.         update(nd);  
    81.     }  
    82.     int query(int nd, int l, int r, int ql, int qr) {  
    83.         if (l >= ql && r <= qr) {  
    84.             return data[nd].val;  
    85.         } else if (l > qr || r < ql)  
    86.             return 0;  
    87.         push_down(nd);  
    88.         int mid = (l + r) >> 1;  
    89.         return (query(lc, l, mid, ql, qr) + query(rc, mid + 1, r, ql, qr)) % mod;  
    90.     }  
    91. }  
    92. namespace Divtree {  
    93.     using namespace segement_tree;  //我感觉这个写法挺好的,表明了树剖对线段树的调用关系
    94.     int ftop[maxn], f[maxn], size[maxn], son[maxn], d[maxn];  
    95.     int timer;  
    96.     int id[maxn];//树剖序   
    97.     void dfs1(int u, int pre) {   //第一次dfs,维护节点的深度、父亲、重量以及重儿子信息
    98.         f[u] = pre;  
    99.         size[u] = 1;  
    100.         d[u] = d[pre] + 1;  
    101.         for (int i = head[u]; i; i = edge[i].nxt) {  
    102.             int v = edge[i].to;  
    103.             if (v == pre) continue;  
    104.             dfs1(v, u);  
    105.             size[u] += size[v];  
    106.             if (size[v] > size[son[u]])  
    107.                 son[u] = v;  
    108.         }  
    109.     }  
    110.     void dfs2(int u, int tp) {    //按树剖序遍历,把树上信息剖分在线性数组上
    111.         ftop[u] = tp;  
    112.         id[u] = ++timer;  
    113.         w[timer] = val[u];   //拷贝
    114.         if (son[u])  
    115.             dfs2(son[u], tp);  //搭建当前重链
    116.         for (int i = head[u]; i; i = edge[i].nxt) {  
    117.             int v = edge[i].to;  
    118.             if (v != f[u] && v != son[u])  
    119.                 dfs2(v, v);   //沿轻边搭建新的重链
    120.         }  
    121.     }  
    122.     void init() {  
    123.         dfs1(root, root);  
    124.         dfs2(root, root);  
    125.         build(1, 1, n);  
    126.     }  
    127.     inline void Msub(int u, int del) {  
    128.         modify(1, 1, n, id[u], id[u] + size[u] - 1, del);   
    129.     }  
    130.     inline int Qsub(int u) {  
    131.         return query(1, 1, n, id[u], id[u] + size[u] - 1) % mod;  
    132.     }  
    133.     void Mrange(int u, int v, int del) {  
    134.         while (ftop[u] != ftop[v]) {  
    135.             if (d[ftop[u]] < d[ftop[v]])  
    136.                 swap(u, v);  
    137.             modify(1, 1, n, id[ftop[u]], id[u], del);   
    138.             u = f[ftop[u]];  
    139.         }  
    140.         if (d[u] > d[v]) swap(u, v);  
    141.         modify(1, 1, n, id[u], id[v], del);  
    142.     }  
    143.     int Qrange(int u, int v) {  
    144.         int ans = 0;  
    145.         while (ftop[u] != ftop[v]) {  
    146.             if (d[ftop[u]] < d[ftop[v]])  
    147.                 swap(u, v);  
    148.             ans = (ans + query(1, 1, n, id[ftop[u]], id[u])) % mod;  
    149.             u = f[ftop[u]];  
    150.         }  
    151.         if (d[u] > d[v]) swap(u, v);  
    152.         ans = (ans + query(1, 1, n, id[u], id[v])) % mod;  
    153.         return ans;  
    154.     }  
    155. using namespace Divtree;  
    156. int main() {  
    157.     read(n), read(m), read(root), read(mod);  
    158.     int u, v, del;  
    159.     for (int i = 1; i <= n; ++i)  
    160.         read(val[i]);  
    161.     for (int i = 1; i < n; ++i) {  
    162.         read(u), read(v);  
    163.         insert(u, v), insert(v, u);  
    164.     }  
    165.     init();  
    166.     register char ch;  
    167.     while (m--) {  
    168.         ch = getchar();  
    169.         while (!isdigit(ch)) ch = getchar();  
    170.         if (ch == '1') {  
    171.             read(u), read(v), read(del);  
    172.             Mrange(u, v, del);  
    173.         } else if (ch == '2') {  
    174.             read(u), read(v);  
    175.             printf("%d ", Qrange(u, v));  
    176.         } else if (ch == '3') {  
    177.             read(u), read(del);  
    178.             Msub(u, del);  
    179.         } else {  
    180.             read(u);  
    181.             printf("%d ", Qsub(u));  
    182.         }  
    183.     }  
    184.     return 0;  
    185. }  
  • 相关阅读:
    Hibernate的注释该如何使用?每一个注释代表什么意思?
    J2SE总结(一)-------容器
    解决hibernate向mysql插入中文乱码问题(更改MySQL字符集)
    android程序员成长路径的思考
    Fragment总结
    onCreateView的一个细节--Fragment
    屏幕适配
    表驱动法3
    表驱动法2
    表驱动法1
  • 原文地址:https://www.cnblogs.com/TY02/p/11147226.html
Copyright © 2020-2023  润新知