线段树详解

article/2025/9/30 2:04:39

文章目录

  • 1. 什么是线段树
  • 2. 线段树的用处
  • 3. 线段树的模板题
  • 4. 动态开点线段树
  • 5. 权值线段树
  • 5. 可持久化线段树

1. 什么是线段树

顾名思义 , 线段树是一棵二叉树 , 但不同的是这棵树的结点储存的值是一个数列中 [ l , r ] [l,r] [l,r] 的某个需要的值 (例如,求和,求最大值,求最小值)

这是一棵典型的线段树 ,其性质是 :

若其中一子节点编号为 a a a,则该节点左儿子编号为 2 a 2a 2a,其右儿子编号为 2 a + 1 2a+1 2a+1

且它的父结点的编号为 a 2 \mathbf{\frac{a}{2}} 2a(c++语言中除法只取整数部分)。

在这里插入图片描述

2. 线段树的用处

线段树的用处就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是 O ( l o g 2 n ) O(log_2 n) O(log2n).

3. 线段树的模板题

  • 线段树(单点修改,区间查询)

  • 线段树(区间修改 , 单点查询)

  • 线段树(区间修改,区间查询)

  • 维护序列

    对于一个长度为 N N N 的序列 a a a 支持以下操作

    1.令所有满足 l < = i < = r l<=i<=r l<=i<=r a i a_i ai 全部变为 a i ∗ c a_i*c aic

    2.令所有满足 l < = i < = r l<=i<=r l<=i<=r a i ai ai 全部变为 a i + c a_i+c ai+c

    3.求所有满足 l < = i < = r l<=i<=r l<=i<=r a i a_i ai 的和。

    显然这是对一个区间做加法和乘法的操作,可以使用线段树完成。

    因为乘的运算级别比加法高,所以在做加法是不用管乘法,在做乘法时要管加法。只要理解了这点,程序就能看懂了

    A C c o d e AC \ code AC code

注意要加快读

#include <bits/stdc++.h>
using namespace std;#define lw(o) o << 1
#define rw(o) o << 1 | 1
const int maxn = 1e5 + 5;struct kk
{long long mul, sum, add;} tr[maxn << 2];int n, M, a[maxn], op, x, y, m;inline void push_up(int t)
{tr[t].sum = (tr[lw(t)].sum + tr[rw(t)].sum) % M;}void build(int o, int l, int r)
{tr[o].mul = 1;if (l == r){tr[o].sum = a[l];return;}int mid = (l + r) >> 1;build(lw(o), l, mid);build(rw(o), mid + 1, r);push_up(o);
}void push_down(int t, int k)
{tr[t << 1].sum = (tr[t << 1].sum * tr[t].mul + tr[t].add * (k + 1 >> 1)) % M;tr[t << 1 | 1].sum = (tr[t << 1 | 1].sum * tr[t].mul + tr[t].add * (k >> 1)) % M;tr[t << 1].mul = tr[t << 1].mul * tr[t].mul % M;tr[t << 1 | 1].mul = tr[t << 1 | 1].mul * tr[t].mul % M;tr[t << 1].add = (tr[t << 1].add * tr[t].mul + tr[t].add) % M;tr[t << 1 | 1].add = (tr[t << 1 | 1].add * tr[t].mul + tr[t].add) % M;tr[t].mul = 1;tr[t].add = 0;
}void cheng(int o, int l, int r, long long val)
{if (x <= l && r <= y){tr[o].mul = tr[o].mul * val % M;tr[o].add = tr[o].add * val % M;tr[o].sum = tr[o].sum * val % M;return;}push_down(o, r - l + 1);int mid = (l + r) >> 1;if (x <= mid){cheng(lw(o), l, mid, val);}if (mid < y){cheng(rw(o), mid + 1, r, val);}push_up(o);
}void jia(int o, int l, int r, long long val)
{if (x <= l && r <= y){tr[o].add = (tr[o].add + val) % M;tr[o].sum = (tr[o].sum + (r - l + 1) * val) % M;return;}push_down(o, r - l + 1);int mid = (l + r) >> 1;if (x <= mid){jia(lw(o), l, mid, val);}if (mid < y){jia(rw(o), mid + 1, r, val);}push_up(o);
}long long query(int o, int l, int r)
{if (x <= l && r <= y){return tr[o].sum;}push_down(o, r - l + 1);int mid = (l + r) >> 1;long long ans = 0;if (x <= mid){ans += query(lw(o), l, mid);}if (mid < y){ans += query(rw(o), mid + 1, r);}if (ans >= M){ans -= M;}push_up(o);return ans;
}signed main()
{cin >> n >> M;for (int i = 1; i <= n; i++){cin >> a[i];}build(1, 1, n);cin >> m;while (m--){int a, b;cin >> op >> x >> y;if (op == 1){cin >> a;cheng(1, 1, n, a);}if (op == 2){cin >> b;jia(1, 1, n, b);}if (op == 3){cout << query(1, 1, n) << endl;}}return 0;
}

