线段树简单入门 (含普通线段树, zkw线段树, 主席树)
线段树简单入门
递归版线段树
线段树的定义
线段树, 顾名思义, 就是每个节点表示一个区间.
线段树通常维护一些区间的值, 例如区间和.
比如, 上图 \([2, 5]\) 区间的和, 为以下区间的和的和:
我们可以这样定义线段树的一个节点:
struct node {
int sum; // 维护该节点表示区间的和
int l, r; // 表示该节点表示的左右区间 (然而实现中常常不需要存储, 后面会说到)
int lc, rc; // 表示该节点的左右孩子 (然而实现中常常不需要存储, 后面会说到)
};
实现中, 我们常常使用完全二叉树来表示线段树, (如果你写过二叉堆的话你会知道要如何表示) 而在完全二叉树中一个节点的左孩子右孩子可以很方便的求出.
(若线段树不是完全二叉树, 可以假装它是完全二叉树, 毕竟这样比较方便)
因为常常我们只需要存储一个值 sum
, 于是下文只存储了一个数组 seg[]
代表所有节点的 sum
值.
线段树的基础操作
例题
已知一个数列,你需要进行下面两种操作:
将某一个数加上 \(x\)
求出某区间每一个数的和
\(0 \le n \le 2\times 10^5\)
更新节点
直接拿左孩子右孩子更新就可以了.
void Updata(int x) {
seg[x] = seg[x << 1] + seg[x << 1 | 1];
}
建树
暴力建就可以了.
void Build(int x, int l, int r, int a[]) { // 给 a[] 数组建线段树
if(l < r) {
int mid = (l + r) >> 1;
Build(x << 1, l, mid);
Build(x << 1 | 1, mid + 1, r);
Updata(x);
} else seg[x] = a[l];
}
修改节点
递归修改, 然后更新.
// cur 表示我们目前查询到了的节点编号
// [l, r] 表示 cur 这个节点所表示的区间
// 将 a[q] 改为 k
void Modify(int cur, int l, int r, int q, int k) {
if(l == r) {
seg[cur] = k;
} else {
int mid = (l + r) >> 1; // 左右子树的区间分别为 [l, mid], [mid + 1, r]
if(q <= mid) Modify(cur << 1, l, mid, q, k); // 要修改的节点在左孩子
else Modify(cur << 1 | 1, mid + 1, r, q, k); // 要修改的节点在右孩子
Updata(cur);
}
}
查询区间和
// cur 表示我们目前查询到了的节点编号
// [l, r] 表示 cur 这个节点所表示的区间
// [ql, qr] 表示我们要查询的区间
int Query(int cur, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) return seg[cur];
// 如果这个节点表示区间被 [ql, qr] 包含, 就返回这个节点的值
int mid = (l + r) >> 1, ret = 0; // 左右子树的区间分别为 [l, mid], [mid + 1, r]
if(ql <= mid) ret += Query(cur << 1, l, mid, ql, qr);
if(qr > mid) ret += Query(cur << 1 | 1, mid + 1, r, ql, qr);
return ret;
}
线段树的懒标记 (lazy tag)
区间修改 (区间增加某数)
我们想做区间修改.
显然暴力修改的时间复杂度是在太奇怪了.
我们考虑每次修改的信息要查询非常多次, 可以使用 lazy tag.
具体做法是: 我们先不增加, 而是标记这个节点需要增加, 到之后再来增加.
比如, 下传是这样的:
void PushTag(int x) {
tag[x << 1] += tag[x]; tag[x << 1 | 1] += tag[x];
int mid = (l + r) >> 1;
seg[x << 1] += (mid - l + 1) * tag[x]; // 左孩子为 [l, mid]
seg[x << 1 | 1] += (r - mid) * tag[x]; // 右孩子为 [mid + 1, r]
tag[x] = 0;
}
这样, 我们就可以写出来区间修改.
int Modify(int cur, int l, int r, int ql, int qr, int k) { // 区间 [ql, qr] 增加 k
if(ql <= l && r <= qr) {
seg[cur] += (r - l + 1) * k;
tag[cur] += k;
} else {
PushTag(cur);
int mid = (l + r) >> 1;
if(ql <= mid) Modify(cur << 1, l, mid, ql, qr, k);
if(qr > mid) Modify(cur << 1 | 1, mid + 1, r, ql, qr, k);
Updata(cur);
}
}
标记下传
每次访问节点都下传一下就可以了.
(此处用到了"均摊分析"的思想 (分析复杂度并不需要用均摊分析) )
暴力的时间复杂度到了哪里呢?
其实你可以发现: 每一次修改需要很多次查询才会把标记降完, 而再加上标记可以合并, 时间复杂度就为严格的 \(O(n \log n)\).
线段树的动态开点
直接用指针实现即可, 这里不多赘述.
zkw 线段树
引入
在我们写线段树的时候, 我们会发现:
化成二进制就是:
我们可以发现一些很有趣的性质, 我们可不可以自底向上做呢?
zkw 线段树的基础操作
建树
(因为后面操作的需要, 我们需要建立 \([0, n + 2)\) 的 zkw 线段树)
inline void Build() {
for(M = 1; M < n + 2; M <<= 1);
for(int i = M + 1; i <= M + n; i++) scanf("%d", seg + i); // 读入
for(int i = M - 1; i; i--) Updata(i);
// 更新所有值, 因为左右孩子编号一定比父亲的编号大, 所以父亲的孩子一定处理过
}
例如, 我们把 \([1, 9, 6, 2, 1, 0]\) 建树, 结果是这样的.
单点修改
可以发现会影响到的只会是要修改的节点的父亲, 直接更新父亲就可以了.
inline void Modify(int x, int v) {
seg[x += M] += v;
while(x) Updata(x >>= 1);
}
区间求和
我们先把开区间转化为闭区间. \([s, t] \to (s - 1, t + 1)\). (这就是我们需要开 \([0, n - 2\)) 的原因)
假设我们需要统计的是红色方框内的东西.
找出 \(s - 1\) 和 \(t + 1\) 的所有深度小于等于他们 LCA 的祖先, 可以发现:
我们要查询的区间恰好被包裹!
所以我们可以考虑这样的操作:
- \(s \leftarrow s + M - 1, t \leftarrow t + M + 1\). (找到叶子结点, 并且设置为开区间)
- 若 \(s\) 是左孩子, 则统计 \(s\) 的父亲的右孩子的答案. (显然这个节点在答案里)
- 若 \(t\) 是右孩子, 则统计 \(t\) 的父亲的左孩子的答案. (显然这个节点也在答案里)
- 将 \(s\) 与 \(t\) 都变为它们各自的父亲.
- 若 \(s\) 与 \(t\) 不是兄弟, 回到 2.
- 此时答案已经统计完毕.
为什么不会多加呢? 因为只有在它们是兄弟的时候才会多加, 而是兄弟的时候就已经停止了.
其它可以根据模拟感性理解.
代码如下:
inline int Query(int s, int t, int ret = 0) {
for(s += M - 1, t += M + 1; s ^ t ^ 1; s >>= 1, t >>= 1) {
if(~s & 1) ret += seg[s + 1];
if(t & 1) ret += seg[t - 1];
}
return ret;
}
## (可持久化线段树) 主席树
### 引入
如果我们要调用某一次修改之后的结果, 怎么做呢?
hjt 想出了一个非常好的办法 (据说是 hjt 考场上不会写划分树于是发明主席树) ~~到这里你应该知道这个名字是怎么来了的~~
### 函数式编程
函数式永远只做定义, 不做修改, 所以函数式编程自带可持久化.
### 主席树的基础操作
#### 单点修改
我们实际上只需要管修改就可以了.

