线段树简单入门

递归版线段树

线段树的定义

线段树, 顾名思义, 就是每个节点表示一个区间.

线段树通常维护一些区间的值, 例如区间和.

比如, 上图 \([2, 5]\) 区间的和, 为以下区间的和的和:

我们可以这样定义线段树的一个节点:

  1. struct node {
  2. int sum; // 维护该节点表示区间的和
  3. int l, r; // 表示该节点表示的左右区间 (然而实现中常常不需要存储, 后面会说到)
  4. int lc, rc; // 表示该节点的左右孩子 (然而实现中常常不需要存储, 后面会说到)
  5. };

实现中, 我们常常使用完全二叉树来表示线段树, (如果你写过二叉堆的话你会知道要如何表示) 而在完全二叉树中一个节点的左孩子右孩子可以很方便的求出.

(若线段树不是完全二叉树, 可以假装它是完全二叉树, 毕竟这样比较方便)

因为常常我们只需要存储一个值 sum, 于是下文只存储了一个数组 seg[] 代表所有节点的 sum 值.

线段树的基础操作

例题

Luogu 树状数组

已知一个数列,你需要进行下面两种操作:

  1. 将某一个数加上 \(x\)

  2. 求出某区间每一个数的和

\(0 \le n \le 2\times 10^5\)

更新节点

直接拿左孩子右孩子更新就可以了.

  1. void Updata(int x) {
  2. seg[x] = seg[x << 1] + seg[x << 1 | 1];
  3. }

建树

暴力建就可以了.

  1. void Build(int x, int l, int r, int a[]) { // 给 a[] 数组建线段树
  2. if(l < r) {
  3. int mid = (l + r) >> 1;
  4. Build(x << 1, l, mid);
  5. Build(x << 1 | 1, mid + 1, r);
  6. Updata(x);
  7. } else seg[x] = a[l];
  8. }

修改节点

递归修改, 然后更新.

  1. // cur 表示我们目前查询到了的节点编号
  2. // [l, r] 表示 cur 这个节点所表示的区间
  3. // 将 a[q] 改为 k
  4. void Modify(int cur, int l, int r, int q, int k) {
  5. if(l == r) {
  6. seg[cur] = k;
  7. } else {
  8. int mid = (l + r) >> 1; // 左右子树的区间分别为 [l, mid], [mid + 1, r]
  9. if(q <= mid) Modify(cur << 1, l, mid, q, k); // 要修改的节点在左孩子
  10. else Modify(cur << 1 | 1, mid + 1, r, q, k); // 要修改的节点在右孩子
  11. Updata(cur);
  12. }
  13. }

查询区间和

  1. // cur 表示我们目前查询到了的节点编号
  2. // [l, r] 表示 cur 这个节点所表示的区间
  3. // [ql, qr] 表示我们要查询的区间
  4. int Query(int cur, int l, int r, int ql, int qr) {
  5. if(ql <= l && r <= qr) return seg[cur];
  6. // 如果这个节点表示区间被 [ql, qr] 包含, 就返回这个节点的值
  7. int mid = (l + r) >> 1, ret = 0; // 左右子树的区间分别为 [l, mid], [mid + 1, r]
  8. if(ql <= mid) ret += Query(cur << 1, l, mid, ql, qr);
  9. if(qr > mid) ret += Query(cur << 1 | 1, mid + 1, r, ql, qr);
  10. return ret;
  11. }

线段树的懒标记 (lazy tag)

区间修改 (区间增加某数)

我们想做区间修改.

显然暴力修改的时间复杂度是在太奇怪了.

我们考虑每次修改的信息要查询非常多次, 可以使用 lazy tag.

具体做法是: 我们先不增加, 而是标记这个节点需要增加, 到之后再来增加.

比如, 下传是这样的:

  1. void PushTag(int x) {
  2. tag[x << 1] += tag[x]; tag[x << 1 | 1] += tag[x];
  3. int mid = (l + r) >> 1;
  4. seg[x << 1] += (mid - l + 1) * tag[x]; // 左孩子为 [l, mid]
  5. seg[x << 1 | 1] += (r - mid) * tag[x]; // 右孩子为 [mid + 1, r]
  6. tag[x] = 0;
  7. }

