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

简单线段树的讲解(一点点的心得体会)

目录

一、初识线段树

图例:

​编辑

数组存储:

指针存储:

理由:

build函数建树

二、线段树的区间修改维护

区间修改维护:

区间修改的操作:

递归更新过程:

区间修改update:

三、线段树的区间查询

区间查询:

区间查询的操作:

递归查询过程:

区间查询query:

例题:

完整代码:

数组实现:

指针实现:

总结


前言

今天我们来学习一下线段树

模板题目:P3372 【模板】线段树 1 - 洛谷


一、初识线段树

首先我们来了解一下什么是线段树

      线段树是一种数据结构,通常用来解决区间查询的问题。它主要用于对一个包含有 n 个元素的数组进行区间操作,如查询某个区间内的最大值、最小值、区间和等。

       线段树的基本思想是将整个数组按照一定规则进行分割,每个节点代表一个区间。每个节点保存区间内的信息,如最大值、最小值、区间和等。父节点的信息可以通过子节点的信息合并得到,这样就可以快速进行区间查询。

      线段树通常是一棵完全二叉树,叶子节点对应于数组中的元素,每个非叶子节点表示了其区间的信息。对于一个包含 n 个元素的数组,线段树的节点数一般是 2n-1 或 2^k-1,其中 k 是大于等于 n 的最小整数。线段树的构建包括建树和更新两个主要操作,查询时可以通过递归的方式进行。

       线段树在解决区间查询问题时效率很高,时间复杂度一般为 O(logn),其中 n 是数组元素个数。因此,线段树被广泛应用于需要频繁进行区间查询的场景,如动态区间最值查询、区间和查询等。

我这次是联系模板题目P3372 【模板】线段树 1 - 洛谷讲解的最简单的类型

图例:

      通过这幅图我们可以看出,线段树是根据不断的从子节点拿值,来更新父节点的值,直到得到整个区间的值,和分治的思想有点像,感觉

线段树的存储方式有两种常见的实现方法:数组存储和指针存储。

  1. 数组存储:

    • 在数组存储中,线段树被表示为一个静态的完全二叉树。数组的下标从 1 开始,对于节点 i,其左子节点为 2i,右子节点为 2i+1。
    • 如果线段树的叶子节点数量为 n,那么数组的大小一般取 4n,以确保足够的空间。
    • 线段树根节点一般存储在数组下标为 1 的位置。
    • 通过按照规则在数组中存储线段树的节点,可以方便地进行查询和更新操作。
  2. 指针存储:

    • 在指针存储中,线段树被表示为一个动态的树结构,每个节点通过指针指向其左右子节点。
    • 每个节点通常由一个包含左右子节点指针的结构体或类表示。
    • 指针存储方式在构建线段树时会动态生成节点,相对于数组存储来说更加灵活,但可能会消耗更多的内存空间。

     无论是数组存储还是指针存储,线段树的基本操作都是相似的,包括建树、查询和更新。选择适合具体应用场景的存储方式可以更好地利用线段树的优势,提高算法效率。

      首先是数组存储,我们最先要知道的是数组的大小需要开多大,在线段树的数组存储中,通常会将数组的大小设置为 4n,其中 n 表示线段树的叶子节点数量。

理由:

  1. 完全二叉树性质:线段树一般是一棵完全二叉树,具有规律性的结构。在数组存储方式下,为了方便表示完全二叉树,需要保证数组的大小是某一层节点数量的上限。对于一棵深度为 k 的完全二叉树,叶子节点数量最多为 2^k,因此数组大小一般设置为 4 * 2^k,以确保足够的空间。

  2. 节点的父子关系:在数组存储方式中,节点 i 的左子节点一般存储在位置 2i,右子节点存储在位置 2i+1。设置数组大小为 4n 可以保证对于任意节点 i,其子节点在数组中的位置都是有效的,不会越界。

  3. 方便计算左右子树位置:在线段树的查询和更新操作中,经常需要根据节点的索引快速定位其左右子树节点。通过设置数组大小为 4n,可以方便地根据节点索引计算出其左右子节点的位置,简化操作。

