目录
一、初识线段树
图例:
编辑
数组存储:
指针存储:
理由:
build函数建树
二、线段树的区间修改维护
区间修改维护:
区间修改的操作:
递归更新过程:
区间修改update:
三、线段树的区间查询
区间查询:
区间查询的操作:
递归查询过程:
区间查询query:
例题:
完整代码:
数组实现:
指针实现:
总结
前言
今天我们来学习一下线段树
模板题目:P3372 【模板】线段树 1 - 洛谷
一、初识线段树
首先我们来了解一下什么是线段树
线段树是一种数据结构,通常用来解决区间查询的问题。它主要用于对一个包含有 n 个元素的数组进行区间操作,如查询某个区间内的最大值、最小值、区间和等。
线段树的基本思想是将整个数组按照一定规则进行分割,每个节点代表一个区间。每个节点保存区间内的信息,如最大值、最小值、区间和等。父节点的信息可以通过子节点的信息合并得到,这样就可以快速进行区间查询。
线段树通常是一棵完全二叉树,叶子节点对应于数组中的元素,每个非叶子节点表示了其区间的信息。对于一个包含 n 个元素的数组,线段树的节点数一般是 2n-1 或 2^k-1,其中 k 是大于等于 n 的最小整数。线段树的构建包括建树和更新两个主要操作,查询时可以通过递归的方式进行。
线段树在解决区间查询问题时效率很高,时间复杂度一般为 O(logn),其中 n 是数组元素个数。因此,线段树被广泛应用于需要频繁进行区间查询的场景,如动态区间最值查询、区间和查询等。
我这次是联系模板题目P3372 【模板】线段树 1 - 洛谷讲解的最简单的类型
图例:
通过这幅图我们可以看出,线段树是根据不断的从子节点拿值,来更新父节点的值,直到得到整个区间的值,和分治的思想有点像,感觉
线段树的存储方式有两种常见的实现方法:数组存储和指针存储。
-
数组存储:
- 在数组存储中,线段树被表示为一个静态的完全二叉树。数组的下标从 1 开始,对于节点 i,其左子节点为 2i,右子节点为 2i+1。
- 如果线段树的叶子节点数量为 n,那么数组的大小一般取 4n,以确保足够的空间。
- 线段树根节点一般存储在数组下标为 1 的位置。
- 通过按照规则在数组中存储线段树的节点,可以方便地进行查询和更新操作。
-
指针存储:
- 在指针存储中,线段树被表示为一个动态的树结构,每个节点通过指针指向其左右子节点。
- 每个节点通常由一个包含左右子节点指针的结构体或类表示。
- 指针存储方式在构建线段树时会动态生成节点,相对于数组存储来说更加灵活,但可能会消耗更多的内存空间。
无论是数组存储还是指针存储,线段树的基本操作都是相似的,包括建树、查询和更新。选择适合具体应用场景的存储方式可以更好地利用线段树的优势,提高算法效率。
首先是数组存储,我们最先要知道的是数组的大小需要开多大,在线段树的数组存储中,通常会将数组的大小设置为 4n,其中 n 表示线段树的叶子节点数量。
理由:
-
完全二叉树性质:线段树一般是一棵完全二叉树,具有规律性的结构。在数组存储方式下,为了方便表示完全二叉树,需要保证数组的大小是某一层节点数量的上限。对于一棵深度为 k 的完全二叉树,叶子节点数量最多为 2^k,因此数组大小一般设置为 4 * 2^k,以确保足够的空间。
-
节点的父子关系:在数组存储方式中,节点 i 的左子节点一般存储在位置 2i,右子节点存储在位置 2i+1。设置数组大小为 4n 可以保证对于任意节点 i,其子节点在数组中的位置都是有效的,不会越界。
-
方便计算左右子树位置:在线段树的查询和更新操作中,经常需要根据节点的索引快速定位其左右子树节点。通过设置数组大小为 4n,可以方便地根据节点索引计算出其左右子节点的位置,简化操作。
然后开一个build函数建树,具体操作如下:
-
定义数组:首先,需要定义一个大小为 4n 的数组,其中 n 是线段树的叶子节点数量。这个数组将用于存储线段树的节点信息。
-
构建线段树:一般将线段树按照完全二叉树的形式存储在数组中。假设根节点在数组中的索引是 1,那么对于节点 i,其左子节点为 2i,右子节点为 2i + 1。
-
存储节点信息:每个节点需要保存代表的区间范围和相应的信息,比如区间的最大值、最小值、和等等。在数组中,可以按照某种顺序依次存储这些信息,以便后续的查询和更新操作。
-
建立线段树:通过递归或迭代的方式构建线段树。一般会从叶子节点开始向上构建,通过合并子节点的信息得到父节点的信息,直至构建完整的线段树。
-
查询和更新:通过线段树的结构和数组存储,可以实现高效的区间查询和更新操作。比如,对于查询一个区间的最大值,可以通过递归向下查询到包含目标区间的节点,并根据存储的信息计算出结果。
-
记得注意边界情况:在实现线段树时,需要考虑树的边界情况,比如树的根节点索引是 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的位置应当是在回溯时将子节点的值取和交给父节点
}
二、线段树的区间修改维护
线段树是一种用于解决区间查询和修改问题的数据结构。在线段树中,区间修改维护指的是在给定一个区间,并修改该区间内所有元素的操作。
-
区间修改维护:
- 当需要修改线段树中某个特定区间的值时,可以通过递归的方式向下更新区间。
- 如果要修改的区间与当前节点表示的区间没有交集,则无需修改该节点。
- 如果要修改的区间完全包含当前节点的区间,则直接更新当前节点的信息,并将修改操作下传给子节点。
- 如果要修改的区间与当前节点的区间部分相交,则需要先将当前节点的信息更新,然后将修改操作同时下传给左右子节点。
-
区间修改的操作:
- 区间修改的操作通常包括加法、减法、赋值等。
- 当需要对区间内的每个元素进行相同的修改时,可以利用线段树的特性进行高效操作。
- 在修改区间时,需要根据当前节点的区间范围、待修改区间和修改方式来确定如何操作当前节点和其子节点。
-
递归更新过程:
- 从线段树的根节点开始递归向下更新,直到找到包含待修改区间的叶子节点。
- 在递归过程中根据节点的区间范围和待修改区间的关系,决定如何更新节点的信息并向下传递修改操作。
此外,对于区间操作,我们考虑引入一个名叫“ 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);//再将修改后的值向上返回,维护父节点
}
三、线段树的区间查询
-
区间查询:
- 当需要查询线段树中某个特定区间的信息时,可以通过递归的方式向下查询区间。
- 如果要查询的区间与当前节点表示的区间没有交集,则无需查询该节点,直接返回默认值(如0或无穷大)。
- 如果要查询的区间完全包含当前节点的区间,则直接返回该节点存储的信息。
- 如果要查询的区间与当前节点的区间部分相交,则需要同时查询左右子节点,并根据查询结果合并得到最终结果。
-
区间查询的操作:
- 区间查询的操作通常包括求和、求最大值、求最小值等。
- 在查询区间时,需要根据当前节点的区间范围、待查询区间和查询方式来确定如何操作当前节点和其子节点。
-
递归查询过程:
- 从线段树的根节点开始递归向下查询,直到找到包含待查询区间的叶子节点。
- 在递归过程中根据节点的区间范围和待查询区间的关系,决定如何查询节点的信息并向下传递查询操作。
- 最终将所有查询结果合并得到最终的区间查询结果。
通过以上方法,可以实现对线段树中特定区间的查询操作。线段树区间查询是线段树的一个重要功能,能够快速有效地获取区间内的信息,提高了区间查询的效率。
区间查询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;
}
总结
本文关于线段树的讲解就到这里,有什么疑问或者有什么错误的地方欢迎一起交流学习