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

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\n", 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\n", Qsub(u));
  182. }
  183. }
  184. return 0;
  185. }

浅谈树链剖分 F&Q的更多相关文章

  1. 蒟蒻浅谈树链剖分之一——两个dfs操作

    树链剖分,顾名思义就是将树形的结构剖分成链,我们以此便于在链上操作 首先我们需要明白在树链剖分中的一些概念 重儿子:某节点所有儿子中子树最多的儿子 重链:有重儿子构成的链 dfs序:按重儿子优先遍历时 ...

  2. 浅谈树链剖分(C++、算法、树结构)

    关于数链剖分我在网上看到的有几个比较好的讲解,本篇主要是对AC代码的注释(感谢各位witer的提供) 这是讲解 http://www.cnblogs.com/kuangbin/archive/2013 ...

  3. acm 2015北京网络赛 F Couple Trees 主席树+树链剖分

    提交 题意:给了两棵树,他们的跟都是1,然后询问,u,v 表 示在第一棵树上在u点往根节点走 , 第二棵树在v点往根节点走,然后求他们能到达的最早的那个共同的点 解: 我们将第一棵树进行书链剖,然后第 ...

  4. acm 2015北京网络赛 F Couple Trees 树链剖分+主席树

    Couple Trees Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://hihocoder.com/problemset/problem/123 ...

  5. 树链剖分 (求LCA,第K祖先,轻重链剖分、长链剖分)

      2020/4/30   15:55 树链剖分是一种十分实用的树的方法,用来处理LCA等祖先问题,以及对一棵树上的节点进行批量修改.权值和查询等有奇效. So, what is 树链剖分? 可以简单 ...

  6. bzoj 3637: Query on a tree VI 树链剖分 && AC600

    3637: Query on a tree VI Time Limit: 8 Sec  Memory Limit: 1024 MBSubmit: 206  Solved: 38[Submit][Sta ...

  7. 洛谷P4482 [BJWC2018]Border 的四种求法 字符串,SAM,线段树合并,线段树,树链剖分,DSU on Tree

    原文链接https://www.cnblogs.com/zhouzhendong/p/LuoguP4482.html 题意 给定一个字符串 S,有 q 次询问,每次给定两个数 L,R ,求 S[L.. ...

  8. BZOJ4012[HNOI2015]开店——树链剖分+可持久化线段树/动态点分治+vector

    题目描述 风见幽香有一个好朋友叫八云紫,她们经常一起看星星看月亮从诗词歌赋谈到 人生哲学.最近她们灵机一动,打算在幻想乡开一家小店来做生意赚点钱.这样的 想法当然非常好啦,但是她们也发现她们面临着一个 ...

  9. 培训补坑(day8:树上倍增+树链剖分)

    补坑补坑.. 其实挺不理解孙爷为什么把这两个东西放在一起讲..当时我学这一块数据结构都学了一周左右吧(超虚的) 也许孙爷以为我们是省队集训班... 好吧,虽然如此,我还是会认真写博客(保证初学者不会出 ...

随机推荐

  1. 【总结】HTTP

    一.HTTP 1.http HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),HTTP 是一个在计算机世界里专门在两点之间传输文字.图片.音频.视频等超文本数 ...

  2. 原生JS结合cookie实现商品评分组件

    开发思路如下: 1.利用JS直接操作DOM的方式开发商品评分组件,主要实现功能有:显示评价评分的样式以及将用户操作后对应的数据返回到主页面 2.主页面引入商品评分组件的js文件并根据规定格式的数据,生 ...

  3. Java学习的第三十天

    1.遇到打印文件使用打印流PrintStream 使用PrintStream写入数据 2.没有问题 3.明天学习用RandomAccessFile随机访问文件

  4. java中常见的六种线程池详解

    之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如 ...

  5. POJ2430 Lazy Cows

    题意描述 Lazy Cows 给定一个 \(2\times b\) 的矩形,和 \(n\) 个矩形上的点. 要求你用 \(k\) 个矩形覆盖这 \(n\) 个点,使得每个点都被覆盖的前提下这些矩形的面 ...

  6. 一文搞懂后台高性能服务器设计的常见套路, BAT 高频面试系列

    微信搜索「编程指北」,关注这个写干货的程序员,回复「资源」,即可获取后台开发学习路线和书籍 先赞后看,养成习惯~ 前言 金九银十,又是一年校招季. 经历过,才深知不易.最近,和作为校招面试官的同事聊了 ...

  7. c# 表达式树(一)

    前言 打算整理c# 代码简化史系列,所以相关的整理一下,简单的引出一下概念. 什么是表达式树呢? 表达式树以树形数据结构表示代码,其中每一个节点都是一种表达式,比如方法调用和 x < y 这样的 ...

  8. AE(After Effects)的简单使用——记一次模板套用的过程

    一.环境配置 操作系统:win10 64位 软件版本:AE16.11版 二.模板下载 可去 newcger 网站进行模板下载,常用都是免费的,很好用,很推荐 三.具体操作 1.模板素材替换 点击[文件 ...

  9. 美团笔试题_ACM最多队伍

    题目: 选择队伍参加ACM比赛,可以选择的人由N+M组成,N代表N个擅长算法的人,M代表M个擅长编程的人,选择要求:每个队伍三个人,每个队伍中至少有一个擅长算法的人,至少有一个擅长编程的人.求可以组成 ...

  10. js 元素添加多个监听

    function addListener(element,e,fn){     if(element.addEventListener){         element.addEventListen ...