然后开一个build函数建树,具体操作如下:

  1. 定义数组:首先,需要定义一个大小为 4n 的数组,其中 n 是线段树的叶子节点数量。这个数组将用于存储线段树的节点信息。

  2. 构建线段树:一般将线段树按照完全二叉树的形式存储在数组中。假设根节点在数组中的索引是 1,那么对于节点 i,其左子节点为 2i,右子节点为 2i + 1。

  3. 存储节点信息:每个节点需要保存代表的区间范围和相应的信息,比如区间的最大值、最小值、和等等。在数组中,可以按照某种顺序依次存储这些信息,以便后续的查询和更新操作。

  4. 建立线段树:通过递归或迭代的方式构建线段树。一般会从叶子节点开始向上构建,通过合并子节点的信息得到父节点的信息,直至构建完整的线段树。

  5. 查询和更新:通过线段树的结构和数组存储,可以实现高效的区间查询和更新操作。比如,对于查询一个区间的最大值,可以通过递归向下查询到包含目标区间的节点,并根据存储的信息计算出结果。

  6. 记得注意边界情况:在实现线段树时,需要考虑树的边界情况,比如树的根节点索引是 1,叶子节点索引从 n+1 开始等,以确保正确地访问和操作节点。

build函数建树

void build(LL l, LL r, LL fa) {if (l == r) // //如果左右区间相同,那么必然是叶子节,只有叶子节点是被真实赋值的{t[fa] = a[l];return;}LL mid = (l + r) >> 1;build( l, mid, fa << 1);build(mid + 1, r, fa << 1 | 1);
//使用二分来优化psuh_up(fa);//此处由于我们是要通过子节点来维护父节点,所以push_up的位置应当是在回溯时将子节点的值取和交给父节点
}

二、线段树的区间修改维护

      线段树是一种用于解决区间查询和修改问题的数据结构。在线段树中,区间修改维护指的是在给定一个区间,并修改该区间内所有元素的操作。

  1. 区间修改维护

    • 当需要修改线段树中某个特定区间的值时,可以通过递归的方式向下更新区间。
    • 如果要修改的区间与当前节点表示的区间没有交集,则无需修改该节点。
    • 如果要修改的区间完全包含当前节点的区间,则直接更新当前节点的信息,并将修改操作下传给子节点。
    • 如果要修改的区间与当前节点的区间部分相交,则需要先将当前节点的信息更新,然后将修改操作同时下传给左右子节点。
  2. 区间修改的操作

    • 区间修改的操作通常包括加法、减法、赋值等。
    • 当需要对区间内的每个元素进行相同的修改时,可以利用线段树的特性进行高效操作。
    • 在修改区间时,需要根据当前节点的区间范围、待修改区间和修改方式来确定如何操作当前节点和其子节点。
  3. 递归更新过程

    • 从线段树的根节点开始递归向下更新,直到找到包含待修改区间的叶子节点。
    • 在递归过程中根据节点的区间范围和待修改区间的关系,决定如何更新节点的信息并向下传递修改操作。

       此外,对于区间操作,我们考虑引入一个名叫“ lazy tag ”(懒标记)的东西——之所以称其“lazy”,是因为原本区间修改需要通过先改变叶子节点的值,然后不断地向上递归修改祖先节点直至到达根节点,时间复杂度最高可以到达 O(nlogn) 的级别。但当我们引入了懒标记之后,区间更新的期望复杂度就降到了 O(logn) 的级别且甚至会更低。

因此,我们再弄一个tag数组,大小也是4*N

区间修改update:

