P3372 【模板】线段树 1 题解

博客园同步

原题链接

简要题意:

维护一个数组的区间修改,区间查询。

关于区间问题,其实有很多不错的算法。

当然 树状数组 也可以解决,不过为了给 后一道模板 做铺垫,我们将本题作为 线段树 的模板题讲解。

线段树什么?顾名思义,每个节点都维护了一个线段的信息,并且 线段树是一棵完全二叉树

比方说:

其中每个节点上写的三个数分别表示:节点编号,区间左端点,区间右端点 。有些节点只写了 节点编号 ,因为它们的 左端点 = 右端点,即元区间,写不下,见谅。

你会发现线段树有这样的性质:

  1. i i 号节点的左儿子编号为 2 × i 2 \times i (如果存在的话),右儿子编号为 2 × i + 1 2 \times i + 1 (如果存在的话)。其实这是完全二叉树的性质。

  2. 如果 i i 号节点维护的区间为 [ l , r ] [l,r] ,则左儿子(如果存在)维护的区间为 [ l , l + r 2 ] [l,\lfloor \frac{l+r}{2} \rfloor] ,右儿子(如果存在)维护的区间为 [ l + r 2 + 1 , r ] [\lfloor \frac{l+r}{2} \rfloor + 1 , r] . 这有利于我们建树。

那么你会说了,嗯,这棵树确实挺不错的,但是能不能维护一些值呢?

——可以,只要是可结合的运算就可以(即可以分裂为两个区间分别计算,再按照某种方式合并的),不一定要满足结合律(比方说区间最大子段和就不满足结合律),比方说 最小值,最大值,区间和,区间异或和,区间最大子段和,区间相邻两数乘积,区间方差等。

你可能不明白是怎么维护的,那我们就样例:

1 5 4 2 3

维护区间和来举例。

此时每个节点从左往右依次为:节点编号,左端点,右端点,区间和。 (不好意思, 9 9 号节点的左右端点应该是 [ 2 , 2 ] [2,2] ,口头更正一下)

这时你又发现两个性质:

  1. i i 维护的区间 l = r l=r ,则 s u m sum (区间和) = a l = a r = a_l = a_r .

  2. i i 维护的区间 l r l \not = r ,则 s u m = l s u m + r s u m sum = lsum + rsum .其中 l s u m lsum i i 左儿子维护的区间和, r s u m rsum 指右儿子维护的区间和。

有了这些性质,我们轻松地写出一段 建树 代码:

#define L i<<1
#define R (i<<1)+1
//t 数组保留建树后的结果(即线段树)
inline void update(int i) {
	t[i].sumi=t[L].sumi+t[R].sumi;
} //更新当前节点
inline void build_tree(int i,int l,int r) { //编号,左端点,右端点
	t[i].l=l; t[i].r=r;
	if(l==r) {
		t[i].sumi=a[l]; t[i].tag=0; // t[i].tag 的含义之后解释
		return;
	} int mid=(l+r)>>1;
	build_tree(L,l,mid);
	build_tree(R,mid+1,r);
	update(i);
}

那么,我们只要解决两个操作:

  1. 区间修改。

  2. 区间查询。

首先我们讲区间查询。

很显然,以样例第一组询问 [ 2 , 4 ] [2,4] 为例,那么我们只需要将这些值相加:

所以答案为 5 + 4 + 2 = 11 5 + 4 + 2 = 11 .

你说:这访问了 3 3 个区间,相当于查询 3 3 次,那和暴力没区别啊?

你觉得没区别?

那如果我们是询问 [ 1 , 5 ] [1,5] 呢?

那不就只要访问 1 1 次,即 1 1 号节点即可?

因为你发现线段树的深度是 log n \log n 的,而你最多查询 log n \log n 个节点的值(读者可自证),所以查询是 log n \log n 的。

如果还不明白,可以看看查询的代码。

inline ll query(int i,int l,int r) { //询问 [l,r] 区间和
	if(l<=t[i].l && t[i].r<=r) return t[i].sumi;
	int mid=(t[i].l+t[i].r)>>1; ll ans=0;
	pushdown(i); //pushdown 是什么之后解释
	if(l<=mid) ans+=query(L,l,r); //如果有部分在左边,就去左边
	if(r>mid) ans+=query(R,l,r); //同理去右边
	return ans; 
}

那么,我们要着手修改,这是个棘手的问题。

你说:行啊,那我也是只要修改 log n \log n 个节点。

——??你修改 [ 1 , 5 ] [1,5] 区间,真的只需要改 1 1 号节点的值吗?其它节点的值不会变吗?会啊。

那如果要覆盖整个区间的修改, [ 1 , 5 ] [1,5] 修改就要修改全线段树!那时间上肯定承受不了,还不如暴力。

但是,出于偷懒的本性,我问你:

如果没人询问这个节点的值,你有必要修改吗?

这就比方说老师让你选做作业,你会不会做

当然不必要!

所以,我们引进一个叫做 lazytag \texttt{lazy}\texttt{tag} 的东西,它就是用来偷懒的。

比方说修改 [ 1 , 4 ] [1,4] 区间,我就在 [ 1 , 3 ] , [ 4 , 4 ] [1,3] , [4,4] 两个区间打上一个标记,表示:我这里是要加上 x x 的,但是我现在偷懒,先不加

然后询问 [ 2 , 3 ] [2,3] 的时候,有个人来了:

