当前位置: 首页 > news >正文

<线段树>

之前线段树学的比较浅,没有深入研究,所以就又看了一下线段树的知识点。

引入

线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。

线段树可以在O(log(n))  的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

核心概念与特性

  1. 数据结构: 一种二叉树结构(通常是完全二叉树或近似完全二叉树)。

  2. 节点含义: 每个节点代表原始数组中的一个区间 [l, r]

    • 叶子节点: 代表单个元素(即区间 [i, i])。

    • 非叶子节点: 代表其左右子节点所代表区间的合并(例如,[l, mid] 和 [mid+1, r])。

  3. 节点存储内容: 节点存储的是其代表区间的某个聚合信息,这个信息需要满足可合并性

    • 常见聚合信息:区间和、区间最小值、区间最大值、区间乘积、区间GCD/LCM等。

    • 可合并性:父节点的信息必须能够由其左右子节点的信息计算得出

  4. 核心目的:

    • 区间查询: 查询任意区间 [L, R] 的聚合信息(例如,[2, 5] 的和、最小值)。

    • 区间更新: 更新原始数组中某个元素的值,或者对某个区间内的所有元素执行相同的修改操作(例如,给 [1, 4] 的所有元素加 3)。

  5. 时间复杂度: (假设数组长度为 n)

    • 构建树: O(n)

    • 单点查询/更新: O(log n)

    • 区间查询: O(log n)

    • 区间更新: O(log n) (需要延迟标记支持)

  6. 空间复杂度: O(n)。通常使用大小为 4*n 的数组来存储(保守估计,确保足够空间)。也可以动态开点优化空间。

基本操作

  1. 建树:

    • 从根节点(代表整个数组 [0, n-1])开始递归。

    • 递归地将区间一分为二(mid = (l + r) / 2),分别构建左右子树。

    • 到达叶子节点时(l == r),用原始数组元素初始化节点数据。

    • 回溯时,利用子节点的信息计算父节点的信息(pushUp 操作)。

  2. 区间查询:

    • 从根节点开始递归。

    • 查询区间 [L, R] 与当前节点区间 [l, r] 的关系:

      • 完全覆盖: [l, r] 完全包含在 [L, R] 内 -> 直接返回当前节点存储的聚合信息。

      • 部分重叠:

        • 如果 L <= mid,则递归查询左子树 [l, mid]

        • 如果 R > mid,则递归查询右子树 [mid+1, r]

      • 无重叠: 返回不影响查询结果的单位值(例如,求和时返回0,求最小值返回无穷大)。

    • 将左右子树的查询结果合并(combine)后返回。

  3. 单点更新:

    • 找到目标位置 pos 所在的叶子节点。

    • 更新该叶子节点的值。

    • 回溯更新所有受影响的父节点的聚合信息(pushUp 操作)。

  4. 区间更新: (关键!需要延迟标记/懒惰标记)

    • 问题: 直接像单点更新一样递归到叶子节点更新整个区间,时间复杂度会退化为 O(n)

    • 解决方案:延迟标记 (Lazy Propagation)

      • 核心思想: 当需要更新一个区间 [L, R] 时,如果当前节点 [l, r] 被 [L, R] 完全覆盖,则:

        • 更新当前节点存储的聚合信息(根据更新操作计算)。

        • 给当前节点打上一个延迟标记 (lazy),记录这个更新操作(例如,“这个区间里的所有元素都要加 delta”)。

        • 停止递归! 不立即更新其子节点。

      • 标记下传: 在后续的查询更新操作访问到当前节点的子节点之前,必须:

        • 根据 lazy 标记的值更新左右子节点的聚合信息

        • 将 lazy 标记的值下传给左右子节点(如果它们还不是叶子节点,也需要打上 lazy 标记)。

        • 清除当前节点的 lazy 标记。

    • 带延迟标记的区间更新步骤:

      • 查询或更新访问到某个节点时,先检查并下传该节点的延迟标记(pushDown 操作)。

      • 如果当前节点区间 [l, r] 完全被更新区间 [L, R] 覆盖:

        • 根据更新操作修改当前节点的聚合信息。

        • 给当前节点打上延迟标记(记录更新操作)。

        • 返回。

      • 否则(部分重叠):

        • 如果 L <= mid,递归更新左子树。

        • 如果 R > mid,递归更新右子树。

        • 更新当前节点的聚合信息(pushUp 操作)。