void psuh_up(LL fa) {t[fa] = t[fa << 1] + t[fa << 1 | 1];//向上不断维护父节点
}
void push_down(LL l,LL r,LL fa) {LL mid = (l + r) >> 1;t[fa << 1] += tag[fa] * (mid - l + 1);tag[fa << 1] += tag[fa];t[fa << 1|1] += tag[fa] * (r-mid);tag[fa << 1|1] += tag[fa];tag[fa] = 0;// //每次将懒惰标识下放到两个儿子节点,自身置为0,以此不断向下传递 
}
void update(LL ql, LL qr, LL l, LL r, LL k, LL fa) {if (ql <= l && qr >= r) //如果区间被包含,直接返回该节点的懒惰标识{t[fa] +=k * (r - l + 1);tag[fa] += k;return;}LL mid = (l + r) >> 1;push_down(l, r, fa);//下放懒惰标识if (ql <= mid)update(ql, qr, l, mid,k, fa << 1);//朝左边下放if (qr > mid)update(ql, qr, mid + 1, r,k, fa << 1 | 1);//右边psuh_up(fa);//再将修改后的值向上返回,维护父节点
}

三、线段树的区间查询

  1. 区间查询

    • 当需要查询线段树中某个特定区间的信息时,可以通过递归的方式向下查询区间。
    • 如果要查询的区间与当前节点表示的区间没有交集,则无需查询该节点,直接返回默认值(如0或无穷大)。
    • 如果要查询的区间完全包含当前节点的区间,则直接返回该节点存储的信息。
    • 如果要查询的区间与当前节点的区间部分相交,则需要同时查询左右子节点,并根据查询结果合并得到最终结果。
  2. 区间查询的操作

    • 区间查询的操作通常包括求和、求最大值、求最小值等。
    • 在查询区间时,需要根据当前节点的区间范围、待查询区间和查询方式来确定如何操作当前节点和其子节点。
  3. 递归查询过程

    • 从线段树的根节点开始递归向下查询,直到找到包含待查询区间的叶子节点。
    • 在递归过程中根据节点的区间范围和待查询区间的关系,决定如何查询节点的信息并向下传递查询操作。
    • 最终将所有查询结果合并得到最终的区间查询结果。

      通过以上方法,可以实现对线段树中特定区间的查询操作。线段树区间查询是线段树的一个重要功能,能够快速有效地获取区间内的信息,提高了区间查询的效率。

区间查询query:

LL query(LL ql, LL qr, LL l, LL r, LL fa) {LL ret = 0;if (ql <= l && qr >= r) 如果区间被包含,直接返回该节点的懒惰标识{return t[fa];}LL mid = (l + r) >> 1;push_down(l, r, fa);//没有被包含,下放任务if (ql <= mid)ret += query(ql, qr, l, mid, fa << 1);if (qr > mid)ret += query(ql, qr, mid + 1, r, fa << 1|1);//在查询范围的左区间和右区间的值相加并返回return ret;
}
例题:

模板题目:P3372 【模板】线段树 1 - 洛谷