他问 [ 1 , 5 ] [1,5] :你没偷懒吧? [ 1 , 5 ] : [1,5]: 没有啊。

他来到左区间 [ 1 , 3 ] [1,3] 问:你没偷懒?

[ 1 , 3 ] [1,3] 只得诚实 就像没做老师布置的作业 地告诉他:我偷懒着呢,标记没下传。

那你还不下传标记!

[ 1 , 3 ] [1,3] 本着偷懒的本性,只把标记下传了一层,更新了区间和。 哼,反正把锅推给别人多好

他使用分身术,先进入 [ 1 , 2 ] [1,2] 问:你偷懒没?

[ 1 , 2 ] [1,2] 哭着说:我 本来没有但是背锅 偷懒了,于是把标记也只下传了一层给 [ 1 , 1 ] [1,1] [ 2 , 2 ] [2,2] 刚刚背了锅,赶紧在让别人背着。然后更新了区间和。

他最后进入 [ 2 , 2 ] [2,2] [ 2 , 2 ] [2,2] 也偷懒了,但是这个锅它给不了别人,所以只能自己吃掉 它修改了区间和之后就把锅吃了。(???)

他的第二个分身进入 [ 3 , 3 ] [3,3] 发现它偷懒了,于是喝令不许偷懒!

3,3只能自己默默地把锅吃了,而不是传递给别人然后 [ 3 , 3 ] [3,3] 修改区间和之后就返回结果,最后他才得到了正确结果。

而最终,没有被询问的 [ 1 , 1 ] [1,1] 仍然在偷懒,不仅没吃锅,而且还没修改区间和 反正没人询问它就是偷懒!

你会发现,区间修改的本质就是一个 背锅与吃锅 偷懒的过程。

你会发现,这样我们也只需修改 log n \log n 个区间,并且每次下传标记的时间复杂度为 O ( 1 ) O(1) ,所以区间修改的时间还是 O ( log n ) O(\log n)

这就是为什么 建树要初始化,询问要下传标记 的原因,不下传标记就真懒死 可见线段树的妙处!

inline void pass(int i,ll x) {
	t[i].tag+=x;
	t[i].sumi+=x*(t[i].r-t[i].l+1);
} //将 i 节点的打上偷懒标记,更新区间和

inline void pushdown(int i) {
	pass(L,t[i].tag);
	pass(R,t[i].tag);
	t[i].tag=0; //记得自己甩锅
}
inline void change(int i,int l,int r,int x) { //区间修改
	if(l<=t[i].l && t[i].r<=r) { //整个包含区间
		t[i].sumi+=x*(t[i].r-t[i].l+1);
		t[i].tag+=x; return ; //偷懒,注意是 +=,因为标记可能累加(不止一口锅)
	} pushdown(i); //下传标记
	int mid=(t[i].l+t[i].r)>>1;
	if(l<=mid) change(L,l,r,x);
	if(r>mid) change(R,l,r,x);
	update(i); //这里需要更新区间和
}

时间复杂度: O ( n log n + m log n ) O(n \log n + m \log n) .

实际得分: 100 p t s 100pts .

#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
const int N=1e6+1;

#define L i<<1
#define R (i<<1)+1

inline ll read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
	ll x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}

struct tree{
	int l,r; ll tag;
	ll sumi;
};
tree t[4*N];
int n,m; ll a[N];

inline void update(int i) {
	t[i].sumi=t[L].sumi+t[R].sumi;
}

inline void pass(int i,ll x) {
	t[i].tag+=x;
	t[i].sumi+=x*(t[i].r-t[i].l+1);
}

inline void pushdown(int i) {
	pass(L,t[i].tag);
	pass(R,t[i].tag);
	t[i].tag=0;
}

inline void build_tree(int i,int l,int r) {
	t[i].l=l; t[i].r=r;
	if(l==r) {
		t[i].sumi=a[l]; t[i].tag=0;
		return;
	} int mid=(l+r)>>1;
	build_tree(L,l,mid);
	build_tree(R,mid+1,r);
	update(i);
}

inline ll query(int i,int l,int r) {
	if(l<=t[i].l && t[i].r<=r) return t[i].sumi;
	int mid=(t[i].l+t[i].r)>>1; ll ans=0;
	pushdown(i);
	if(l<=mid) ans+=query(L,l,r);
	if(r>mid) ans+=query(R,l,r);
	return ans; 
}

inline void change(int i,int l,int r,int x) {
	if(l<=t[i].l && t[i].r<=r) {
		t[i].sumi+=x*(t[i].r-t[i].l+1);
		t[i].tag+=x; return ;
	} pushdown(i);
	int mid=(t[i].l+t[i].r)>>1;
	if(l<=mid) change(L,l,r,x);
	if(r>mid) change(R,l,r,x);
	update(i);
}

inline ll ask(int i,int l) {
	if(t[i].l==l && t[i].r==l) return t[i].sumi;
	pushdown(i); int mid=(t[i].l+t[i].r)>>1;
	if(l<=mid) return ask(L,l);
	else return ask(R,l);
}

int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	build_tree(1,1,n);
	while(m--) {
		int opt=read(),l,r; ll x;
		if(opt==1) {
			l=read(),r=read(),x=read();
			change(1,l,r,x);
		} else {
			l=read(),r=read();
			printf("%lld\n",query(1,l,r));
		}
	}
	return 0;
}
发布了33 篇原创文章 · 获赞 40 · 访问量 1018

猜你喜欢

转载自blog.csdn.net/bifanwen/article/details/105295788