4. 动态开点线段树

通常来说,线段树占用空间是总区间长 n n n 的常数倍,空间复杂度是 O ( 4 n ) O(4n) O(4n) 。然而,有时候 n n n 很巨大,而我们又不需要使用所有的节点,这时便可以动态开点——不再一次性建好树,而是一边修改、查询一边建立。我们不再用 p ∗ 2 p*2 p2 p ∗ 2 + 1 p*2+1 p2+1 代表左右儿子,而是用两个数组 L [ ] L[] L[] R [ ] R[] R[] 记录左右儿子的编号。设总查询次数为 m m m ,则这样的总空间复杂度为 O ( m l o g n ) O(m \ log \ n) O(m log n)

比起普通线段树,动态开点线段树有一个优势:它能够处理零或负数位置。此时,求 m i d mid mid 时不能用 ( c l + c r ) / 2 (cl+cr)/2 (cl+cr)/2 ,而要用 ( c l + c r − 1 ) / 2 (cl+cr-1)/2 (cl+cr1)/2 (因为 c l cl cl 等于 c r cr cr 时会退出递归)

因为缓存命中等原因,动态开点线段树写成结构体形式速度往往更快一些。不过 t r e e [ t r e e [ p ] . l s ] . v a l tree[tree[p].ls].val tree[tree[p].ls].val 之类的写法怎么看都很反人类 繁琐,所以我一般会用宏简化一下。

// MAXV一般能开多大开多大,例如内存限制128M时可以开到八百万左右
namespace SegTree
{
#define ls(x) tree[x].ls
#define rs(x) tree[x].rs
#define val(x) tree[x].val
#define mark(x) tree[x].mark
using T = int;
const int MAXV = 1.6e7, L = 1, R = 1e9;
const T NA = -2e9;
int cnt = 1;
struct node
{T val, mark = NA;int ls, rs;
} tree[MAXV];
T op(T a, T b)
{return a + b;
}
void upd(int p, T d, int len)
{val(p) += d * len;mark(p) += d;
}
void push_down(int p, int len)
{if (!ls(p)) ls(p) = ++cnt; // 左儿子不存在,创建新节点if (!rs(p)) rs(p) = ++cnt; // 右儿子不存在,创建新节点if (mark(p) != NA){upd(ls(p), mark(p), len / 2);upd(rs(p), mark(p), len - len / 2);mark(p) = NA;}
}
void update(int l, int r, T d, int p = 1, int cl = L, int cr = R)
{if (cl >= l && cr <= r)return upd(p, d, cr - cl + 1);push_down(p, cr - cl + 1);int mid = (cl + cr - 1) / 2;if (mid >= l)update(l, r, d, ls(p), cl, mid);if (mid < r)update(l, r, d, rs(p), mid + 1, cr);val(p) = op(val(ls(p)), val(rs(p)));
}
T query(int l, int r, int p = 1, int cl = L, int cr = R)
{if (cl >= l && cr <= r)return val(p);push_down(p, cr - cl + 1);int mid = (cl + cr - 1) / 2;if (mid >= r)return query(l, r, ls(p), cl, mid);else if (mid < l)return query(l, r, rs(p), mid + 1, cr);elsereturn op(query(l, r, ls(p), cl, mid), query(l, r, rs(p), mid + 1, cr));
}
#undef ls
#undef rs
#undef val
#undef mark
}; // namespace SegTree