完整代码:
数组实现:
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL n, m, t[N * 4], tag[N * 4], a[N];
void psuh_up(LL fa) {t[fa] = t[fa << 1] + t[fa << 1 | 1];//向上不断维护父节点
}
void push_down(LL l,LL r,LL fa) {LL mid = (l + r) >> 1;t[fa << 1] += tag[fa] * (mid - l + 1);tag[fa << 1] += tag[fa];t[fa << 1|1] += tag[fa] * (r-mid);tag[fa << 1|1] += tag[fa];tag[fa] = 0;// //每次将懒惰标识下放到两个儿子节点,自身置为0,以此不断向下传递 
}
LL query(LL ql, LL qr, LL l, LL r, LL fa) {LL ret = 0;if (ql <= l && qr >= r) 如果区间被包含,直接返回该节点的懒惰标识{return t[fa];}LL mid = (l + r) >> 1;push_down(l, r, fa);//没有被包含,下放任务if (ql <= mid)ret += query(ql, qr, l, mid, fa << 1);if (qr > mid)ret += query(ql, qr, mid + 1, r, fa << 1|1);//在查询范围的左区间和右区间的值相加并返回return ret;
}
void update(LL ql, LL qr, LL l, LL r, LL k, LL fa) {if (ql <= l && qr >= r) //如果区间被包含,更新懒惰标识并返回{t[fa] +=k * (r - l + 1);tag[fa] += k;return;}LL mid = (l + r) >> 1;push_down(l, r, fa);//下放懒惰标识if (ql <= mid)update(ql, qr, l, mid,k, fa << 1);//朝左边下放if (qr > mid)update(ql, qr, mid + 1, r,k, fa << 1 | 1);//右边psuh_up(fa);//再将修改后的值向上返回,维护父节点
}
void build(LL l, LL r, LL fa) {if (l == r) // //如果左右区间相同,那么必然是叶子节,只有叶子节点是被真实赋值的{t[fa] = a[l];return;}LL mid = (l + r) >> 1;build(l, mid, fa << 1);build(mid + 1, r, fa << 1 | 1);//使用二分来优化psuh_up(fa);//此处由于我们是要通过子节点来维护父节点,所以push_up的位置应当是在回溯时将子节点的值取和交给父节点
}
int main() {cin >> n >> m;for (int i = 1; i <= n; i++)cin >> a[i];build(1, n, 1);while (m--) {int op; cin >> op;if (op == 1) {LL x, y, k; cin >> x >> y >> k;update(x, y, 1, n, k, 1);}else if(op==2){LL x, y;cin >> x >> y;cout << query(x, y, 1, n, 1) << endl;}}return 0;
}
指针实现:
#include <iostream>
#include <vector>using namespace std;struct Node {int start, end;int sum;Node *left, *right;Node(int start, int end) : start(start), end(end), sum(0), left(nullptr), right(nullptr) {}
};Node* buildSegmentTree(vector<int>& nums, int start, int end) {if (start > end) {return nullptr;}Node* root = new Node(start, end);if (start == end) {root->sum = nums[start];} else {int mid = start + (end - start) / 2;root->left = buildSegmentTree(nums, start, mid);root->right = buildSegmentTree(nums, mid + 1, end);root->sum = root->left->sum + root->right->sum;}return root;
}int query(Node* root, int qs, int qe) {if (root == nullptr || qs > root->end || qe < root->start) {return 0;} else if (qs <= root->start && qe >= root->end) {return root->sum;} else {return query(root->left, qs, qe) + query(root->right, qs, qe);}
}int main() {vector<int> nums = {1, 3, 5, 7, 9, 11};Node* root = buildSegmentTree(nums, 0, nums.size() - 1);cout << "Sum of elements in range [2, 4]: " << query(root, 2, 4) << endl;return 0;
}


总结

本文关于线段树的讲解就到这里,有什么疑问或者有什么错误的地方欢迎一起交流学习

相关文章:

  • 动态规划算法:状态压缩
  • 【python编程从入门到到实践】第二章 变量和简单的数据类型
  • Nginx 文件上传大小限制及 `client_max_body_size` 最大值详解
  • Linux 系统盘制作 | 引导加载器(GRUB 为例)| mount
  • 二叉树进阶 - 二叉搜索树
  • PDF转excel+json ,vue3+SpringBoot在线演示+附带源码
  • 宇树机器狗go2—slam建图(1)点云格式
  • MLLMs for TSAD ?
  • 单例模式:懒汉式的两种优化写法
  • 编译报错 宏 _IOC_SIZEBITS,而这个宏在编译时未定义
  • Bash 中的数学运算详解
  • 【每天一个知识点】模式识别
  • 自动驾驶---决策规划之导航增强端到端
  • Jinja2模板引擎SSTI漏洞
  • 加密壳(二)将shellcode写入PE
  • STL——红黑树的封装及map/set的模拟实现
  • 数字孪生火星探测车,星际探索可视化
  • 泛目录二级目录【实用指南】,无极站群系统2025升级版
  • leetcode125.验证回文串
  • java蓝桥杯b组
  • 美国海关新规致跨境包裹延误,DHL暂停超800美元对美个人货运
  • 菲律宾群岛地区发生5.6级地震,震源深度20千米
  • 白宫慌了!将设工作组紧急处理对中国加征关税危机
  • 青创上海—2025浦东徒步行活动举行,“青年草坪创新创业湃对”正式亮相
  • 观察|美军在菲律宾部署新导弹,试图继续构建“导弹链”
  • 经济日报:从三个变化看外贸破局之道