基础概念:
线段(区间)[L,R] 所对应的线段树是由区间 [L,R] 及其子区间构成的二叉树(如下图所示)
线段树具有的特性:
(1)线段树的叶结点为只有一个元素的区间,因此长度为 n 的区间所对应的线段树有 n 个叶结点;
(2)在构造线段树时,将每一个区间分成两个部分,且两部分的长度之差不超过1,因此线段树的叶结点出现在最后一层或倒数第二层,由此可得,线段树的高度不超过;
线段树近似于满二叉树,因此可以用二叉树的数组表示法表示线段树。对于长度为n的区间,由于对应线段树的高度为,由(2)可知,在用数组表示线段树时,数组的大小的上界为
即数组的大小不超过4n。另外,线段树的每一个结点表示一个区间,线段树结点中所存储的数据值为该区间的和。
线段树的主要功能
线段树适用于与区间统计有关的问题,如果需要对区间内的数据进行动态更新,而且需要进行区间查询,那么使用线段树可以达到较快的更新和查询速度。
线段树的更新
单点更新:
改变一个元素的值即改变叶结点的值,则从叶结点到根结点的所有结点的和都要更新。
区间更新:
lazy标记就好像父亲给儿子代管零花钱。当我们做懒惰修改,当所修改区间完全覆盖子节点区间,先修改该子节点区间的sum值,再打上一个lazy标记,然后立刻返回,不用管下面孩子的更新。等到下载需要时,父节点再下传lazy标记(零花钱)。
完整代码展示:
1.数组类型定义:
#include<iostream>
#include<map>
#include<stack>
using namespace std;
//线段树近似于满二叉树,因此可以用二叉树的数组表示法表示线段树。
constexpr auto N = 1000;
//sum[i]为结点i所对应区间中元素的和,初始值为0;lazy[i]为结点i的懒惰结点(延时更新值)
//在后面线段树操作中采用延时更新时使用该数组,初始值为0
int sum[N << 2], lazy[N << 2];
//根据其类似二叉树的性质,由自底向上的方法构造线段树
//当一个结点的孩子更新完毕后,需要更新该结点到根结点上所有结点的值,即需要更新当前结点的信息,成为向上更新函数
void pushUp(int rt) {
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
//根据数组a构建表示区间和线段树,rt表示当前结点编号,l,r表示当前结点区间
//该算法类似于后根遍历
void build(int a[N], int rt, int l, int r) {
if (l == r) { //达到叶结点
sum[rt] = a[l]; //叶结点中只有一个元素,元素和即为该元素的值
return;
}
int mid = (l + r) >> 1;
build(a, rt << 1, l,mid); //构建左子树
build(a, (rt << 1) | 1, mid + 1, r); //构建右子树,这里(rt << 1) | 1相当于rt=2*rt+1;
pushUp(rt); //根据孩子结点信息更新当前结点的信息
}
/*
线段树的更新有两种类型:单点更新(改变一个元素的值)和区间更新(改变一个下标区间内所有元素的值)
单点更新:
改变一个元素的值即改变叶结点的值,则从叶结点到根结点的所有结点的和都要更新。
*/
//单点更新,将a[pos]的值增加inc (递归+值更新)
void pointUpd(int rt, int l, int r, int pos, int inc) {
if (l == r) { //达到叶结点,此时l=r=pos
sum[rt] += inc; //修改叶结点的值
return;
}
int mid = (l + r) >> 1;
if (pos <= mid)pointUpd(rt << 1, l, mid, pos, inc);//pos在左子树
else pointUpd(rt << 1 | 1, mid + 1, r, pos, inc);//pos在右子树
pushUp(rt); //更新当前结点到根节点的所有结点
}
//将结点rt的延时更新值向下传递。参数ln、rn分别为结点rt的左子树和右子树所对应区间的长度
void pushDown(int rt, int ln, int rn) {
if (lazy[rt]) {
lazy[rt << 1]+=lazy[rt]; //当前结点延迟更新值传递给左孩子
lazy[(rt << 1) | 1] += lazy[rt]; //当前结点延迟更新值传递给右孩子
//利用当前结点rt的延时更新值修改rt的孩子结点的sum
sum[rt << 1] += lazy[rt] * ln;
sum[rt << 1 | 1] += lazy[rt] + rn;
lazy[rt] = 0; //清除本节点的延时更新值
}
}
//区间更新,将a在区间[L,R]中的元素值都增加inc
//由于采用延时更新,因此在更新到达终止节点就不在向下更新
void rangeUpd(int rt, int l, int r, int L, int R, int inc) {
if (L <= l && r <= R) { //包含,结点[l,r]为中直接点
sum[rt] += inc * (r - l + 1); //更新当前区间的和,当前区间的每一个元素都增加inc
lazy[rt] += inc; //更新当前结点的延时更新值
return;
}
int mid = (r + l) >> 1;
pushDown(rt, mid - l + 1, r - mid); //更新当前结点的sum,并将延时更新值向孩子结点传递
//判断左右子树与[L,R]有无交集,若有交集则继续向下搜索
if (L <= mid) rangeUpd(rt<<1,l,mid,L,R,inc);
if (R > mid)rangeUpd((rt << 1) | 1, mid + 1, r, L, R, inc);
pushUp(rt); //更新当前结点到根结点的所有结点
}
/*
查询方法:
从根节点出发对线段树进行先根遍历,如果遇到终止节点,则可以直接返回该结点的sum,
最终结果为所有终止节点的sum的和。
注意在对一个结点进行处理前需要嗲用函数pushDown将该节点的延时值向下传递
*/
//线段树的查询操作,查询下标区间L~R中元素的和,结果作为函数的返回值
int query(int rt, int l, int r, int L, int R) {
if (L <= l && r <=R) //判断结点l~r为终止节点
return sum[rt];
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid); //更新当前结点的sum,并将延时更新值向孩子结点传递
int ret = 0;
if (L <= mid) ret += query(rt << 1, l, mid, L, R);
if (R > mid)ret += query((rt << 1) | 1, mid + 1, r, L, R);
return ret;
}
int main() {
int a[] = { 2,5,3,4,1,6,8,9,7,3 };
build(a, 1, 0, 9);
pointUpd(1, 0, 9, 7, 5);
rangeUpd(1, 0, 9, 2, 5, 5);
cout<<query(1, 0, 9, 0,9);
}
2.结构体定义
#include<iostream>
using namespace std;
const int maxn = 1e5;
typedef long long ll;
struct node {
ll left, right; //左右端点。
ll sum;//区间[left,right]的和
ll lazy;//lazy标记。
}SegTree[maxn << 2];
//建树和普通线段树是没有什么区别的
void BuildTree(int rt, int l, int r) {
SegTree[rt].left = l, SegTree[rt].right = r;
SegTree[rt].lazy = 0;
if (l == r) {
cin >> SegTree[rt].sum;//赋值。
return;
}
BuildTree(rt << 1, l, (l + r) >> 1); //递归建立左子树
BuildTree(rt << 1 | 1, ((l + r) >> 1) + 1, r);//递归建立右子树。
SegTree[rt].sum = SegTree[rt << 1].sum + SegTree[rt << 1 | 1].sum;
}
//Pushdown函数(lazy标记下移)
void PushDown(int rt) {
//rt代表要下移的父结点。
if (SegTree[rt].lazy) {
//如果是有标记的。
SegTree[rt << 1].lazy += SegTree[rt].lazy;//标记下移给左孩子。
SegTree[rt << 1 | 1].lazy += SegTree[rt].lazy;//标记下移给右孩子。
//左右孩子将欠下的给补上。
SegTree[rt << 1].sum += (SegTree[rt << 1].right - SegTree[rt << 1].left + 1) * SegTree[rt].lazy;
SegTree[rt << 1 | 1].sum += (SegTree[rt << 1 | 1].right - SegTree[rt << 1 | 1].left + 1) * SegTree[rt].lazy;
SegTree[rt].lazy = 0;//把欠的给清除。
}
}
//Update更新函数
void UpDate(int rt, int c, int l, int r) {
//对区间修改的函数也同样可以对单点进行修改。
if (SegTree[rt].left == l && SegTree[rt].right == r) {
SegTree[rt].lazy += c; //记下标记,
SegTree[rt].sum += (SegTree[rt].right - SegTree[rt].left + 1) * c;
return; //停止递归。
}
if (SegTree[rt].left == SegTree[rt].right) {
//到了叶子结点,不能往下了,也返回。
return;
}
PushDown(rt);//到了这步发现区间没有全覆盖我们自然要先标记下移,再去寻找左右孩子
int mid = (SegTree[rt].left + SegTree[rt].right) / 2;
if (r <= mid) {
//更新区间全部在左孩子。
UpDate(rt << 1, c, l, r);
}
else if (l > mid) {
//更新区间全部在右孩子。
UpDate(rt << 1 | 1, c, l, r);
}
else {
//否则左右区间都有。
UpDate(rt << 1, c, l, mid); //更新左孩子
UpDate(rt << 1 | 1, c, mid + 1, r); //更新右孩子。
}
SegTree[rt].sum = SegTree[rt << 1].sum + SegTree[rt << 1 | 1].sum;
}
//QueryTree函数(区间查询)
ll QueryTree(int rt, int l, int r) {
//区间查询,对[l,r]区间查询
if (SegTree[rt].left == l && SegTree[rt].right == r) {
return SegTree[rt].sum;
}
PushDown(rt);
int mid = (SegTree[rt].right + SegTree[rt].left) >> 1;
ll ans = 0;
if (r <= mid) {
//说明全部在左孩子
ans += QueryTree(rt << 1, l, r);
}
else if (l > mid) {
//说明全部在右孩子
ans += QueryTree(rt << 1 | 1, l, r);
}
else {
ans += QueryTree(rt << 1, l, mid);
ans += QueryTree(rt << 1 | 1, mid + 1, r);
}
return ans;
}
int main() {
int n, m;
while (cin >> n >> m) {
BuildTree(1, 1, n);
while (m--) {
string op;
int a, b, c;
cin >> op;
if (op == "Q") {
cin >> a >> b;
cout << QueryTree(1, a, b) << endl;
}
else {
cin >> a >> b >> c;
UpDate(1, c, a, b);
}
}
}
return 0;
}
拓展:
线段树 从入门到进阶(超清晰,简单易懂)_繁凡さん的博客-CSDN博客_线段树进阶https://blog.csdn.net/weixin_45697774/article/details/104274713?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167413366516800217086477%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=167413366516800217086477&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-104274713-null-null.142%5Ev71%5Econtrol_1,201%5Ev4%5Eadd_ask&utm_term=%E7%BA%BF%E6%AE%B5%E6%A0%91&spm=1018.2226.3001.4187
逻辑与(&&)、逻辑或(||)、按位与(&)、按位或(|)、按位异或(^)、按位取反(~)_Aczy156的博客-CSDN博客_按位同或https://blog.csdn.net/qq_43345204/article/details/92794251?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167418008616800192278342%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=167418008616800192278342&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-92794251-null-null.142%5Ev71%5Econtrol_1,201%5Ev4%5Eadd_ask&utm_term=%E6%8C%89%E4%BD%8D%E5%BC%82%E6%88%96&spm=1018.2226.3001.4187
线段树进阶之延迟标记 (~详细整理)区间修改_unique_pursuit的博客-CSDN博客https://pursuit.blog.csdn.net/article/details/107880933?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-2-107880933-blog-79921997.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-2-107880933-blog-79921997.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=5