浅谈树链剖分 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板子居然跑得和倍增一样快)
至此树剖的整体思路已经明了,我们在剖分出的线性区间内架起一棵线段树来维护树上信息即可。下面附上我的树剖代码,码风应该比较清爽,其中线段树的写法也是从大佬那里抄来的精华。希望对各位学习树剖的同学有所帮助。
- #include <iostream>
- #include <cctype>
- #include <cstdio>
- #define maxn 100010
- #define BUG putchar('*')
- using namespace std;
- template <typename T>
- void read(T &x) {
- x = 0;
- int f = 1;
- char ch = getchar();
- while (!isdigit(ch)) {
- if (ch == '-')
- f = -1;
- ch = getchar();
- }
- while (isdigit(ch)) {
- x = x * 10 + (ch ^ 48);
- ch = getchar();
- }
- x *= f;
- return;
- }
- int mod;
- int head[maxn], top;
- int n, m, root;
- int val[maxn], w[maxn]; //共计三个数据数组:原始值、树剖序对应值和线段树
- struct E {
- int to, nxt;
- } edge[maxn << 1];
- inline void insert(int u, int v) {
- edge[++top] = (E) {v, head[u]};
- head[u] = top;
- }
- namespace segement_tree { //线段树部分
- #define lc (nd<<1)
- #define rc ((nd<<1)|1)
- struct node {
- int val, len;
- inline friend node operator + (node a, node b) {
- return (node) {(a.val + b.val) % mod, a.len + b.len};
- }
- } data[maxn << 2];
- int tag[maxn << 2]; //lazy_tag只维护简单的加减
- inline node operator * (node a, int b) {
- return (node) {a.val + (b * a.len) % mod, a.len};
- }
- inline void put_tag(int nd, int del) {
- data[nd] = data[nd] * del;
- tag[nd] = tag[nd] + del;
- }
- inline void update(int nd) {
- data[nd] = data[lc] + data[rc];
- }
- inline void push_down(int nd) { //下推,同时释放当前节点的tag信息
- put_tag(lc, tag[nd]);
- put_tag(rc, tag[nd]);
- tag[nd] = 0;
- }
- void build(int nd, int l, int r) {
- if (l == r) {
- data[nd] = (node) {w[l], 1};
- return;
- }
- int mid = (l + r) >> 1;
- build(lc, l, mid);
- build(rc, mid + 1, r);
- update(nd);
- }
- void modify(int nd, int l, int r, int ql, int qr, int del) {
- if (l >= ql && r <= qr) {
- put_tag(nd, del);
- return;
- } else if (l > qr || r < ql)
- return;
- push_down(nd);
- int mid = (l + r) >> 1;
- modify(lc, l, mid, ql ,qr, del);
- modify(rc, mid + 1, r, ql, qr, del);
- update(nd);
- }
- int query(int nd, int l, int r, int ql, int qr) {
- if (l >= ql && r <= qr) {
- return data[nd].val;
- } else if (l > qr || r < ql)
- return 0;
- push_down(nd);
- int mid = (l + r) >> 1;
- return (query(lc, l, mid, ql, qr) + query(rc, mid + 1, r, ql, qr)) % mod;
- }
- }
- namespace Divtree {
- using namespace segement_tree; //我感觉这个写法挺好的,表明了树剖对线段树的调用关系
- int ftop[maxn], f[maxn], size[maxn], son[maxn], d[maxn];
- int timer;
- int id[maxn];//树剖序
- void dfs1(int u, int pre) { //第一次dfs,维护节点的深度、父亲、重量以及重儿子信息
- f[u] = pre;
- size[u] = 1;
- d[u] = d[pre] + 1;
- for (int i = head[u]; i; i = edge[i].nxt) {
- int v = edge[i].to;
- if (v == pre) continue;
- dfs1(v, u);
- size[u] += size[v];
- if (size[v] > size[son[u]])
- son[u] = v;
- }
- }
- void dfs2(int u, int tp) { //按树剖序遍历,把树上信息剖分在线性数组上
- ftop[u] = tp;
- id[u] = ++timer;
- w[timer] = val[u]; //拷贝
- if (son[u])
- dfs2(son[u], tp); //搭建当前重链
- for (int i = head[u]; i; i = edge[i].nxt) {
- int v = edge[i].to;
- if (v != f[u] && v != son[u])
- dfs2(v, v); //沿轻边搭建新的重链
- }
- }
- void init() {
- dfs1(root, root);
- dfs2(root, root);
- build(1, 1, n);
- }
- inline void Msub(int u, int del) {
- modify(1, 1, n, id[u], id[u] + size[u] - 1, del);
- }
- inline int Qsub(int u) {
- return query(1, 1, n, id[u], id[u] + size[u] - 1) % mod;
- }
- void Mrange(int u, int v, int del) {
- while (ftop[u] != ftop[v]) {
- if (d[ftop[u]] < d[ftop[v]])
- swap(u, v);
- modify(1, 1, n, id[ftop[u]], id[u], del);
- u = f[ftop[u]];
- }
- if (d[u] > d[v]) swap(u, v);
- modify(1, 1, n, id[u], id[v], del);
- }
- int Qrange(int u, int v) {
- int ans = 0;
- while (ftop[u] != ftop[v]) {
- if (d[ftop[u]] < d[ftop[v]])
- swap(u, v);
- ans = (ans + query(1, 1, n, id[ftop[u]], id[u])) % mod;
- u = f[ftop[u]];
- }
- if (d[u] > d[v]) swap(u, v);
- ans = (ans + query(1, 1, n, id[u], id[v])) % mod;
- return ans;
- }
- } using namespace Divtree;
- int main() {
- read(n), read(m), read(root), read(mod);
- int u, v, del;
- for (int i = 1; i <= n; ++i)
- read(val[i]);
- for (int i = 1; i < n; ++i) {
- read(u), read(v);
- insert(u, v), insert(v, u);
- }
- init();
- register char ch;
- while (m--) {
- ch = getchar();
- while (!isdigit(ch)) ch = getchar();
- if (ch == '1') {
- read(u), read(v), read(del);
- Mrange(u, v, del);
- } else if (ch == '2') {
- read(u), read(v);
- printf("%d\n", Qrange(u, v));
- } else if (ch == '3') {
- read(u), read(del);
- Msub(u, del);
- } else {
- read(u);
- printf("%d\n", Qsub(u));
- }
- }
- return 0;
- }
浅谈树链剖分 F&Q的更多相关文章
- 蒟蒻浅谈树链剖分之一——两个dfs操作
树链剖分,顾名思义就是将树形的结构剖分成链,我们以此便于在链上操作 首先我们需要明白在树链剖分中的一些概念 重儿子:某节点所有儿子中子树最多的儿子 重链:有重儿子构成的链 dfs序:按重儿子优先遍历时 ...
- 浅谈树链剖分(C++、算法、树结构)
关于数链剖分我在网上看到的有几个比较好的讲解,本篇主要是对AC代码的注释(感谢各位witer的提供) 这是讲解 http://www.cnblogs.com/kuangbin/archive/2013 ...
- acm 2015北京网络赛 F Couple Trees 主席树+树链剖分
提交 题意:给了两棵树,他们的跟都是1,然后询问,u,v 表 示在第一棵树上在u点往根节点走 , 第二棵树在v点往根节点走,然后求他们能到达的最早的那个共同的点 解: 我们将第一棵树进行书链剖,然后第 ...
- acm 2015北京网络赛 F Couple Trees 树链剖分+主席树
Couple Trees Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://hihocoder.com/problemset/problem/123 ...
- 树链剖分 (求LCA,第K祖先,轻重链剖分、长链剖分)
2020/4/30 15:55 树链剖分是一种十分实用的树的方法,用来处理LCA等祖先问题,以及对一棵树上的节点进行批量修改.权值和查询等有奇效. So, what is 树链剖分? 可以简单 ...
- 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 ...
- 洛谷P4482 [BJWC2018]Border 的四种求法 字符串,SAM,线段树合并,线段树,树链剖分,DSU on Tree
原文链接https://www.cnblogs.com/zhouzhendong/p/LuoguP4482.html 题意 给定一个字符串 S,有 q 次询问,每次给定两个数 L,R ,求 S[L.. ...
- BZOJ4012[HNOI2015]开店——树链剖分+可持久化线段树/动态点分治+vector
题目描述 风见幽香有一个好朋友叫八云紫,她们经常一起看星星看月亮从诗词歌赋谈到 人生哲学.最近她们灵机一动,打算在幻想乡开一家小店来做生意赚点钱.这样的 想法当然非常好啦,但是她们也发现她们面临着一个 ...
- 培训补坑(day8:树上倍增+树链剖分)
补坑补坑.. 其实挺不理解孙爷为什么把这两个东西放在一起讲..当时我学这一块数据结构都学了一周左右吧(超虚的) 也许孙爷以为我们是省队集训班... 好吧,虽然如此,我还是会认真写博客(保证初学者不会出 ...
随机推荐
- AWS Lambda 借助 Serverless Framework,迅速起飞
前言 微服务架构有别于传统的单体式应用方案,我们可将单体应用拆分成多个核心功能.每个功能都被称为一项服务,可以单独构建和部署,这意味着各项服务在工作时不会互相影响 这种设计理念被进一步应用,就变成了无 ...
- redis方法-
//链接错误注意 //1.防火墙 //2.配置文件IP绑定 $redis = new Redis(); //连接redis $redis->connect('127.0.0.1', 6379); ...
- 关于python递归函数,这样写就对了
大家好我是致力于让每个人都能够轻松学会编程的小梁,在这条路上任重道远,关注我,每天让您获取来自编程的乐趣. 关注公众号"轻松学编程".了解更多. 今天就给大家分享一下关于使用递归函 ...
- 【Java】阿里巴巴开发规范手册
Java 开发手册 一. 编程规约 (一) 命名风格 [强制]代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束. 反例: _name, $name, __name [强制]代码中 ...
- SQL 速查表
关系数据库,基于关系模型,使用关系(表)存储数据,同时定义了完整性约束.常见的关系数据库系统包括:Oracle.MySQL/MariaDB.SQL Server.PostgreSQL 等等. SQL, ...
- 5.1 ICommand
1. CanExecute实现 只是将命令绑定到特定控件是不会触发CanExecute方法执行,该方法只是在实例化命令的时候才使用,其余如果需要触发该事件,只能通过后台代码组合触发,如更新一个后台类字 ...
- HTML5+CSS3 QQ会员页面导航
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...
- Spring Cloud 整合分布式链路追踪系统Sleuth和ZipKin实战,分析系统瓶颈
导读 微服务架构中,是否遇到过这种情况,服务间调用链过长,导致性能迟迟上不去,不知道哪里出问题了,巴拉巴拉....,回归正题,今天我们使用SpringCloud组件,来分析一下微服务架构中系统调用的瓶 ...
- [MIT6.006] 4. Heaps and Heap Sort 堆,堆排序
第4节课仍然是讲排序,但介绍的是一种很高效的堆排序. 在编程过程中,有时候会需要进行extrat_max的操作,即从一个数列里挨个抽取最大值并将其它从原数列中移除.而排序问题也可以看作是一个extra ...
- C# 中的数字分隔符 _
编写 C# 代码时,我们时常会用到很大的数字,例如下面定义的变量: const long loops = 50000000000; 您能快速读出这是多少吗?是不是还是会有很多人把光标定位到最后一位,然 ...