超全面的线段树:从入门到入坟

超全面的线段树:从入门到入坟

\(Pre\):其实线段树已经学了很久了,突然线段树这个数据结构比较重要吧,现在想写篇全面的总结,帮助自己复习,同时造福广大\(Oier\)(虽然线段树的思维难度并不高)。本篇立志做一篇最浅显易懂,最全面的线段树讲解,采用\(lyd\)写的《算法竞赛进阶指南》上的顺序,从最基础的线段树到主席树,本篇均会涉及,并且附有一定量的习题,以后可能会持续更新,那么现在开始吧!


目录

  • 更新日志
  • 线段树想\(AC\)之基本原理(雾*1
  • 线段树想偷懒之懒标记(雾*2
  • 线段树想应用之扫描线(雾*3
  • 线段树想瘦身之开点与合并(雾*4
  • 线段树想持久之主席树(雾*5
  • 线段树想...不,你不想

更新日志

5.11 update:修改部分字词,基本原理30%完成,大纲完成。
5.4 update:基本原理20%完成。

线段树想\(AC\)之基本原理

什么是线段树啊?

首先,你得有的基本知识。

然后。

以下内容摘自百度百科

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

很懵?没关系,我们继续。

其实,线段树(\(Segment\) \(Tree\))是一种基于分治思想的二叉树结构,(Q1:为什么一定是二叉?)如果你学过树状数组,你会清楚地知道两者的差异性,并且随着学习的深入,你会发现线段树是一种更为通用的数据结构。

最基本的线段树包含以下几个概念:

  1. 线段树每个节点表示一个区间
  2. 线段树的唯一根节点表示整个区间统计范围,如[\(1,N\)]。
  3. 线段树的每个叶节点表示一个长度为\(1\)的元区间,如[\(x,x\)]。
  4. 线段树上的每个节点[\(l,r\)],它的左子节点是[\(l,mid\)],右子节点是[\(mid+1,r\)],其中\(mid=(l+r)/2\)(这是线段树的标准写法,也有其他不同的写法,但作为初学者,还是从标准入手好)。

如图,这就是一棵线段树。我们可以发现,当整个区间统计长度为\(2\)的整数次幂时,整棵线段树一定是一棵完全二叉树(Q2:为什么),那我们就可以用堆的编号方法来给线段树来编号啊(其实图中已经编好了)。

即:

  1. 根节点编号为\(1\)。
  2. 编号为\(x\)的节点,它的左儿子编号为\(x*2\),右儿子编号为\(x*2+1\)。

这样,我们就可以用一个数组来存所有节点的编号了!
至于正确性,,,既然你都学到线段树了,那就不用我说了吧。。。

诶等等,那万一整个区间长度不是\(2\)的整数次幂呢?

看这张图!

可以惊讶地发现,我们同样可以使用父子二倍标记法。正确性显然,只不过,正是因为这种情况,所以树的最后一层节点编号在数组中的位置可能不是连续的。

如果区间长度为\(N\),在最理想的状况下,即\(N\)是\(2\)的整数次幂时,\(N\)个叶节点的满二叉树有\(N+N/2+N/4+...+1=2N-1\)个节点。只要不是这种情况,那就还有一层,所以我们保存线段树节点编号的数组长度要大于等于\(4N\)。

于是线段树信息储存如下:

struct SegmentTree {
    int l, r;//每个区间左右端点
    int dat;//区间数据
    //其他一些附加信息
}sak[4*MAX];

当然,线段树的写法多种多样,这是最稳的一种,还有一种是记录左右儿子编号的,后面我们再说,\(zkw\)线段树就不介绍了吧。。。

建树

我们需要从根节点“\(1\)”出发,向下递归建树,并把每个节点所代表的区间赋给它。当到达了根节点,便传值,再向上维护信息。

以维护区间和为例,我们可以这样建树:

inline void build(int p, int l, int r) {
    sak[p].l = l, sak[p].r = r;
    if (l == r) {//叶节点赋值
        sak[p].sum = a[l];
        return;
    }
    int mid = (l + r) / 2;
    build(2*p, l, mid);//递归建左儿子树
    build(2*p + 1, mid + 1, r);//递归建右儿子树
    sak[p].sum = sak[2*p].sum + sak[2*p + 1].sum;//向上传递区间和的信息
}

单点修改

显然,每次操作,我们都需要从根节点开始遍历,递归找到需要修改的叶子节点,然后修改,然后向上传递信息。(Q3:正确性)

inline void change(int p, int x, int val) {
    if (sak[p].l == sak[p].r) { sak[p].sum = val; return; }//找到x位置
    int mid = (l + r) / 2;
    if (x <= mid) change(p*2, x, val)
    else change(p*2+1, x, val);
    sak[p].sum = sak[2*p].sum + sak[2*p + 1].sum;//向上传递区间和的信息   

因为整棵树的深度是\(logN\),所以单次修改的时间复杂度为\(O(logN)\)。

区间查询

这里直接给出算法过程,正确性显然。

  1. 若当前节点所表示的区间已经被询问区间所完全覆盖,则立即回溯,并传回该点的信息。
  2. 若当前节点的左儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的左儿子。
  3. 若当前节点的右儿子所表示的区间已经被询问区间所完全覆盖,就递归访问它的右儿子。

以返回区间和为例:

inline ll ask(int p, int l, int r) {
    if (l <= sak[p].l && r >= sak[p].r) {//对应1操作
        return sak[p].sum;
    }
    pushdown(p);
    ll val = 0;
    int mid = (sak[p].l + sak[p].r) / 2;
    if (l <= mid) val += ask(2*p, l, r);//对应2操作
    if (r > mid) val += ask(2*p + 1, l, r);//对应3操作
    return val;
} 

【例题】Can you answer on these queries 3

【习题】Interval GCD

Q&A

  • A1:你都看到这里了,就不用我说了吧。
  • A2:图+\(YY\)一下,无需多说。
  • A3:修改的节点只包含在递归时经过的区间中,所以只会对递归时经过的区间产生影响。

线段树想偷懒之懒标记

【引题】A Simple Problem with Intergers

【习题】[LG P3373] 线段树 2

【习题】[雅礼集训2017] 市场

Q&A

线段树想应用之扫描线

【引题】Atlantis

【例题】Stars in Your Window

Q&A

线段树想瘦身之开点与合并

【例题】Promotion Counting

Q&A

线段树想持久之主席树

【例题】K-th Number

Q&A

原文地址:https://www.cnblogs.com/silentEAG/p/10808978.html

时间: 2024-08-29 10:54:04

超全面的线段树:从入门到入坟的相关文章

hdu 1754:I Hate It(线段树,入门题,RMQ问题)

I Hate It Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 33726    Accepted Submission(s): 13266 Problem Description 很多学校流行一种比较的习惯.老师们很喜欢询问,从某某到某某当中,分数最高的是多少.这让很多学生很反感.不管你喜不喜欢,现在需要你做的是,就是按照老师的要求

线段树--从入门到精通

线段树,强大的数据结构,用处也是比较广的. 首先,我们要明白线段树是个啥? 线段树,线段嘛,有左右端点,那么它当然可以代表一个区间,那么区间上的好多事情都可以用它来搞,比如:区间加,区间乘,区间求和. 首先让我们先看个线段树的模型. 如图,这就是一棵线段树的模型. 圈内的点表示这是第几个点,红色表示这个点表示的区间范围. 每个点和它的左右两个儿子的编号是有一定的关系的: 点N,它的左儿子编号为N$\times$2,右儿子编号为N$\times$2+1. 线段树支持单点修改,区间修改,单点查询,区

线段树之入门篇

线段树(interval tree) 是把区间逐次二分得到的一树状结构,它反映了包括归并排序在内的很多分治算法的问题求解方式. 上图是一棵典型的线段树,它对区间[1,10]进行分割,直到单个点.这棵树的特点 是: 1. 每一层都是区间[a, b]的一个划分,记 L = b - a 2. 一共有log2L层 3. 给定一个点p,从根到叶子p上的所有区间都包含点p,且其他区间都不包含点p. 4. 给定一个区间[l; r],可以把它分解为不超过2log2 L条不相交线段的并. 其中第四点并不是很显然,

数据库从入门到入坟

目录 一.数据库的类型 关系性数据库 非关系性数据库 数据库编码 二.MySQL 1.启动 2.修改密码 3.配置文件 4.设置严格模式 三.数据库的增删改查 1.库 类似于文件夹 2.表 类似于文件 3.记录 4.查杀进程 5.修改表名.增加字段.删除字段.修改字段 1.修改表名 2.增加字段 3.移动字段次序 4.删除字段 5.修改字段 6.复制表 四.存储引擎 五.数据类型 1.常用数据类型 2.数值类型 六.日期类型 七.字符串类型 八.枚举类型与集合类型 九.约束 1.not null

线段树入门---给定多个线段求点的出现个数

线段树是一颗二叉树,他的每个节点都是一个区间,此题为线段树的入门题目,只是学习笔记.例题:给定N个线段,给定M个点,求点在多少个线段中出现过,此时如果用传统的方法来求,时间复杂度太高,但是,线段树的时间复杂度还可以接受. 步骤为: 1. 首先找一个区间,能覆盖给定的所有区间, 然后把此区间建立线段树,建立线段树的方式是二分法建立,即它的左孩子是他的左半个区间,右孩子是它的右边那个区间.一个图足以说明清楚 2. 将所有的区间映射到此树上, 从根节点开始遍历, 每遍历一个节点考虑四种情况: 1) 当

bzoj-1012 1012: [JSOI2008]最大数maxnumber(线段树)

题目链接: 1012: [JSOI2008]最大数maxnumber Time Limit: 3 Sec  Memory Limit: 162 MB Description 现在请求你维护一个数列,要求提供以下两种操作:1. 查询操作.语法:Q L 功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值.限制:L不超过当前数列的长度.2. 插入操作.语法:A n 功能:将n加上t,其中t是最近一次查询操作的答案(如果还未执行过查询操作,则t=0),并将所得结果对一个固定的常数D取模,将所得

HDU 1166 敌兵布阵 (线段树 &amp; 树状数组)

敌兵布阵 Time Limit:1000MS    Memory Limit:32768KB    64bit IO Format:%I64d & %I64u SubmitStatus 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1166 Description C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了.A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的

HDU 1754 I Hate It (线段树 &amp; 树状数组)

I Hate It Time Limit: 9000/3000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 39959 Accepted Submission(s): 15863 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1754 Problem Description 很多学校流行一种比较的习惯.老师们很喜欢询问,从某某到某某当

区间求最值 线段树

湖南师范大学 11460 区间求最值 区间求最值   Problem description   给定一个长度为N 的数组,有q个询问,每个询问是求在数组的一段区间内那个元素的因子的个数最大,比如24的因子的个数就是8.  Input   首先是一个整数t,表示有t组测试数据,每组测试数据的第一行是一个整数N(1<=N<=10^6),第二行有N个整数ai(1<=ai<=10^6,i=1,2,.....N)表示数组的元素.第三行有一个整数q(1<=q<=10^5),代表有