这样, 我们就可以写出来区间修改.

  1. int Modify(int cur, int l, int r, int ql, int qr, int k) { // 区间 [ql, qr] 增加 k
  2. if(ql <= l && r <= qr) {
  3. seg[cur] += (r - l + 1) * k;
  4. tag[cur] += k;
  5. } else {
  6. PushTag(cur);
  7. int mid = (l + r) >> 1;
  8. if(ql <= mid) Modify(cur << 1, l, mid, ql, qr, k);
  9. if(qr > mid) Modify(cur << 1 | 1, mid + 1, r, ql, qr, k);
  10. Updata(cur);
  11. }
  12. }

标记下传

每次访问节点都下传一下就可以了.

(此处用到了"均摊分析"的思想 (分析复杂度并不需要用均摊分析) )

暴力的时间复杂度到了哪里呢?

其实你可以发现: 每一次修改需要很多次查询才会把标记降完, 而再加上标记可以合并, 时间复杂度就为严格的 \(O(n \log n)\).

线段树的动态开点

直接用指针实现即可, 这里不多赘述.

zkw 线段树

引入

在我们写线段树的时候, 我们会发现:

化成二进制就是:

我们可以发现一些很有趣的性质, 我们可不可以自底向上做呢?

zkw 线段树的基础操作

建树

(因为后面操作的需要, 我们需要建立 \([0, n + 2)\) 的 zkw 线段树)

  1. inline void Build() {
  2. for(M = 1; M < n + 2; M <<= 1);
  3. for(int i = M + 1; i <= M + n; i++) scanf("%d", seg + i); // 读入
  4. for(int i = M - 1; i; i--) Updata(i);
  5. // 更新所有值, 因为左右孩子编号一定比父亲的编号大, 所以父亲的孩子一定处理过
  6. }

例如, 我们把 \([1, 9, 6, 2, 1, 0]\) 建树, 结果是这样的.

单点修改

可以发现会影响到的只会是要修改的节点的父亲, 直接更新父亲就可以了.

  1. inline void Modify(int x, int v) {
  2. seg[x += M] += v;
  3. while(x) Updata(x >>= 1);
  4. }

区间求和

我们先把开区间转化为闭区间. \([s, t] \to (s - 1, t + 1)\). (这就是我们需要开 \([0, n - 2​\)) 的原因)

假设我们需要统计的是红色方框内的东西.

找出 \(s - 1\) 和 \(t + 1\) 的所有深度小于等于他们 LCA 的祖先, 可以发现:

我们要查询的区间恰好被包裹!

所以我们可以考虑这样的操作:

  1. \(s \leftarrow s + M - 1, t \leftarrow t + M + 1\). (找到叶子结点, 并且设置为开区间)
  2. 若 \(s\) 是左孩子, 则统计 \(s​\) 的父亲的右孩子的答案. (显然这个节点在答案里)
  3. 若 \(t\) 是右孩子, 则统计 \(t\) 的父亲的左孩子的答案. (显然这个节点也在答案里)
  4. 将 \(s\) 与 \(t\) 都变为它们各自的父亲.
  5. 若 \(s\) 与 \(t\) 不是兄弟, 回到 2.
  6. 此时答案已经统计完毕.

为什么不会多加呢? 因为只有在它们是兄弟的时候才会多加, 而是兄弟的时候就已经停止了.

其它可以根据模拟感性理解.

