P3372 【模板】线段树 1 题解

CSDN同步

原题链接

简要题意:

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

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

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

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

比方说:

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

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

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

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

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

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

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

1 5 4 2 3

维护区间和来举例。

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

这时你又发现两个性质:

  1. \(i\) 维护的区间 \(l=r\),则 \(sum\)(区间和)\(= a_l = a_r\).

  2. \(i\) 维护的区间 \(l \not = r\),则 \(sum = lsum + rsum\).其中 \(lsum\)\(i\) 左儿子维护的区间和,\(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]\) 为例,那么我们只需要将这些值相加:

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

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

你觉得没区别?

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

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

因为你发现线段树的深度是 \(\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\) 个节点。

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

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

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

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

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

当然不必要!

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

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

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

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

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

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

那你还不下传标记!

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

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

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

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

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

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

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

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

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

实际得分:\(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;
}
posted @ 2020-04-03 17:17  bifanwen  阅读(212)  评论(0编辑  收藏  举报