可以看到,除了在 p u s h d o w n push_down pushdown 中进行了新节点的创建,其他基本和普通线段树一致。动态开点线段树不需要 b u i l d build build ,通常用在没有提供初始数据的场合(例如初始全 0 0 0 ),这时更能显示出优势,例如 C F 915 E CF915E CF915E(这道题因为是赋值,也可以使用珂朵莉树)。

当然,除了动态开点,其实先离散化再建树也常常能达到效果。但动态开点写起来更简单直观,而且在强制在线时只能这样做。

5. 权值线段树

桶我们经常使用,例如计数排序时用 c n t cnt cnt 数组记录每个数出现的次数。权值线段树就是用线段树维护一个桶,它可以 O ( l o g v ) O(log \ v) O(log v) v v v 为值域)地查询某个范围内的数出现的总次数。不仅如此,它还可以 O ( l o g v ) O(log \ v) O(log v) 地求得第 k k k 大的数。事实上,它常常可以代替平衡树使用。

由于权值线段树需要按值域开空间,所以常常动态开点。

// MAXV一般能开多大开多大,例如内存限制128M时可以开到八百万左右
namespace SegTree
{
#define ls(x) tree[x].ls
#define rs(x) tree[x].rs
#define val(x) tree[x].val
using T = int;
const int MAXV = 8e6, L = -1e7, R = 1e7; // 值域为[L, R]
const T NA = -2e9;
int cnt = 1;
struct node
{T val;int ls, rs;
} tree[MAXV];
T op(T a, T b)
{return a + b;
}
void upd(int p, T d, int len)
{val(p) += d * len;
}
void push_down(int p)
{if (!ls(p)) ls(p) = ++cnt;if (!rs(p)) rs(p) = ++cnt;
}
void update(int x, T d, int p = 1, int cl = L, int cr = R) // 单点修改
{if (cl == cr)return upd(p, d, 1);push_down(p);int mid = (cl + cr - 1) / 2;if (x <= mid)update(x, d, ls(p), cl, mid);elseupdate(x, d, rs(p), mid + 1, cr);val(p) = op(val(ls(p)), val(rs(p)));
}
T query(int l, int r, int p = 1, int cl = L, int cr = R)
{if (cl >= l && cr <= r)return val(p);push_down(p);int mid = (cl + cr - 1) / 2;if (mid >= r)return query(l, r, ls(p), cl, mid);else if (mid < l)return query(l, r, rs(p), mid + 1, cr);elsereturn op(query(l, r, ls(p), cl, mid), query(l, r, rs(p), mid + 1, cr));
}
void insert(int v) // 插入
{update(v, 1);
}
void erase(int v) // 删除
{update(v, -1);
}
int countl(int v)
{return query(L, v - 1);
}
int countg(int v)
{return query(v + 1, R);
}
int rank(int v) // 求排名
{return countl(v) + 1;
}
int kth(int k, int p = 1, int cl = L, int cr = R) // 求指定排名的数
{if (cl == cr)return cl;int mid = (cl + cr - 1) / 2;if (val(ls(p)) >= k)return kth(k, ls(p), cl, mid); // 往左搜elsereturn kth(k - val(ls(p)), rs(p), mid + 1, cr); // 往右搜
}
int pre(int v) // 求前驱
{int r = countl(v);return kth(r);
}
int suc(int v) // 求后继
{int r = val(1) - countg(v) + 1;return kth(r);
}
#undef ls
#undef rs
#undef val
#undef mark
}; // namespace SegTree

代替平衡树使用时,权值线段树代码比较短,但是当值域较大、询问较多时空间占用会比较大。

5. 可持久化线段树

可持久化线段树,也叫主席树。

可持久化数据结构思想,就是保留整个操作的历史,即,对一个线段树进行操作之后,保留访问操作前的线段树的能力。

最简单的方法,每操作一次,建立一颗新树。这样对空间的需求会很大。