对于每次修改一个节点, 我们都新建一个节点, 而不要修改原来的节点, 新建出来的节点和原来的节点的多数是一样的.
```cpp
node *NewNode(int val, node *lc, node *rc) {
node *ptr = new node;
ptr->lchild = lc; ptr->rchild = rc; ptr->val = val;
return ptr;
}
void Modify(node *&cur, node *fa, int l, int r, int x) {
cur = NewNode(fa->val + 1, fa->lchild, fa->rchild);
if(l != r) {
int mid = (l + r) >> 1;
if(x <= mid) Modify(cur->lchild, cur->lchild, l, mid, x);
else Modify(cur->rchild, cur->rchild, mid + 1, r, x);
}
}
(其实主席树比线段树短?)
区间修改
由于主席树不便于标记下传, 你可以使用标记永久化.
静态区间第 k 大
如果我们按原区间从左到右给编号为它的值的位置增加 \(1\), 则我们可以用第 \(r\) 个版本的某一个节点和减去第 \(l - 1\) 个版本的同一节点 (是表示区间相同) 和得到区间内有多少个在范围内的数.
考虑整体二分.
因为线段树的结构刚好适合二分, 所以我们不用再写 query 了.
int Query(node *u, node *v, int l, int r, int k) {
if(l == r) return l;
int mid = (l + r) >> 1, m = v->lchild->val - u->lchild->val;
if(m >= k)
return Query(u->lchild, v->lchild, l, mid, k);
else return Query(u->rchild, v->rchild, mid + 1, r, k - m);
}
结语
讲完了
祝大家身体健康
线段树简单入门 (含普通线段树, zkw线段树, 主席树)的更多相关文章
- zkw线段树——简单易懂好写好调的线段树
0.简介 zkw线段树是一种非递归线段树,与普通线段树不同的是,它是棵标准的满二叉树,所以遍历过程可全程使用位运算,常数一般比线段树小得多. 1.结构/建树 前面说了,zkw线段树是满二叉树,可是原数 ...
- 线段树(单标记+离散化+扫描线+双标记)+zkw线段树+权值线段树+主席树及一些例题
“队列进出图上的方向 线段树区间修改求出总量 可持久留下的迹象 我们 俯身欣赏” ----<膜你抄> 线段树很早就会写了,但一直没有总结,所以偶尔重写又会懵逼,所以还是要总结一下. ...
- 数据结构3——浅谈zkw线段树
线段树是所有数据结构中,最常用的之一.线段树的功能多样,既可以代替树状数组完成"区间和"查询,也可以完成一些所谓"动态RMQ"(可修改的区间最值问题)的操作.其 ...
- 洛谷P3834 [模板]可持久化线段树1(主席树) [主席树]
题目传送门 可持久化线段树1(主席树) 题目背景 这是个非常经典的主席树入门题——静态区间第K小 数据已经过加强,请使用主席树.同时请注意常数优化 题目描述 如题,给定N个正整数构成的序列,将对于指定 ...
- 普及向 ZKW线段树!
啊,是否疲倦了现在的线段树 太弱,还递归! 那我们就欢乐的学习另外一种神奇的线段树吧!(雾 他叫做zkw线段树 这个数据结构灰常好写(虽然线段树本身也特别好写……) 速度快(貌似只在单点更新方面比 ...
- 莫队或权值线段树 或主席树 p4137
题目描述 有一个长度为n的数组{a1,a2,…,an}.m次询问,每次询问一个区间内最小没有出现过的自然数. 输入格式 第一行n,m. 第二行为n个数. 从第三行开始,每行一个询问l,r. 输出格式 ...
- 牛客网 暑期ACM多校训练营(第一场)J.Different Integers-区间两侧不同数字的个数-离线树状数组 or 可持久化线段树(主席树)
J.Different Integers 题意就是给你l,r,问你在区间两侧的[1,l]和[r,n]中,不同数的个数. 两种思路: 1.将数组长度扩大两倍,for(int i=n+1;i<=2* ...
- 主席树入门(区间第k大)
主席树入门 时隔5个月,我又来填主席树的坑了,现在才发现学算法真的要懂了之后,再自己调试,慢慢写出来,如果不懂,就只会按照代码敲,是不会有任何提升的,都不如不照着敲. 所以搞算法一定要弄清原理,和代码 ...
- A - 低阶入门膜法 - K-th Number (主席树查询区间第k小)
题目链接:https://cn.vjudge.net/contest/284294#problem/A 题目大意:主席树查询区间第k小. 具体思路:主席树入门. AC代码: #include<i ...
随机推荐
- Gym - 101334F 单调栈
当时我的第一想法也是用单调栈,但是被我写炸了:我也不知道错在哪里: 看了大神的写法,用数组模拟的: 记录下单调递增栈的下标,以及每个数字作为最小值的最左边的位置. 当有数据要出栈的时候,说明栈里的数据 ...
- 【转】Activity、Window、View的关系
1.先看一个现象 1 2 3 4 5 6 7 8 9 10 11 public class MainActivity extends Activity { @Override pr ...
- Let’s Encrypt 最近很火的免费SSL 使用教程
2015年10月份,微博上偶然看到Let's Encrypt 推出了beta版,作为一个曾经被https虐出血的码农来说,这无疑是一个重磅消息.并且在全站Https的大趋势下,Let's Encryp ...
- JavaScript内存管理
低级语言,比如C,有低级的内存管理基元,想malloc(),free().另一方面,JavaScript的内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时"自动" ...
- Android onMeasure 方法的测量规范MeasureSpec
一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求.一个MeasureSpec由大小和模式组成.它有三种模式:UNSPECIFIED(未 ...
- Question20180106 Java环境变量的配置及为什么要配置环境变量
Question 1 Java环境变量的配置及为什么要配置环境变量 Q1.1为什么要配置环境变量 在学习JAVA的过程中,涉及到多个环境变量(environment variable)的概念,如PA ...
- oracle-03 表的管理
一.表名和列名的命名规则1).必须以字母开头2).长度不能超过30个字符3).不能使用oracle的保留字4).只能使用如下字符 a-z,a-z,0-9,$,#等 二.数据类型1).字符类char 长 ...
- xcode7--iOS开发---将app打包发布至app store
时隔3个月再次接触应用打包,又是一顿折腾 说说这次的感受吧: 变得是打包时间减少到4小时(其中大部分时间还是xcode7或者是iOS9的原因),不变的是还是一如既往的坑!! 好了,废话不多说,下面讲讲 ...
- java实现简单计算器功能
童鞋们,是不是有使用计算器的时候,还要进入运行,输入calc,太麻烦了,有时候甚至还忘记单词怎么拼写,呵呵程序员自己写代码实现,又简单,又方便啊 以下为代码(想要生成可执行工具可参考:http://w ...
- 【PTA 天梯赛训练】六度空间(广搜)
“六度空间”理论又称作“六度分隔(Six Degrees of Separation)”理论.这个理论可以通俗地阐述为:“你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过五个人你就能够 ...