# 线段树专题 ## 一、线段树基础 ### 1. 线段树简介 线段树是算法竞赛中常用的用来维护区间信息的数据结构。 线段树可以在很小的时间复杂度内实现 **单点修改、区间修改、区间查询**(即区间求和,求区间 $max$ ,求区间 $min$ ,区间 $gcd$ 等)操作。 但是,线段树所维护的信息,需要满足 **区间加法**。 #### 区间加法 如果一个区间 $[l,r]$(线段树中一个点表示一个区间)满足区间加法的意思是一个区间 $[l,r]$ 的线段树维护的信息(即区间最大值,区间最小值,区间和,区间 $gcd$ 等),可以由两个区间 $[l,mid]$ 和 $[mid+1,r]$合并而来。 ### 2. 线段树的基本概念 线段树,是一种基于分治思想的二叉搜索树。它支持的所有操作都可以 $O(logn)$ 的时间复杂度完成。 线段树的 **基本特点**: ![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2022/12/7ac2125b295f96d209968be250d1dd08.png) * ① 线段树的每一个节点表示一个区间 * ② 线段树有唯一根,这个根表示的所有会被线段树统计的总区间,一般情况下,根表示的区间就是 $[1,n]$ * ③ 线段树的叶子节点表示的区间为 $[x,x]$ ,且长度为 $1$ * ④ 线段树中如果一个节点表示的区间是 $[l,r]$ ,且这个点不为叶子节点,即 $l≠r$,那么这个节点的左子树的根表示的区间就是 $[l,mid]$ 这个节点的右子树的根表示的区间就是 $[mid+1,r]$,其中 $\large mid=⌊\frac{l+r}{2}⌋$。 ### 3.线段树的存储方式 直接采用堆存储方式,即一颗线段树的根的编号是 $1$ ,设一个不为根的节点编号 $x$ ,则这个点的父节点是 $⌊\frac{x}{2}⌋$ ,他的两个子节点的编号分别是 $2x$ 和 $2x+1$ 。为了线段树的节点不超过存储范围,一般线段树都要开 $4n$ 的空间,即区间总长度的 $4$ 倍。 因为一颗线段树最多是一颗满二叉树,而满二叉树的最后一层是$n$ 个点,前面的点数是 $n−1$ ,所以一共要 $2n−1$ 的空间,但由于线段树有可能最后一层节点还有子节点,比如说 $n=10$ 的时候,如图:
这里就是一个例子,最后一层是多出来的,而最后一层节点最多 $2n$ 个节点,最坏情况下就最右边两个节点,最右下角的一个节点的编号是 $2n−1+2n=4n−1$ ,所以线段树一般开 $4n$ 。 ### 4.建立线段树 **思路** 我们递归遍历初始区间,把遍历到的所有节点表示的区间记录下来,如果这个节点不是叶子节点(即区间长度大于$1$),那么就分别遍历左子树和右子树,否则就是叶子节点,不仅要把表示的区间记录下来,还要把线段树维护的信息也记录下来,维护的信息在叶子节点上基本上就是这个数本身。 时间复杂度 $O(logn)$ **代码** ```cpp {.line-numbers} struct Node { int l,r; LL sum; //这里可以维护任何满足区间加法的信息,这里就用区间求和了 }tr[N << 2]; //要开四倍空间 void pushup (int u) { //这里只有区间和,区间和就是由一个点的左右子节点的和相加 tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum; } void build (int u,int l,int r) { //当前正在下标为u的点,这个点表示的区间是[l,r] if (l == r) { tr[u] = {l,r,a[l]};//叶子,初始值 return ; } tr[u] = {l,r}; //记得存储当前点表示的区间,否则你会调上一整天 int mid = (l + r) >> 1; build (u << 1,l,mid),build (u << 1 | 1,mid + 1,r); //u << 1就是u * 2,u << 1 | 1就是u * 2 + 1 pushup (u); //向上推送,意思是把某一个节点的信息由他的子节点算出来 } ``` ### 5.单点修改 **思路** 我们通过二分查找的形式找到要修改的点,然后把找的过程上的链都修改一下。 时间复杂度 $O(logn)$ **代码** ```cpp {.line-numbers} void change(int u,int x,int v) { //当前这个点是下标为u的点,要把第x个数修改成d if (tr[u].l == tr[u].r) { tr[u].sum = v; //叶子修改 return ; } int mid = tr[u].l + tr[u].r >> 1; if (x <= mid) change (u << 1 , x , v); //如果在左边就递归修改左半边区间 else change (u << 1 | 1 , x , v); //如果在右边就递归修改右半边区间 pushup (u) //向上推送信息 } ``` ### 6.区间修改 【线段树的精髓——$Lazy$标记】 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308290840605.png) #### (1). $Lazy$标记是啥?有啥用? > **答**: >$Lazy$标记一般用在线段树的区间修改中使用了它,能将你原本需要 $O(n)$的区间修改,变得快非常多~ 举个例子秒懂: >- 省里发通知,退休人员工资普遍上涨$200$元,比即日起生效。 >- 市里收到通知后,领导说:不行,咱们地方财政没有钱,先计着吧,不能发,记住哪些人能多发$200$元就行了。这样多快,不用花钱,不用费事~,这叫记录懒标记,懒政嘛~ >- 第二个月了,领导说:把上次那些没发$200$元的所有人,都修改为$400$。这叫懒标记叠加。 >- 第$N$个月,省里调查组到市里调研,去$XX$厂,会询问大家工资上涨的执行情况,这时候,市领导马上让执行:把$XX$厂的欠款马上发了,别让省里调查组调查出毛病来,其它厂先记着,还是不发。这叫懒标记下放。 #### (2). 如何理解$Lazy$标记?怎么写? 我们再用一次之前的图 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308211108339.png) 此时,假设初始数据为:`1 2 3 4 5 6 7 8 9 10` 我们需要将`3~7`号数据修改为`4`(当然,加上的操作也是差不多的) 正常思维我们会顺着节点编号$dfs$,找到`3~7`号数据的所有叶子节点以及路径上的父亲节点,再一一修改 慢死了对不对,最差条件下速度为 $O(n*q)$ ($q$是询问次数) ##### 现在我们使用$Lazy$标记 从($1,10$)节点出发,开始搜索;当搜索到($1,5$)节点时,发现$3$ ~ $7$这个数据范围未完全覆盖此区间(说人话就是,$1$~$5$区间里,$1,2$号数据是不需要修改的),然后接着搜($1,3$),($1,2$)....... 恭喜你搜索到了($3,3$)节点!此时你发现$3$号数据需要修改,而($3,3$)节点是一个叶节点(叶节点的$l$与$r$相等),那么,直接修改($3,3$)节点的$max$值为$4$ 接着搜,当你搜到$(4,5)$节点时,你会发现$4$号数据与$5$号数据都需要修改,如果继续搜索下去,去修改($4,4$)节点与($5,5$)节点,那就体现不出$Lazy$标记的 **懒** 之处了 我们选择不再往下搜索,而是将($4,5$)节点的$Lazy$标记修改为$4$,然后直接回溯,这样就表示($4,5$)节点覆盖的所有数据(就是$4$~$5$号数据啦)都被修改为了$4$ 同理$tr[12].lazy=4$ ( 也就是($6,7$)节点的$Lazy$标记为$4$) 来看看代码 ```cpp {.line-numbers} // 区间[l,r]统一增加v void modify(int u, int l, int r, int v) { if (l <= tr[u].l && r >= tr[u].r) { tr[u].tag += v; // 懒标记 tr[u].mx += v; // 区间最大值也需要加上v return; } // 下放懒标记 pushdown(u); // 分裂 int mid = (tr[u].l + tr[u].r) >> 1; if (l <= mid) modify(u << 1, l, r, v); // 与左区间有交集 if (r > mid) modify(u << 1 | 1, l, r, v); // 与右区间有交集 pushup(u); // 将结果的变更更新到祖先节点 } ``` 第一个$if$的判断依据:若是需要修改的区间能完全覆盖当前区间,则直接修改当前区间的$Lazy$标记并回溯 #### 为什么一定要在有新的$lazy$标记修改前进入$pushdown$呢? 若一个区间(比如说区间($1,5$))的$Lazy$标记已经被修改为$3$,现在我们需要修改区间($1,3$)为$4$,假如我们直接修改区间($1,3$)的$Lazy$标记为$4$,那么,我们在询问$3$号数据时,会先访问到($1,5$)的$Lazy$标记,而这个$Lazy$标记会挡住$(1,3)$上的$Lazy$ > **解释**:当我们搜到($1,5$)节点时,如果它的$Lazy$标记为$3$,那么我们就默认$1$~$5$号节点都被修改为了$3$,而不会继续搜索查看下一层的子节点,这也是为什么$Lazy$标记省时间的原因。 我们再看一次之前的图(可以对着比划嗷) ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308211127578.png) 那么我们怎么解决这个问题呢?其实很简单的一个下放操作就可以解决啦 下放操作 ```cpp {.line-numbers} // 父节点向子节点传递懒标记 void pushdown(int u) { auto &root = tr[u], &ls = tr[u << 1], &rs = tr[u << 1 | 1]; if (root.tag) { // 如果存在懒标记 // tag传递到子段,子段的sum和需要按 区间长度*root.tag 进行增加 ls.tag += root.tag, ls.sum += (LL)(ls.r - ls.l + 1) * root.tag; rs.tag += root.tag, rs.sum += (LL)(rs.r - rs.l + 1) * root.tag; // 清除懒标记 root.tag = 0; } } // 以u为根,在区间[l,r]之间全都增加v void modify(int u, int l, int r, int v) { if (tr[u].l >= l && tr[u].r <= r) { // 如果区间完整命中 tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * v; // 总和增加 = 区间长度*v tr[u].tag += v; // 懒标记+v return; } pushdown(u); // 如果自己身上有旧的tag数值,在递归前需要将原tag值pushdown到子孙节点去 if (tr[ls].r >= l) modify(ls, l, r, tag); if (tr[rs].l <= r) modify(rs, l, r, tag); pushup(u); } ``` 代码在递归时多了一个$pushdown$操作~ 仔细看这个$pushdown$,发现它其实就是把自己的$lazy$传递给了自己的两个子节点,然后将自己的$lazy$标记清零,预备下一次的修改。 我们在进行新一轮修改的时候,使用$modify$函数,一直搜索到最底层的全覆盖节点。 搜索下一层之前,如果此次修改的节点$lazy$标记为$0$,也就是它以前没有被修改过,那就不管它,继续搜索 如果$lazy$标记不为零,也就是它以前被打上过标记,那么为了避免之前说过的无法更新的情况,需要将$lazy$标记清零(以便于下次询问时不会停留在上层标记不为零的非最新标记处) 但是直接清零标记会导致区间内一部分的小区间标记丢失,于是我们采用下放操作,将父亲节点的$lazy$标记传递给两个子节点,防止信息丢失 在递归边界处,如果区间已经被完全覆盖,就不用管$lazy$标记是否会影响下层(本来就是表示的该区间内所有数据都是这个标记的意思嘛),直接修改$lazy$标记 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308211129069.png) 最后,多找题写写才能理解线段树(包括各种结构,算法)的灵魂呀,去洛谷搜索 线段树 $tag$ 找题目做一做吧 ### 7.区间查询 **思路** 区间查询类似区间修改,只不过变为查询了。 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308290839811.png) **代码** ```cpp {.line-numbers} int mid = (tr[u].l + tr[u].r) >> 1; int sum = 0; if (l <= mid) sum += query(ls, l, r); // 左半边有被查询到的数据,就递归左半边 if (r >= mid + 1) sum += query(rs, l, r); // 右半边有被查询到的数据,就递归右半边 return sum; ``` **或者** ```cpp {.line-numbers} if (tr[ls].r < l) return query(rs, l, r); if (tr[rs].l > r) return query(ls, l, r); return query(ls, l, r) + query(rs, l, r); ``` 时间复杂度 $O(logn)$ ### 二、题单 **[$AcWing$ $1275$. 最大数](https://www.cnblogs.com/littlehb/p/16152232.html)** **【单点修改,区间查询】** **[$AcWing$ $245$. 你能回答这些问题吗](https://www.cnblogs.com/littlehb/p/16152783.html)** **【单点修改,区间最大子段和,前缀最大值+后缀最大值+区间和+整体最大值】** **[$AcWing$ $246$. 区间最大公约数](https://www.cnblogs.com/littlehb/p/16152939.html)** **[单点修改,区间查询,差分,更相减损数,推式子,记录$sum$和,记录最大公约数,最大公约数具有传递性]** **[$GSS1$ - $Can$ $you$ $answer$ $these$ $queries$ $I$](https://www.cnblogs.com/littlehb/p/16204025.html)** **【就是上面的$AcWing$ $245$】** **[$GSS3$ - $Can$ $you$ $answer$ $these$ $queries$ $III$](https://www.cnblogs.com/littlehb/p/16204777.html)** **【区间最大子段和+单点修改,$GSS1$的小修小改版】** **[$GSS5$ - $Can$ $you$ $answer$ $these$ $queries$ $V$](https://www.cnblogs.com/littlehb/p/16206228.html)** **【有范围限制的区间最大子段和】** **[$POJ$ $3264$ $Balanced$ $Lineup$](https://www.cnblogs.com/littlehb/p/16185270.html)** **【线段树+单点修改+区间查询最大值、最小值】** **[$HDU$ $3333$ $Turing$ $Tree$](https://www.cnblogs.com/littlehb/p/16188952.html)** **【图灵树,线段树(树状数组)+单点修改+离线操作+边构建边计算+数字去重求区间和】** **[$POJ$ $2828$ $Buy$ $Tickets$](https://www.cnblogs.com/littlehb/p/16189410.html)** **【线段树+倒序枚举+单点修改+魔改版本+怪异计数区间和】** **[$AcWing$ $243$. 一个简单的整数问题2](https://www.cnblogs.com/littlehb/p/16143694.html)** **【区间修改+懒标记+区间查询】** **[$HDU$ $1698$ $Just$ $a$ $Hook$](https://www.cnblogs.com/littlehb/p/16184486.html)** **【区间修改统一值+区间查询】** **[$HDU$ $3577$ $Fast$ $Arrangement$](https://www.cnblogs.com/littlehb/p/16184914.html)** **【线段树+区间修改+维护最大值】** **[$Luogu$ $P2894$(区间连续一段空的长度)](https://www.cnblogs.com/littlehb/p/17019449.html)** **【线段树+区间修改+懒标记+维护连续空白房间数】** > ① 整体最大、左最大、右最大+组装出区间,更新父亲的统计信息 > ② 当父亲接到修改标识,准备$pushdown$时,除了下传懒标记外,也同步更新了左右儿子的统计信息,以保证未将懒标记下传到底时,直接获取的子树统计信息也是正确的 **[$P1253$ 扶苏的问题](https://www.cnblogs.com/littlehb/p/17663960.html)** **【线段树+两个懒标记(注意标记的顺序和叠加),柯朵莉树】** **[$P3740$ $[HAOI2014]$ 贴海报](https://www.cnblogs.com/littlehb/p/17656780.html)** **【区间覆盖问题,离散化, 插入$r[i]+1$,倒序枚举】** **[$P1204$ $[USACO1.2]$ 挤牛奶$Milking$ $Cows$](https://www.cnblogs.com/littlehb/p/17656983.html)** **【贪心、线段重合、求最大重叠段长度和最大间距、柯朵莉树】** **[$P4979$ 矿洞:坍塌](https://www.cnblogs.com/littlehb/p/17661421.html)** **【线段树,区间数字是否一致,用的是数字$HASH$,在数字数量少的情况下是可行的,柯朵莉树需要吸氧】** **[$P2787$ 语文$1$($chin1$)- 理理思维](https://www.cnblogs.com/littlehb/p/17662149.html)** **【$26$棵线段树,利用桶来排序,懒标记$TAG=1$表示区间整体修改为$1$,$TAG=2$表示区间整体修改为$0$】** **[$SP13015$ $Counting$ $Primes$](https://www.cnblogs.com/littlehb/p/17662387.html)** **【线段树,懒标记,模板题,欧拉筛】** **[$CF915E$ $Physical$ $Education$ $Lessons$](https://www.cnblogs.com/littlehb/p/17664882.html)** **【线段树解法,柯朵莉树解法】** **[$P4344$ [$SHOI2015$]脑洞治疗仪](https://www.cnblogs.com/littlehb/p/17671651.html)** **【线段树维护区间最大连续子序列和,柯朵莉树解法】** **[$P2253$ 好一个一中腰鼓!](https://www.cnblogs.com/littlehb/p/17679450.html)** **【线段树,区间内最长不重复区间长度】** **[$P2572$ [$SCOI2010$] 序列操作](https://www.cnblogs.com/littlehb/p/17677903.html)** **【线段树,$8$个属性,$2$个懒标记的线段树,难度立即提升!柯朵莉树只能过$3$个测试点】** **[#10115. 「一本通 4.1 例 3」校门外的树](https://www.cnblogs.com/littlehb/p/17630351.html)** **【左右括号问题,树状数组,线段树,单点修改】** **[$CF444C$ $DZY$ $Loves$ $Colors$](https://www.cnblogs.com/littlehb/p/17681314.html)** **【魔改线段树,根据$same$属性决策是否懒标记下传】** #### (3)、线段树维护区间可合并信息 [$AcWing$ $1277$. 维护序列](https://www.cnblogs.com/littlehb/p/16164815.html) [扫描线+线段树+乘法加法两个懒标记] [$POJ$ $2777$ $Count$ $Color$](https://www.cnblogs.com/littlehb/p/16199072.html) [二进制表示选择状态+线段树计算1的个数和+懒标记更新] #### (4)、线段树维护区间不可合并信息(暴力计算) [$GSS4$ - $Can$ $you$ $answer$ $these$ $queries$ $IV$](https://www.cnblogs.com/littlehb/p/16202039.html) [暴力开方] [$P4145$ 上帝造题的七分钟 $2$ / 花神游历各国](https://www.cnblogs.com/littlehb/p/16202116.html) 这个其实就是上面那道题,一模一样,输出有点差别而已。 [$CF438D$ $The$ $Child$ $and$ $Sequence$](https://www.cnblogs.com/littlehb/p/16202919.html)[暴力取模] #### (5)、线段树优化建图 [$Legacy$ - $CF786B$](https://www.cnblogs.com/littlehb/p/16195613.html) #### (6)、线段树+大小转换为$01$序列+二分 [$P2824$ HEOI2016 排序](https://www.cnblogs.com/littlehb/p/16206924.html) #### (7)、线段树维护树上信息 [POJ 3321 Apple Tree](https://www.cnblogs.com/littlehb/p/16198287.html) [$dfs$序求子树节点和] 据说线段树还可以应用$bfs$序,目前还没有找到合适的练习题,挖坑待填吧~ [dfs序基础讲解(小白版)](https://blog.nowcoder.net/n/4c6c919ac4e14fb0a61274d4e5fb5e12) #### (8)、线段树分裂与合并 [$P5494$ 【模板】线段树分裂](https://www.cnblogs.com/littlehb/p/16210449.html) [$P4556$ [$Vani$有约会]雨天的尾巴 /【模板】线段树合并](https://www.cnblogs.com/littlehb/p/16210459.html) --- ## TODO 难度太高 或者 还没有学习到 #### [$CF343D$ $Water$ $Tree$](https://www.luogu.com.cn/problem/CF343D) **【涉及到树链剖分】** #### [$P4690$ [$Ynoi2016$] 镜中的昆虫](https://www.luogu.com.cn/problem/P4690) **【NOI级别,黑题,先不做】**