而注意到,对于点修改,每次操作最多影响 ⌊ log ⁡ 2 ( n − 1 ) ⌋ + 2 \left\lfloor\log_2(n-1)\right\rfloor+2 log2(n1)+2 个节点,于是,其实操作前后的两个线段树,结构一样,

而且只有 ⌊ log ⁡ 2 ( n − 1 ) ⌋ + 2 \left\lfloor\log_2(n-1)\right\rfloor+2 log2(n1)+2 个节点不同,其余的节点都一样,于是可以重复利用其余的点。

这样,每次操作,会增加 ⌊ log ⁡ 2 ( n − 1 ) ⌋ + 2 \left\lfloor\log_2(n-1)\right\rfloor+2 log2(n1)+2 个节点。

于是,这样的线段树,每次操作需要 O ( log ⁡ 2 ( n ) ) O(\log_2(n)) O(log2(n)) 的空间。

对于每一个版本的线段树,用 r t rt rt 数组记录它的根节点就行了。

例题

对于本题,必须先将数据进行离散化。

然后我们逐一插入数字。

将每个数字的大小(即离散化后的编号)插入到它的位子上,然后并把所有包括它的区间的 s u m sum sum 都加 1 1 1

若有 n n n 个数,则有 n n n 个版本的主席树。

假设读入 1 、 5 、 2 、 4 1、5、2、4 1524

现在要查询 [ 2 , 5 ] [2, 5] [2,5] 中第 3 3 3 大的数,我们首先把第 1 1 1 棵线段树和第 5 5 5 棵拿出来。

然后我们发现,将对应节点的数相减,刚刚好就是 [ 2 , 5 ] [2,5] [2,5] 内某个范围内的数的个数。比如 [ 1 , 4 ] [1, 4] [1,4] 这个节点相减是 2 2 2,就说明 [ 2 , 5 ] [2,5] [2,5] 内有 2 2 2 个数是在 1 4 1~4 1 4 范围内(就是 2 , 3 2,3 2,3)。

所以对于一个区间 [ l , r ] [l, r] [l,r],我们可以每次算出在 [ l , m i d ] [l, mid] [l,mid] 范围内的数,如果数量 > = k >=k >=k k k k 就是第 k k k 大),就往左子树走,否则就往右子树走。

AC CODE

#include <bits/stdc++.h>
#define _ 200010
using namespace std;int l, r, k, q, ans;
int cnt_node, n, m;
int sum[_ << 5], rt[_], lc[_ << 5], rc[_ << 5];
int a[_], b[_];
int p;void build(int &t, int l, int r)
{t = ++cnt_node;if (l == r)return;int mid = (l + r) >> 1;build(lc[t], l, mid);build(rc[t], mid + 1, r);
}int modify(int o, int l, int r)
{int oo = ++cnt_node;lc[oo] = lc[o];rc[oo] = rc[o];sum[oo] = sum[o] + 1;if (l == r)return oo;int mid = (l + r) >> 1;if (p <= mid)lc[oo] = modify(lc[oo], l, mid);elserc[oo] = modify(rc[oo], mid + 1, r);return oo;
}int query(int u, int v, int l, int r, int k)
{int ans, mid = ((l + r) >> 1), x = sum[lc[v]] - sum[lc[u]];if (l == r)return l;if (x >= k)ans = query(lc[u], lc[v], l, mid, k);elseans = query(rc[u], rc[v], mid + 1, r, k - x);return ans;
}signed main()
{scanf("%d%d", &n, &m);for (register int i = 1; i <= n; i += 1)scanf("%d", &a[i]), b[i] = a[i];sort(b + 1, b + n + 1);q = unique(b + 1, b + n + 1) - b - 1;build(rt[0], 1, q);for (register int i = 1; i <= n; i += 1){p = lower_bound(b + 1, b + q + 1, a[i]) - b;rt[i] = modify(rt[i - 1], 1, q);}while (m--){scanf("%d%d%d", &l, &r, &k);ans = query(rt[l - 1], rt[r], 1, q, k);printf("%d\n", b[ans]);}return 0;
}

http://chatgpt.dhexx.cn/article/t179ESC7.shtml

相关文章

超详解线段树(浅显易懂,几乎涵盖所有线段树类型讲解,匠心之作,图文并茂)