关键点与技巧

  1. 区间划分: 通常使用 mid = (l + r) / 2(整数除法),左子树管 [l, mid],右子树管 [mid+1, r]

  2. 存储结构:

    • 数组存储: 最常用。根节点下标为 1。节点 i 的左子节点下标为 2*i (i << 1),右子节点下标为 2*i + 1 (i << 1 | 1)。需要开 4 * n 大小的数组。

    • 为什么:

    • 指针/结构体存储: 更灵活,支持动态开点。

  3. 聚合信息的可合并性: 这是线段树能够工作的基础。必须定义好如何合并两个相邻区间的信息 (combine 函数)。

  4. 延迟标记的设计:

    • 标记需要记录未下传的更新操作

    • 需要定义好如何用标记更新节点数据 (apply 函数)。

    • 需要定义好如何将标记下传给子节点(通常是累加覆盖,取决于操作类型)。

    • 复杂操作(如区间加乘混合)需要更精细的标记设计(例如,维护 (add, mul) 两个标记,并定义好运算顺序)。

  5. 查询/更新时的递归终止条件: 通常是 l > R || r < L(无交集)或 L <= l && r <= R(完全覆盖)。

  6. 离散化: 当原始数组的值域很大(如坐标范围很大)但元素个数相对较少时,可以将坐标映射到较小的连续整数范围,再用线段树处理。常用于处理“区间覆盖”、“矩形面积并”等问题。

常见应用场景

  1. 区间求和、区间乘积。

  2. 区间最小值/最大值查询(Range Minimum/Maximum Query - RMQ)。

  3. 区间赋值、区间加减(带延迟标记)。

  4. 区间合并(如最长连续1、最大子段和)。

  5. 区间GCD/LCM。

  6. 扫描线算法(解决矩形面积并、矩形周长并等问题的基础)。

  7. 二维线段树(处理矩阵问题,如子矩阵查询/更新,但更复杂)。

优缺点

  • 优点:

    • 区间查询和区间更新的时间复杂度优异 (O(log n))。

    • 通用性强,能处理多种区间聚合操作和更新操作(只要满足可合并性)。

    • 结构清晰。

  • 缺点:

    • 代码实现相对复杂,尤其是带延迟标记的区间更新。

    • 空间开销较大(通常 4*n)。

    • 对于某些特定问题(如纯RMQ),有更简洁的替代方案(如稀疏表Sparse Table)。

 基本模板:

