问题:

【模板】线段树1

已知一个数列,你需要进行下面两种操作:

    1.将某区间每一个数加上 k。

    2.求出某区间每一个数的和。

最简的方法是使用 for 循环,每一次更改和询问都去将数组更新一遍

但这种做法的时间复杂度达到了O ( n² ),如果想要AC拿满分,线段树是必不可少的 

QLineSeries 线段 线段图是什么?_线段树

 

线段树前身-分块思想:

如图,这是一个数组,我们将它3个3个分成一块,每一个块储存着其下方对应的值的和

在开始使用之前,我们需要先将每个块初始化。以这个问题为例,每一个块中保存的是对应范围的和

QLineSeries 线段 线段图是什么?_线段树_02

 

若要给蓝色区间加上某个值

QLineSeries 线段 线段图是什么?_其他_03

 我们便在上方的 满足一整块的“块” 中打上标记,整块加上那一个值,而不满整块的区间则单独加上那个值。

QLineSeries 线段 线段图是什么?_线段树_04

若现要查询粉色区间的和,只需要将C(上一次步骤已经整体加过了某个数)、D、E三段相加,便可用较小的时间复杂度解决这个问题QLineSeries 线段 线段图是什么?_初始化_05

这样过后原本需要四次操作的问题被我们简化成了两次(一个“块”+ 三个单个值)

但是分块思想的上限呢?是每 sqrt(n)分一块,最劣情况下的时间复杂度是 O(sqrt(n))(n的开平方)

那么...有没有更快的呢?

线段树-分块套分块

如图,这是一个线段树,可以看到,它是由很多的线段(其实就是“块”)组合而成的。大的线段又二分下去成为更短的线段,直到每条线段仅剩下一个值

在开始使用之前,我们需要先将线段树初始化。以这个问题为例,每一条线段需要维护的是它的区间值的和。

那么对于每一条线段,它的值就为它的 left_son 的值与它的 right_son 的值相加

当一条线段的区间只有一个值的时候,它的值是它数组对应的那个值

QLineSeries 线段 线段图是什么?_数组_06

知道线段树的基本逻辑之后,我们便可以开始操作

若我们要给粉色区间的每个数加上某个值,可以给区间最上方的整段的线段打上标记(lazy_tag),这样下次询问时,我们只需知道

QLineSeries 线段 线段图是什么?_线段树_07QLineSeries 线段 线段图是什么?_数组_08

如图,我们在更改时只需要给这些粉色的线段记上标记,再更新这些粉色线段上方的线段中的值

(因为线段树是树形结构,使用dfs实现 初始化、修改、查询...,回溯时会顺带更新到上方的值),效率提升了不少

那么查询时会不会出什么问题呢?

QLineSeries 线段 线段图是什么?_其他_09

如图,我们这时想要查询蓝色区间的值,对应到下方就是线段 A 与线段 B 的值相加

但是此时我们会发现,线段 A 由于下方的修改,更新往上传递,它的值是没问题的

但是线段 B 就不同了,它的上方线段被记上懒标记,但仅仅止步于上方,B 线段本身是没有被更新到的

这个时候我们就必须使用一些手段让 B 线段也拥有这个标记,才能保证查询准确

这就是懒标记的下放

QLineSeries 线段 线段图是什么?_数组_10

如图,我们在查询时,dfs会往下走。在往下走的同时,我们将路径上每一条线段的懒标记下放

(一条线段下放懒标记只会更新到它下方的线段,并不会因为自身失去了懒标记而改变值)

同时,在更改的时候也不要忘记pushdown!!这可以避免冲突)

下放过后,原先线段的懒标记被转移到它的 left_son 和 right_son 上,使他们更新自身值,最大程度地减少了时间复杂度,解决了此问题

最后贴上码满注释的贴心板子代码(c++)

#include <bits/stdc++.h>
#define ll long long//好的define可以大大减少代码量
#define mid ((l + r) / 2)
#define lson t * 2, l, mid//由于线段树是经典的二叉树,所以t的左儿子的编号为t*2,左儿子的区间自然是t的左区间至t的区间的中间(mid)
#define rson t * 2 + 1, mid + 1, r//右儿子同理

using namespace std;

const ll N = 1e6 + 1;

struct SegTree {
      //记得要开四倍空间,我们老师的(划掉)血的教训
	ll val[N * 4],y[N * 4];//val代表每个线段的值,y代表每个线段的 懒标记
      //顺便不要学我这个lazy的命名方法,当时抽了才用了y这个字母,记得用容易标识,同时比较简短的变量名,比如 tag
    
	void Pushup(ll t) {
		val[t] = val[t * 2] + val[t * 2 + 1];//传入t,由t的左儿子和右儿子更新t的值
	}

	void Build(ll *v, ll t, ll l, ll r) {//初始化线段树
		if (l == r) {//区间内只有一个值,那它的区间和必定是他自身
			val[t] = v[l];
			return ;//记得写完if马上敲上return,如果忘了这句return,死循环半天可能都找不出
		}
		Build(v, lson), Build(v, rson);//向下递归
		Pushup(t);//获取到左右儿子的值后更新自身
	}
	void Pushdown(ll t,ll l,ll r){//懒标记下推
		if(y[t]){
			y[t << 1] += y[t];//把t的标记转移到左儿子上
			val[t << 1] += y[t] * (mid - l + 1);//左儿子的值要加上(它区间内有多少个数 * 下放下来的标记)
			y[t << 1 | 1] += y[t];//右儿子同理
			val[t << 1 | 1] += y[t] * (r - mid);
			y[t]=0;//记得把上方的标记清0
		}
	}
	void Modify(ll x, ll z, ll k, ll t, ll l, ll r) {//将x到z的区间加上k
		if (z < l || x > r) return ;
		Pushdown(t,l,r);//不要吝啬pushdown!不知道要不要写的时候写就完了,事实上没有这句pushdown的话只能过一个点
		if(z >= r && x <= l){//查到了
			y[t] += k;//加上标记
			val[t] += k * (r - l + 1);//加上值
			return ;
		}
		Modify(x, z, k, lson), Modify(x, z, k, rson);//向下递归
		Pushup(t);//左儿子右儿子的值更新完了之后,记得还要更新自身
	}

	ll Query(ll x, ll y, ll t, ll l, ll r) {//查询x到y区间的区间和
		if (r < x || l > y) return 0; 
		Pushdown(t,l,r);//查的时候也一定要pushdown,原因在上方
		if (x <= l && r <= y) return val[t];//查到了
		return Query(x, y, lson) + Query(x, y, rson);//没查到,往下递归查 左儿子 和 右儿子 的和
        //因为是查询,没有数值变化,所以不需要再次更新自身
	}
}tree;

ll n, m;
ll v[N];//原数组

int main() {
	scanf("%lld%lld", &n, &m);
	for (ll i = 1; i <= n; ++i) scanf("%lld", &v[i]);//输入原数组
	tree.Build(v, 1, 1, n);//初始化线段树
	for (ll i = 1; i <= m; ++i) {
		ll opt;
		scanf("%lld", &opt);
		if (opt == 1) {//将x到m区间的每个值加上k
			ll x, m, k;
			scanf("%lld%lld%lld",&x ,&m ,&k);
			tree.Modify(x, m, k, 1, 1, n);
		}
		if (opt == 2) {//查询x到y的区间和
			ll x, y;
			scanf("%lld%lld", &x, &y);
			printf("%lld\n", tree.Query(x, y, 1, 1, n));
		}
	}
	return 0;
}