一&#xff0c;什么是线段树&#xff1f; 线段树是怎样的树形结构? 线段树是一种二叉搜索树&#xff0c;而二叉搜索树&#xff0c;首先满足二叉树&#xff0c;即每个结点最多有两颗子树&#xff0c;并且是一颗搜索树&#xff0c;我们要知道&#xff0c;线段树的每个结点都存储…

线段树 从入门到进阶(超清晰,简单易懂)

目录 第一部 概念引入第二部 简单&#xff08;无pushdown&#xff09;的线段树1、单点修改&#xff0c;区间查询2、区间修改&#xff0c;单点查询 第三部 进阶线段树第四部 乘法&#xff08;根号&#xff09;线段树1、乘法线段树2、根号线段树模板题与代码&#xff1a;单点修改…

Navicat Premium 数据库开发工具

官网地址&#xff1a;https://www.navicat.com.cn/ Navicat Premium 是一套数据库开发工具&#xff0c;让你从单一应用程序中同时连接 MySQL、MariaDB、MongoDB、SQL Server、Oracle、PostgreSQL 和 SQLite 数据库。它与 Amazon RDS、Amazon Aurora、Amazon Redshift、Microso…

Navicat 一款数据库开发软件

Navicat premium是一款数据库管理工具&#xff0c;非常好用&#xff0c;能复制表结构等&#xff0c;大大简化了后端开发的工程量&#xff0c;其软件示意图如下 Navicat Premium 15 已经发布了&#xff0c;但是价格昂贵&#xff0c;所以将 no-money version 的双手奉上。 使用…

Java学习中的数据库和数据库开发工具

一、数据库 1、数据库&#xff0c;通常是一个戒一组文件&#xff0c;保存了一些符合特定规格的数据&#xff0c;数据库对应的英询单词是DataBase&#xff0c;简称DB&#xff1b;数据库软件称为数据库管理系统&#xff0c;英文简称DBMS&#xff0c;全称为DataBase Management S…

五大主流数据库深度对比!数据库开发、管理看这篇就够了

开发数据库应用&#xff0c;选择一个好的数据库是非常重要的。目前&#xff0c;商品化的数据库管理系统以关系型数据库为主导产品&#xff0c; 技术比较成熟。面向对象的数据库管理系统技术先进&#xff0c;数据库易于开发、维护。无论是关系型数据库还是非关系型数据库&#x…

mysql数据库开发环境_MySQL数据库教程-环境与集成开发工具

MySQL数据库基础教程 数据库是存储数据与管理数据的基础,也是目前绝大多数动态网站开发所必备的基础知识。MySQL数据库教程系列文章主要以MySQL数据库管理系统为例对关系型数据库相关定义与操作等进行讲解。并结合php与PDO等讲解,为初学者提供快速学习PHP动态网站开发所需的知…

C/C++ MySQL数据库开发