代码如下:

  1. inline int Query(int s, int t, int ret = 0) {
  2. for(s += M - 1, t += M + 1; s ^ t ^ 1; s >>= 1, t >>= 1) {
  3. if(~s & 1) ret += seg[s + 1];
  4. if(t & 1) ret += seg[t - 1];
  5. }
  6. return ret;
  7. }
  8. ## (可持久化线段树) 主席树
  9. ### 引入
  10. 如果我们要调用某一次修改之后的结果, 怎么做呢?
  11. hjt 想出了一个非常好的办法 (据说是 hjt 考场上不会写划分树于是发明主席树) ~~到这里你应该知道这个名字是怎么来了的~~
  12. ### 函数式编程
  13. 函数式永远只做定义, 不做修改, 所以函数式编程自带可持久化.
  14. ### 主席树的基础操作
  15. #### 单点修改
  16. 我们实际上只需要管修改就可以了.
  17. ![yjhzkw4.png](https://i.loli.net/2019/02/28/5c7775065b31f.png)
  18. 对于每次修改一个节点, 我们都新建一个节点, 而不要修改原来的节点, 新建出来的节点和原来的节点的多数是一样的.
  19. ```cpp
  20. node *NewNode(int val, node *lc, node *rc) {
  21. node *ptr = new node;
  22. ptr->lchild = lc; ptr->rchild = rc; ptr->val = val;
  23. return ptr;
  24. }
  25. void Modify(node *&cur, node *fa, int l, int r, int x) {
  26. cur = NewNode(fa->val + 1, fa->lchild, fa->rchild);
  27. if(l != r) {
  28. int mid = (l + r) >> 1;
  29. if(x <= mid) Modify(cur->lchild, cur->lchild, l, mid, x);
  30. else Modify(cur->rchild, cur->rchild, mid + 1, r, x);
  31. }
  32. }

(其实主席树比线段树短?)

区间修改

由于主席树不便于标记下传, 你可以使用标记永久化.

静态区间第 k 大

如果我们按原区间从左到右给编号为它的值的位置增加 \(1\), 则我们可以用第 \(r\) 个版本的某一个节点和减去第 \(l - 1\) 个版本的同一节点 (是表示区间相同) 和得到区间内有多少个在范围内的数.

考虑整体二分.

因为线段树的结构刚好适合二分, 所以我们不用再写 query 了.

  1. int Query(node *u, node *v, int l, int r, int k) {
  2. if(l == r) return l;
  3. int mid = (l + r) >> 1, m = v->lchild->val - u->lchild->val;
  4. if(m >= k)
  5. return Query(u->lchild, v->lchild, l, mid, k);
  6. else return Query(u->rchild, v->rchild, mid + 1, r, k - m);
  7. }

结语

讲完了

祝大家身体健康

线段树简单入门 (含普通线段树, zkw线段树, 主席树)的更多相关文章

  1. zkw线段树——简单易懂好写好调的线段树

    0.简介 zkw线段树是一种非递归线段树,与普通线段树不同的是,它是棵标准的满二叉树,所以遍历过程可全程使用位运算,常数一般比线段树小得多. 1.结构/建树 前面说了,zkw线段树是满二叉树,可是原数 ...

  2. 线段树(单标记+离散化+扫描线+双标记)+zkw线段树+权值线段树+主席树及一些例题

    “队列进出图上的方向 线段树区间修改求出总量 可持久留下的迹象 我们 俯身欣赏” ----<膜你抄>     线段树很早就会写了,但一直没有总结,所以偶尔重写又会懵逼,所以还是要总结一下. ...

  3. 数据结构3——浅谈zkw线段树

    线段树是所有数据结构中,最常用的之一.线段树的功能多样,既可以代替树状数组完成"区间和"查询,也可以完成一些所谓"动态RMQ"(可修改的区间最值问题)的操作.其 ...

  4. 洛谷P3834 [模板]可持久化线段树1(主席树) [主席树]

    题目传送门 可持久化线段树1(主席树) 题目背景 这是个非常经典的主席树入门题——静态区间第K小 数据已经过加强,请使用主席树.同时请注意常数优化 题目描述 如题,给定N个正整数构成的序列,将对于指定 ...

  5. 普及向 ZKW线段树!

    啊,是否疲倦了现在的线段树 太弱,还递归! 那我们就欢乐的学习另外一种神奇的线段树吧!(雾 他叫做zkw线段树   这个数据结构灰常好写(虽然线段树本身也特别好写……) 速度快(貌似只在单点更新方面比 ...

  6. 莫队或权值线段树 或主席树 p4137

    题目描述 有一个长度为n的数组{a1,a2,…,an}.m次询问,每次询问一个区间内最小没有出现过的自然数. 输入格式 第一行n,m. 第二行为n个数. 从第三行开始,每行一个询问l,r. 输出格式 ...

  7. 牛客网 暑期ACM多校训练营(第一场)J.Different Integers-区间两侧不同数字的个数-离线树状数组 or 可持久化线段树(主席树)

    J.Different Integers 题意就是给你l,r,问你在区间两侧的[1,l]和[r,n]中,不同数的个数. 两种思路: 1.将数组长度扩大两倍,for(int i=n+1;i<=2* ...

  8. 主席树入门(区间第k大)

    主席树入门 时隔5个月,我又来填主席树的坑了,现在才发现学算法真的要懂了之后,再自己调试,慢慢写出来,如果不懂,就只会按照代码敲,是不会有任何提升的,都不如不照着敲. 所以搞算法一定要弄清原理,和代码 ...

  9. A - 低阶入门膜法 - K-th Number (主席树查询区间第k小)

    题目链接:https://cn.vjudge.net/contest/284294#problem/A 题目大意:主席树查询区间第k小. 具体思路:主席树入门. AC代码: #include<i ...

随机推荐

  1. Gym - 101334F 单调栈

    当时我的第一想法也是用单调栈,但是被我写炸了:我也不知道错在哪里: 看了大神的写法,用数组模拟的: 记录下单调递增栈的下标,以及每个数字作为最小值的最左边的位置. 当有数据要出栈的时候,说明栈里的数据 ...

  2. 【转】Activity、Window、View的关系

    1.先看一个现象 1 2 3 4 5 6 7 8 9 10 11 public class MainActivity extends Activity {       @Override     pr ...

  3. Let’s Encrypt 最近很火的免费SSL 使用教程

    2015年10月份,微博上偶然看到Let's Encrypt 推出了beta版,作为一个曾经被https虐出血的码农来说,这无疑是一个重磅消息.并且在全站Https的大趋势下,Let's Encryp ...

  4. JavaScript内存管理

    低级语言,比如C,有低级的内存管理基元,想malloc(),free().另一方面,JavaScript的内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时"自动" ...

  5. Android onMeasure 方法的测量规范MeasureSpec

    一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求.一个MeasureSpec由大小和模式组成.它有三种模式:UNSPECIFIED(未 ...

  6. Question20180106 Java环境变量的配置及为什么要配置环境变量

    Question 1  Java环境变量的配置及为什么要配置环境变量 Q1.1为什么要配置环境变量 在学习JAVA的过程中,涉及到多个环境变量(environment variable)的概念,如PA ...

  7. oracle-03 表的管理

    一.表名和列名的命名规则1).必须以字母开头2).长度不能超过30个字符3).不能使用oracle的保留字4).只能使用如下字符 a-z,a-z,0-9,$,#等 二.数据类型1).字符类char 长 ...

  8. xcode7--iOS开发---将app打包发布至app store

    时隔3个月再次接触应用打包,又是一顿折腾 说说这次的感受吧: 变得是打包时间减少到4小时(其中大部分时间还是xcode7或者是iOS9的原因),不变的是还是一如既往的坑!! 好了,废话不多说,下面讲讲 ...

  9. java实现简单计算器功能

    童鞋们,是不是有使用计算器的时候,还要进入运行,输入calc,太麻烦了,有时候甚至还忘记单词怎么拼写,呵呵程序员自己写代码实现,又简单,又方便啊 以下为代码(想要生成可执行工具可参考:http://w ...

  10. 【PTA 天梯赛训练】六度空间(广搜)

    “六度空间”理论又称作“六度分隔(Six Degrees of Separation)”理论.这个理论可以通俗地阐述为:“你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过五个人你就能够 ...