线段树(重要!多加理解懒惰标记!)

基础概念:

线段(区间)[L,R] 所对应的线段树是由区间 [L,R] 及其子区间构成的二叉树(如下图所示)

线段树具有的特性: 

(1)线段树的叶结点为只有一个元素的区间,因此长度为 n 的区间所对应的线段树有 n 个叶结点;

(2)在构造线段树时,将每一个区间分成两个部分,且两部分的长度之差不超过1,因此线段树的叶结点出现在最后一层或倒数第二层,由此可得,线段树的高度不超过[\log_{2}n ]+1

线段树近似于满二叉树,因此可以用二叉树的数组表示法表示线段树。对于长度为n的区间,由于对应线段树的高度为[\log_{2}n ]+1,由(2)可知,在用数组表示线段树时,数组的大小的上界为

                2^{[\log_{2}n]+1} - 1 < 2^{[\log_{2}n]+1+1} = 2^{\log_{2}n}*2^{2} = 4n

即数组的大小不超过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 

猜你喜欢

转载自blog.csdn.net/qq_62687015/article/details/128738074
今日推荐