C/C++ MySQL数据库开发笔记 MySQL启动方式手动启动MySQL数据库:命令行方式启动MySQL数据库:配置环境变量MySQL在Linux下安装MySQL的配置文件重要参数设置C/C++ 访问MySQL数据库(MySQL开发环境的配置(MySQL开发头文件和库文件)MySQL 数据库的连接MySQL 数据类型以及对应的 …

1、 数据库开发规范

第1章 数据库开发规范的制定 数据库设计步骤&#xff1a; 1、数据结构设计&#xff1a;逻辑设计-》物理设计 2、实际工作中&#xff1a;逻辑设计物理设计 3、物理设计&#xff1a;表名字段名字段类型 数据库设计几个规范&#xff1a; 数据库命名规范、数据库基本设计规范…

数据库开发-2-开发数据库的要点

Lec2-开发数据库的要点 1. 开发成功数据库应用的特点 需要理解数据库体系结构需要理解锁和并发控制特性&#xff1a;每个数据库都以不同的方式实现最重要的是不要把数据库当"黑盒" 要了解每一个数据库的体系结构和特征最常见的问题就是因为我们对数据库本身了解不足…

数据库开发(Sqlite)

1、数据库开发 1.1 数据与数据管理 什么是信息&#xff1f;   信息是指对现实世界存在方式或运动状态的反应。 什么是数据&#xff1f;   数据是指存储在某一媒体上&#xff0c;能够被识别的物理符号&#xff1b;   数据的概念在数据处理领域已经被大为拓宽&#xff0c…

《软件测试的艺术》第4章:测试用例的设计-白盒测试

写在前面&#xff1a;原书中包含白盒测试、黑盒测试、错误猜测、测试策略四个小节&#xff0c;涵盖内容较多&#xff0c;因此按章节拆分叙述。 《软件测试的艺术》&#xff1a; 白盒测试--语句覆盖 语句覆盖的用例设计原则&#xff1a;将程序中的每条语句至少执行一次。 白盒…

《软件测试的艺术》读后感 Or 读书笔记

《软件测试的艺术》读后感 Or 读书笔记 第一章 一次自评价测试第二章 软件测试的心理学和经济学第三章 代码检查、走查与评审第四章 测试用例的设计第五章 模块&#xff08;单元&#xff09;测试第六章 更高级别的测试第七章 可用性&#xff08;或用户体验&#xff09;测试第八…

软件测试的艺术 学习笔记

文章目录 4.2黑盒测试4.2.1 等价划分4.2.2 边界值分析4.2.3 因果图 4.3 错误猜测4.4 测试策略 5. 模块(单元&#xff09;测试5.1 测试用例设计5.2 增量测试5.3 自顶向下测试与自底向上测试5.3.1 自顶向下的测试5.3.2 自底向上的测试5.3.3 比较 5.4 执行测试 6 更高级别的测试6.…

《软件测试的艺术》第六章 更高级别的测试

《软件测试的艺术》第六章 更高级别的测试 6.0 前言软件开发过程模型 6.1 功能测试6.2 系统测试6.2.1 能力测试6.2.2 容量测试6.2.3 强度测试6.2.4 可用性测试6.2.5 安全性测试6.2.6 性能测试6.2.7 存储测试6.2.8 配置设置6.2.9 兼容性/转换测试6.2.10 安装测试6.2.11 可靠性测…

模块测试(单元测试)——软件测试的艺术

是大型程序测试的第一个步骤【大型程序即超过500条语句的程序】 了解 模块测试是对程序中的单个程序、子程序/过程进行测试的过程【并非对整个程序】&#xff1a; 关注点在较小单元&#xff0c;是一种管理组合的测试元素的手段减轻调试的难度&#xff0c;把错误定位到一个小…

《软件测试的艺术》第1章:一次自评价测试

写在前面&#xff1a; 相比于芯片验证&#xff0c;软件测试有着悠久的历史沉淀和更为完整的生态&#xff0c;和芯片验证在某些方面上几乎有着相同的思路和方法。因此从软件测试的视角出发&#xff0c;重新思考芯片验证的方方面面。第一个系列为《软件测试的艺术》学习。 第一…

9年测试老鸟:Glenford J编写《软件测试的艺术》PDF,高清中文版

内容简介 本书以一次自评价测试开篇&#xff0c;从软件测试的心理学和经济学人手&#xff0c;探讨了代码检查、走查与评审、测试用例的设计、模块(单元)测试、系统测试、调试等主题&#xff0c;以及极限测试、因特网应用系统测试等高级主题&#xff0c;全面展现了作者的软件测…

系统测试——软件测试的艺术

系统测试有着特定的目的&#xff1a;将系统或程序与其初始目标进行比较&#xff0c;给定目标后有两含义&#xff1a; 系统测试不局限于系统&#xff0c;若产品是一个程序&#xff1a;系统测试就是试图说明程序作为一个整体是如何不满足其目标的过程根据定义&#xff0c;若产品…

《软件测试的艺术》重点记录

----定义---- 测试是为发现错误而执行程序的过程。 测试提高了程序的可靠性或质量。 ----测试方法---- 黑盒测试&#xff1a;又称之为数据驱动的测试或输入/输出驱动的测试。 白盒测试&#xff1a;对程序的逻辑结构进行检查&#xff0c;从中获取测试数据。 ----测试的原则…