区间修改:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<iostream>
#include<bits/stdc++.h>
using namespace std;// 线段树节点结构体
struct ss {int x, y;       // 节点表示的区间范围 [x, y]int sum = 0;    // 区间和(本题中表示区间内1的数量)int p = 0;      // 懒惰标记(记录该区间需要翻转的次数,模2处理)
}a[400005];         // 线段树数组(通常开4倍空间)int n, m;  // n: 灯的数量, m: 操作次数// 初始化线段树
// l: 当前区间左边界, r: 当前区间右边界, i: 当前节点在数组中的下标
void ChuXianDuanShu(int l, int r,int i) {if (r == l) {  // 到达叶子节点(区间长度为1)a[i].x = l;  // 记录区间左端点a[i].y = l;  // 记录区间右端点return;}int mid = (l+r)>>1;  // 计算区间中点// 递归构建左子树ChuXianDuanShu(l, mid, i * 2);// 递归构建右子树ChuXianDuanShu(mid + 1, r, i * 2 + 1);// 初始化当前节点a[i].sum = 0;    // 初始和为0(全为0状态)a[i].x = l;      // 记录区间左端点a[i].y = r;      // 记录区间右端点
}// 处理懒惰标记(实际是执行翻转操作并传递标记)
// i: 当前节点下标
void QingLanDuoBiaoJi(int i) {// 执行翻转操作:区间内1的数量 = 区间长度 - 当前1的数量a[i].sum = a[i].y - a[i].x + 1 - a[i].sum;// 翻转标记取反(模2处理,翻转两次等于不翻转)a[i].p = (++a[i].p) % 2;
}// 区间修改(翻转操作)
// l,r: 要修改的区间, i: 当前节点下标, zhi: 未使用的参数(可移除)
void Xiu(int l, int r,int i,int zhi) {// 当前节点区间完全包含在修改区间内if (a[i].x >= l && a[i].y <= r) {// 直接翻转当前区间a[i].sum = a[i].y - a[i].x + 1 - a[i].sum;// 更新懒惰标记(翻转次数+1并模2)a[i].p = (++a[i].p) % 2;return;}// 若存在未下传的懒惰标记(翻转1次未处理)if (a[i].p == 1) {a[i].p = (++a[i].p) % 2;  // 清除当前节点标记(先+1再模2变为0)// 下传标记到左孩子QingLanDuoBiaoJi(i * 2);// 下传标记到右孩子QingLanDuoBiaoJi(i * 2 + 1);}int mid =((a[i].y + a[i].x) >> 1);  // 计算当前节点区间中点// 若修改区间与左子树有重叠if (l <= mid) {Xiu(l, r, i * 2, zhi);}// 若修改区间与右子树有重叠if (mid < r) {  // 注意:这里使用mid<r而不是r>mid,避免边界问题Xiu(l, r, i * 2 + 1, zhi);}// 更新当前节点的区间和(左右子树和相加)a[i].sum = a[i * 2].sum + a[i * 2 + 1].sum;
}// 区间查询(查询1的数量)
// l,r: 查询区间, i: 当前节点下标
int Cha(int l, int r, int i) {// 当前节点区间完全包含在查询区间内if (a[i].x >= l && a[i].y <= r) {return a[i].sum;  // 直接返回当前区间和}// 若存在未下传的懒惰标记if (a[i].p == 1) {a[i].p = (++a[i].p) % 2;  // 清除当前节点标记// 下传标记到左孩子QingLanDuoBiaoJi(i * 2);// 下传标记到右孩子QingLanDuoBiaoJi(i * 2 + 1);}int mid = a[i].x + ((a[i].y - a[i].x) >> 1); int q = 0; // 若查询区间与左子树有重叠if (l <= mid) {q += Cha(l, r, i * 2);}// 若查询区间与右子树有重叠if (mid < r) {  // 注意边界处理(右开区间)q += Cha(l, r, i * 2 + 1);}return q;  
}

单点修改是不需要懒惰标记的。

例:P3870 [TJOI2009] 开关

题目描述

现有 n 盏灯排成一排,从左到右依次编号为:1,2,……,n。然后依次执行 m 项操作。

操作分为两种:

  1. 指定一个区间 [a,b],然后改变编号在这个区间内的灯的状态(把开着的灯关上,关着的灯打开);
  2. 指定一个区间 [a,b],要求你输出这个区间内有多少盏灯是打开的。

灯在初始时都是关着的。

输入格式

第一行有两个整数 n 和 m,分别表示灯的数目和操作的数目。

接下来有 m 行,每行有三个整数,依次为:c、a、b。其中 c 表示操作的种类。

  • 当 c 的值为 0 时,表示是第一种操作。
  • 当 c 的值为 1 时,表示是第二种操作。

a 和 b 则分别表示了操作区间的左右边界。

输出格式

每当遇到第二种操作时,输出一行,包含一个整数,表示此时在查询的区间中打开的灯的数目。

输入输出样例

输入 #1复制

4 5
0 1 2
0 2 4
1 2 3
0 2 4
1 1 4

输出 #1复制

1
2

说明/提示

数据规模与约定

对于全部的测试点,保证 2≤n≤105,1≤m≤105,1≤a,b≤n,c∈{0,1}。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<iostream>
#include<bits/stdc++.h>
using namespace std;// 线段树节点结构体
struct ss {int x, y;       // 节点表示的区间范围 [x, y]int sum = 0;    // 区间和(本题中表示区间内1的数量)int p = 0;      // 懒惰标记(记录该区间需要翻转的次数,模2处理)
}a[400005];         // 线段树数组(通常开4倍空间)int n, m;  // n: 灯的数量, m: 操作次数// 初始化线段树
// l: 当前区间左边界, r: 当前区间右边界, i: 当前节点在数组中的下标
void ChuXianDuanShu(int l, int r,int i) {if (r == l) {  // 到达叶子节点(区间长度为1)a[i].x = l;  // 记录区间左端点a[i].y = l;  // 记录区间右端点return;}int mid = (l+r)>>1;  // 计算区间中点// 递归构建左子树ChuXianDuanShu(l, mid, i * 2);// 递归构建右子树ChuXianDuanShu(mid + 1, r, i * 2 + 1);// 初始化当前节点a[i].sum = 0;    // 初始和为0(全为0状态)a[i].x = l;      // 记录区间左端点a[i].y = r;      // 记录区间右端点
}// 处理懒惰标记(实际是执行翻转操作并传递标记)
// i: 当前节点下标
void QingLanDuoBiaoJi(int i) {// 执行翻转操作:区间内1的数量 = 区间长度 - 当前1的数量a[i].sum = a[i].y - a[i].x + 1 - a[i].sum;// 翻转标记取反(模2处理,翻转两次等于不翻转)a[i].p = (++a[i].p) % 2;
}// 区间修改(翻转操作)
// l,r: 要修改的区间, i: 当前节点下标, zhi: 未使用的参数(可移除)
void Xiu(int l, int r,int i,int zhi) {// 当前节点区间完全包含在修改区间内if (a[i].x >= l && a[i].y <= r) {// 直接翻转当前区间a[i].sum = a[i].y - a[i].x + 1 - a[i].sum;// 更新懒惰标记(翻转次数+1并模2)a[i].p = (++a[i].p) % 2;return;}// 若存在未下传的懒惰标记(翻转1次未处理)if (a[i].p == 1) {a[i].p = (++a[i].p) % 2;  // 清除当前节点标记(先+1再模2变为0)// 下传标记到左孩子QingLanDuoBiaoJi(i * 2);// 下传标记到右孩子QingLanDuoBiaoJi(i * 2 + 1);}int mid =((a[i].y + a[i].x) >> 1);  // 计算当前节点区间中点// 若修改区间与左子树有重叠if (l <= mid) {Xiu(l, r, i * 2, zhi);}// 若修改区间与右子树有重叠if (mid < r) {  // 注意:这里使用mid<r而不是r>mid,避免边界问题Xiu(l, r, i * 2 + 1, zhi);}// 更新当前节点的区间和(左右子树和相加)a[i].sum = a[i * 2].sum + a[i * 2 + 1].sum;
}// 区间查询(查询1的数量)
// l,r: 查询区间, i: 当前节点下标
int Cha(int l, int r, int i) {// 当前节点区间完全包含在查询区间内if (a[i].x >= l && a[i].y <= r) {return a[i].sum;  // 直接返回当前区间和}// 若存在未下传的懒惰标记if (a[i].p == 1) {a[i].p = (++a[i].p) % 2;  // 清除当前节点标记// 下传标记到左孩子QingLanDuoBiaoJi(i * 2);// 下传标记到右孩子QingLanDuoBiaoJi(i * 2 + 1);}int mid = a[i].x + ((a[i].y - a[i].x) >> 1); int q = 0; // 若查询区间与左子树有重叠if (l <= mid) {q += Cha(l, r, i * 2);}// 若查询区间与右子树有重叠if (mid < r) {  // 注意边界处理(右开区间)q += Cha(l, r, i * 2 + 1);}return q;  
}
int main(){ios::sync_with_stdio(false);        // 禁用同步cin.tie(nullptr);                   // 解除cin与cout绑定cin >> n >> m;ChuXianDuanShu(1, n, 1);int l, r, f;for (int i = 0; i < m; i++) {cin >> f >> l >> r;if (f == 0) {Xiu(l, r, 1, 0);}else {cout << Cha(l, r, 1) << endl;}}return 0;
}

相关文章:

  • [嵌入式实验]实验四:串口打印电压及温度
  • Java求职面试:从核心技术到AI与大数据的全面考核
  • 不起火,不爆炸,高速摄像机、数字图像相关DIC技术在动力电池新国标安全性能测试中的应用
  • 005 ElasticSearch 许可证过期问题
  • 深入了解linux系统—— 库的制作和使用
  • IBM DB2数据库管理工具IBM Data Studio
  • Unity QFramework 简介
  • Git 教程 | 如何将指定文件夹回滚到上一次或某次提交状态(命令详解)
  • 基于多尺度卷积和扩张卷积-LSTM的多变量时间序列预测
  • Orcad 修复Pin Name重复问题
  • MonoPCC:用于内窥镜图像单目深度估计的光度不变循环约束|文献速递-深度学习医疗AI最新文献
  • 5.3.1_2二叉树的层次遍历
  • Relooking:损失权重λ 、梯度权重α、学习率η
  • http传输协议的加密
  • 【C/C++】线程安全初始化:std::call_once详解
  • VoltAgent 是一个开源 TypeScript 框架,用于构建和编排 AI 代理
  • 【题解-洛谷】B4278 [蓝桥杯青少年组国赛 2023] 简单算术题
  • Java 注解与反射(超详细!!!)
  • React从基础入门到高级实战:React 生态与工具 - React 国际化(i18n)
  • Mac系统下,利用wget批量下载ICESat-2测高内陆水位高数据ALT13
  • 网站建设的基本费用/如何接广告赚钱
  • 男女做那个那个的视频网站/今天nba新闻最新消息
  • 中国建设银行总行门户网站/怎么接广告赚钱
  • web动态网站开发的书籍/怎么做平台推广
  • 什么网站做电子章做得好/seo方式包括
  • 常德网站建设设计/营销型网站策划