线段树学习笔记

文章详细介绍了线段树这种数据结构的基础知识,包括其用于区间信息统计的功能,以及如何进行区间查询和修改。讨论了通用结构、基本操作、注意事项,并通过实例展示了线段树在处理区间最大值、最大连续子段和、最大公约数等问题的应用。此外,还介绍了延迟标记和扫描线等优化技术,以提高处理区间修改指令的效率。
摘要由CSDN通过智能技术生成

基础知识:

线段树是基于分之思想的二叉树结构,用于在区间上进行信息统计,其 基本用途为对序列进行维护,支持查询与修改指令。  

通用结构:
  1. 线段树的每个节点都代表一个区间。
  2. 线段树具有唯一的根节点,代表的区间是整个统计范围,如 $[1,N]$
  3. 线段树的每个叶节点都代表一个长度为 1 的元区间 $[x,x]$
  4. 对于每个内部节点 $[l,r]$,它的左子节点是 $[l,mid]$,右子节点是 $[mid+1,r]$,其中 $mid=(l+r)/2$(向下整除)
  5. 对于需要计算线段长度的问题,此时线段树每个叶节点代表一个长度为 x_{i+1}-x_{i} 的元区间 [i,i],其在图中则表示 [x_{i}\sim x_{i+1}] 这一段线段,因此建树时应该少建一个点 build(1,0,n-2),其中有n条线段。(例如扫描线会用到)
基本操作:
  1. pushup 
  2. pushdown
  3. build 
  4. modify
  5. query

注意事项:

  1. 我们保存线段树的数组长度要不小于 $4N$ 才能保证不会越界
  2. build 操作中一定要记得给每个节点都要赋初始值 
  3. pushdown 操作要在用到 子节点 的信息之前使用,放在前面
  4. pushup 操作在子节点都更新过后使用,目的是将父节点更新,放在后面

基本操作(以找区间最大值为例):

1. 向上传递值 

void pushup(int u){
	tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}
2. 建树
struct node{
	int l,r,v;
}tr[N*4];
void build(int u,int l,int r){
	tr[u].l=l; tr[u].r=r;
	if(l==r) return;
	int mid=l+r>>1;
	build(u<<1,l,mid); build(u<<1|1,mid+1,r);
	pushup(u);
}
3. 单点修改
void modify(int u,int x,int v){
	if(tr[u].l==x&&tr[u].r==x) tr[u].v=v;
	else{
		int mid=tr[u].l+tr[u].r>>1;
		if(x<=mid) modify(u<<1,x,v);
		else modify(u<<1|1,x,v);
		pushup(u);
	}
}
4. 区间查询
int query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
	int mid=tr[u].l+tr[u].r>>1;
	int v=0;
	if(l<=mid) v=max(v,query(u<<1,l,r));
	if(r>mid) v=max(v,query(u<<1|1,l,r));
	return v; 
}

例题:

1. AcWing 1275. 最大数

最基础的线段树模板

“单点修改”+“查询区间最大值”

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
#define ll long long
struct node{
	int l,r,v;
}tr[N*4];
int n,m,last,p;
void pushup(int u){
	tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}
void build(int u,int l,int r){
	tr[u].l=l; tr[u].r=r;
	if(l==r) return;
	int mid=l+r>>1;
	build(u<<1,l,mid); build(u<<1|1,mid+1,r);
	pushup(u);
}
int query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
	int mid=tr[u].l+tr[u].r>>1;
	int v=0;
	if(l<=mid) v=max(v,query(u<<1,l,r));
	if(r>mid) v=max(v,query(u<<1|1,l,r));
	return v; 
}
void modify(int u,int x,int v){
	if(tr[u].l==x&&tr[u].r==x) tr[u].v=v;
	else{
		int mid=tr[u].l+tr[u].r>>1;
		if(x<=mid) modify(u<<1,x,v);
		else modify(u<<1|1,x,v);
		pushup(u);
	}
}

int main(){
	n=0; last=0;
	scanf("%d%d",&m,&p);
	build(1,1,m);
	char op[2]; int x;
	while(m--){
		scanf("%s%d",op,&x);
		if(op[0]=='Q'){
			last=query(1,n-x+1,n);
			printf("%d\n",last);
		}
		else{
			modify(1,n+1,((ll)x+last)%p);
			n++;
		}
	}
	return 0;
}
2. AcWing 245. 你能回答这些问题吗

“单点修改”+“查询区间的最大连续子段和”  
解法:我们再多维护 4 个信息:区间和 $sum$,区间最大连续子段和 tamx紧靠左端的最大连续子段和 $lmax$紧靠右端的最大连续子段和 $rmax$。  
我们只需完善 $change$ 函数从下到上传递的信息:

t[p].sum=t[p* 2].sum+t[p* 2+1].sum;\\ t[p].lmax=max(t[p* 2].lmax,t[p* 2].sum+t[p* 2+1].lmax);\\ t[p].rmax=max(t[p* 2+1].rmax,t[p* 2+1].sum+t[p* 2].rmax);\\ t[p].dat=max(t[p* 2].dat,max(t[p* 2+1].dat,t[p* 2].rmax+t[p* 2+1].lmax));


然后 $query$ 函数返回值为 $node$ 结构体,用于得到答案后从下到上得到最优解。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct node{
	int l,r;
	int sum,tmax,lmax,rmax;
}tr[N*4];
int n,m,w[N];
void change(node &u,node &l,node &r){
	u.sum=l.sum+r.sum;
	u.lmax=max(l.lmax,l.sum+r.lmax);
	u.rmax=max(r.rmax,r.sum+l.rmax);
	u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u){
	change(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
	if(l==r) tr[u]={l,r,w[l],w[l],w[l],w[l]};
	else{
		tr[u]={l,r};
		int mid=l+r>>1;
		build(u<<1,l,mid); build(u<<1|1,mid+1,r);
		pushup(u);
	}
}
void modify(int u,int x,int v){
	if(tr[u].l==x&&tr[u].r==x) tr[u]={x,x,v,v,v,v};
	else{
		int mid=tr[u].l+tr[u].r>>1;
		if(x<=mid) modify(u<<1,x,v);
		else modify(u<<1|1,x,v);
		pushup(u);
	}
}
node query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
	else{
		int mid=tr[u].l+tr[u].r>>1;
		if(r<=mid) return query(u<<1,l,r);
		else if(l>mid) return query(u<<1|1,l,r);
		else{
			auto left=query(u<<1,l,r);
			auto right=query(u<<1|1,l,r);
			node res;
			change(res,left,right);
			return res;
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	build(1,1,n);
	while(m--){
		int op,x,y;
		scanf("%d%d%d",&op,&x,&y);
		if(op==1){
			if(x>y) swap(x,y);
			printf("%d\n",query(1,x,y).tmax);
		}
		else{
			modify(1,x,y);
		}
	}
	return 0;
}
3. AcWing 246. 区间最大公约数

“区间修改”+“求区间的最大公约数”  
根据“更相减损法”,我们知道 $gcd(x,y)=gcd(x,y-x)$。它还可以进一步扩展为三个数 $gcd(x,y,z)=gcd(x,y-x,z-y)$,实际上,该性质对任意多个整数都成立。  
我们建立一个长度为 $N$ 的新序列 $B$,其中 $B[i]=A[i]-A[i-1],B[1]$ 可为任意值,一般为$A[1]$。数列 $B$ 称为 $A$差分序列。用线段树维护序列 $B$的区间最大公约数。  
对于询问 “$Q~l~r$”,就等于求出 $gcd(A[l],query(1,l+1,r))$  
对于指令 “$C~l~r~d$”,只有 $B[l]$ 加了 $d$$B[r+1]$ 减掉了 $d$,所以在维护 $B$ 的线段树上只需两次单点修改即可。  
上述需要序列 $B$ 和对序列 $B$ 操作的原因:
假如我们对需要求 gcd 的区间都做这样一个处理,那么当这个区间的初值同时改变时,相邻数字差值并不会改变。当然,第一个数会改变,而区间最后的一个数也会改变。   
比如: $A1,A2,A3,A4,A5,A6$  
经过处理变为:$A_1,A_2-A_1,A_3-A_2,A_4-A_3,A_5-A_4,A_6-A_5$
若把 $A_3$\sim$A_5$ 均加上 $x$  
那么会变为 $A_1,A_2-A_1,A_3-A_2+x,A_4-A_3,A_5-A_4,A_6-A_5-x$
变化的原因是 $A_3$ 变大了,而 $A_2$ 没变,$A_5$ 变大了,而 $A_6$ 没变,所以我们就省去了区间修改的操作  
另外询问时需要数列 $A$ 中的值,需要一个额外的支持“区间增加、单点查询”的树状数组 $C$$A$ 进行维护,也可以直接用线段树维护区间和

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
#define ll long long
struct node{
	int l,r;
	ll sum,d;
}tr[N*4];
int n,m;
ll w[N];
ll gcd(ll a,ll b){
	return b?gcd(b,a%b):a;
}
void change(node &u,node &l,node &r){
	u.sum=l.sum+r.sum;
	u.d=gcd(l.d,r.d);
}
void pushup(int u){
	change(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
	if(l==r){
		ll b=w[r]-w[r-1];
		tr[u]={l,r,b,b};
	}
	else{
		tr[u].l=l; tr[u].r=r;
		int mid=l+r>>1;
		build(u<<1,l,mid); build(u<<1|1,mid+1,r);
		pushup(u);
	}
}
void modify(int u,int x,ll v){
	if(tr[u].l==x&&tr[u].r==x){
		ll b=tr[u].sum+v;
		tr[u]={x,x,b,b};
	}
	else{
		int mid=tr[u].l+tr[u].r>>1;
		if(x<=mid) modify(u<<1,x,v);
		else modify(u<<1|1,x,v);
		pushup(u);
	}
}
node query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
	else{
		int mid=tr[u].l+tr[u].r>>1;
		if(r<=mid) return query(u<<1,l,r);
		else if(l>mid) return query(u<<1|1,l,r);
		else{
			auto left=query(u<<1,l,r);
			auto right=query(u<<1|1,l,r);
			node res;
			change(res,left,right);
			return res;
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%lld",&w[i]);
	build(1,1,n);
	while(m--){
		char op[2]; int l,r; ll d;
		scanf("%s%d%d",op,&l,&r);
		if(op[0]=='Q'){
			auto left=query(1,1,l);
			node right={0,0,0,0};
			if(l+1<=r) right=query(1,l+1,r);
			printf("%lld\n",abs(gcd(left.sum,right.d)));
		}
		else{
			scanf("%lld",&d);
			modify(1,l,d);
			if(r+1<=n) modify(1,r+1,-d);
		}
	}
	return 0;
}

延迟标记

我们在执行 “区间修改指令” 时,同样可以在 $l<=u_l<=u_r<=r$ 的情况下立即返回,只不过在回溯之前向节点 u 增加一个标记,标识“该节点曾经被修改过,但其子节点尚未被更新”。
如果在后续的指令 pushdown 中,需要从节点 u 向下递归,我们再检查 u 是否具有标记。若有标记,就根据标记信息更新 u 的两个子节点,同时为 u 的两个子节点增加标记,然后清除 u 的标记。

标记对于当前点 u 来说,当前点 u 已经更新过了,而其子节点的标记和值没有被更新

例题:

1. AcWing 243. 一个简单的整数问题2

“区间修改”+“查询区间和” 

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+10;
struct node{
	int l,r;
	ll sum,add;
}tr[N*4];
int n,m,w[N];
void pushup(int u){
	tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void pushdown(int u){
	auto &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
	if(root.add){
		left.add+=root.add; left.sum+=(ll)(left.r-left.l+1)*root.add;
		right.add+=root.add; right.sum+=(ll)(right.r-right.l+1)*root.add;
		root.add=0;
	}
}
void build(int u,int l,int r){
	if(l==r) tr[u]={l,r,w[l],0};
	else{
		tr[u].l=l; tr[u].r=r;
		int mid=l+r>>1;
		build(u<<1,l,mid); build(u<<1|1,mid+1,r);
		pushup(u);
	}
}
void modify(int u,int l,int r,ll d){
	if(tr[u].l>=l&&tr[u].r<=r){
		tr[u].sum+=(ll)(tr[u].r-tr[u].l+1)*d;
		tr[u].add+=d;
	}
	else{
		pushdown(u);
		int mid=tr[u].l+tr[u].r>>1;
		if(l<=mid) modify(u<<1,l,r,d);
		if(r>mid) modify(u<<1|1,l,r,d);
		pushup(u);
	}
}
ll query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
	ll res=0;
	pushdown(u);
	int mid=tr[u].l+tr[u].r>>1;
	if(l<=mid) res=query(u<<1,l,r);
	if(r>mid) res+=query(u<<1|1,l,r);
	return res;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	build(1,1,n);
	char op[2]; int l,r; ll d;
	while(m--){
		scanf("%s %d%d",op,&l,&r);
		if(op[0]=='Q'){
			printf("%lld\n",query(1,l,r));
		}
		else{
			scanf("%lld",&d);
			modify(1,l,r,d);
		}
	}
	return 0;
}
2. AcWing 1277. 维护序列

y总的解题思路

先 \times 再 +,这样可以保持形式统一

 代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+10;
struct node{
	int l,r;
	int sum,add,mul;
}tr[N*4];
int n,w[N],p,m;
void eval(node &t,int add,int mul){
	t.sum=((ll)t.sum*mul+(ll)(t.r-t.l+1)*add)%p;
	t.add=((ll)t.add*mul+add)%p;
	t.mul=(ll)t.mul*mul%p;
}
void pushup(int u){
	tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%p;
}
void pushdown(int u){
	eval(tr[u<<1],tr[u].add,tr[u].mul);
	eval(tr[u<<1|1],tr[u].add,tr[u].mul);
	tr[u].add=0; tr[u].mul=1;
}
void build(int u,int l,int r){
	if(l==r) tr[u]={l,r,w[l],0,1};
	else{
		tr[u]={l,r,0,0,1};
		int mid=l+r>>1;
		build(u<<1,l,mid); build(u<<1|1,mid+1,r);
		pushup(u);
	}
}
void modify(int u,int l,int r,int add,int mul){
	if(tr[u].l>=l&&tr[u].r<=r) eval(tr[u],add,mul);
	else{
		pushdown(u);
		int mid=tr[u].l+tr[u].r>>1;
		if(l<=mid) modify(u<<1,l,r,add,mul);
		if(r>mid) modify(u<<1|1,l,r,add,mul);
		pushup(u);
	}
}
int query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
	pushdown(u);
	int res=0;
	int mid=tr[u].l+tr[u].r>>1;
	if(l<=mid) res=query(u<<1,l,r);
	if(r>mid) res=(res+query(u<<1|1,l,r))%p;
	return res;
}
int main(){
	scanf("%d%d",&n,&p);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	build(1,1,n);
	scanf("%d",&m);
	while(m--){
		int op,l,r,d;
		scanf("%d%d%d",&op,&l,&r);
		if(op==1){
			scanf("%d",&d);
			modify(1,l,r,0,d);
		}
		else if(op==2){
			scanf("%d",&d);
			modify(1,l,r,d,1);
		}
		else{
			printf("%d\n",query(1,l,r));
		}
	}
	return 0;
}
3. 发工资咯

这道题我写的和正解略有不同,耗费了我 7 个小时,不断调试修改才写出来的,纪念一下。

正解:

 

 对于一个人 他前 i 天发的总工资即为阴影区域的矩形面积 S,所以我们只需要算出总的红色区域的面积 S_1,再减去红色区域空白的面积 S_2 ,即 S=S_1-S_2 。对于当前人来说,第 i 天增加的工资 c 对 $sum$ 的贡献为  c\times (m-i+1),最后对答案统计时我们只需要减去    S_2=tv\times (m-i),其中 tv 为最后工资的高度。

我们需要 4 个变量来统计,v 代表当前区间所有人工资的总和sum 代表当前区间所有人已经发的工资的总和tv 代表当前区间的子节点每一个人工资应该加上的tsum 代表当前区间的子节点每一个人发的工资应该加上的

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e5+10;
const ll mod=998244353;
struct node{
	int l,r;
	ll v,sum,tv,tsum;
}tr[N*4];
int n,m;
void pushup(int u){
	tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%mod;
	tr[u].v=(tr[u<<1].v+tr[u<<1|1].v)%mod;
	tr[u].tv=tr[u].tsum=0;
}
void pushdown(node &u,node &x,node &y){
	x.sum=(x.sum+(x.r-x.l+1)*u.tsum%mod)%mod;
	x.v=(x.v+(x.r-x.l+1)*u.tv%mod)%mod;
	x.tsum=(x.tsum+u.tsum)%mod;
	x.tv=(x.tv+u.tv)%mod;
	
	y.sum=(y.sum+(y.r-y.l+1)*u.tsum%mod)%mod;
	y.v=(y.v+(y.r-y.l+1)*u.tv%mod)%mod;
	y.tsum=(y.tsum+u.tsum)%mod;
	y.tv=(y.tv+u.tv)%mod;
	
	u.tsum=u.tv=0;
}
void build(int u,int l,int r){
	tr[u]={l,r,0,0,0,0};
	if(l==r) return;
	int mid=l+r>>1;
	build(u<<1,l,mid); build(u<<1|1,mid+1,r);
	pushup(u);
}
void modify(int u,int l,int r,ll c,int i){
	if(tr[u].l>=l&&tr[u].r<=r){
		tr[u].v=(tr[u].v+(tr[u].r-tr[u].l+1)*c%mod)%mod;
		tr[u].sum=(tr[u].sum+(tr[u].r-tr[u].l+1)*c%mod*(m-i+1)%mod)%mod;
		tr[u].tv=(tr[u].tv+c)%mod;
		tr[u].tsum=(tr[u].tsum+c*(m-i+1)%mod)%mod;
		return;
	}
	pushdown(tr[u],tr[u<<1],tr[u<<1|1]);
	int mid=tr[u].l+tr[u].r>>1;
	if(l<=mid) modify(u<<1,l,r,c,i);
	if(r>mid) modify(u<<1|1,l,r,c,i);
	pushup(u);
}
ll query(int u,int l,int r,int i){
	if(tr[u].l>=l&&tr[u].r<=r){
		ll res=(tr[u].sum-tr[u].v*(m-i)%mod+mod)%mod;
		return res;
	}
	pushdown(tr[u],tr[u<<1],tr[u<<1|1]);
	int mid=tr[u].l+tr[u].r>>1;
	ll res=0;
	if(l<=mid) res=(res+query(u<<1,l,r,i))%mod;
	if(r>mid) res=(res+query(u<<1|1,l,r,i))%mod;
	return res;
}
int main(){
	scanf("%d%d",&n,&m);
	build(1,1,n);
	for(int i=1;i<=m;i++){
		int op,l,r; ll c;
		scanf("%d%d%d",&op,&l,&r);
		if(op==0){
			scanf("%lld",&c);
			modify(1,l,r,c,i);
		}
		else{
			ll ans=query(1,l,r,i)%mod;
			printf("%lld\n",ans);
		}
	}
	return 0;
}

我的解法:

struct node{
	int l,r;
	ll v,sum,tv,tsum,tcnt;
// v 工资总和  sum 已经发的工资总和
// tv 对于每个人他要增加的工资
// tsum 对于每个人他要增加的发的工资
// tcnt 几次懒标记没有下传 
}tr[N*4];

 v 所有人的工资总和 , sum 所有人已经发的工资总和  ,tv 对于每个人他要增加的工资, tsum 对于每个人他要增加的发的工资,tcnt 几次懒标记没有下传 

u 为父亲节点 ,x 为子节点

对于 u 父亲节点增加工资,其向下传 懒标记 时 

\begin{vmatrix} x.v+c_1 \\ x.v+c_1+c_2 \\ x.v+c_1+c_2+c_3 \end{vmatrix}

因此 :x.sum=x.sum+x.v\times u.tcnt+(x.r-x.l+1)\times u.tsum

\begin{vmatrix} x.tv+c_1 \\ x.tv+c_1+c_2 \\ x.tv+c_1+c_2+c_3 \end{vmatrix}

因此 :x.tsum=x.tsum+x.tv\times u.tcnt+u.tsum

其他变量更新则是正常加上即可 

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod=998244353;
const int N=2e5+10;
struct node{
	int l,r;
	ll v,sum,tv,tsum,tcnt;
// v 工资总和  sum 已经发的工资总和
// tv 对于每个人他要增加的工资
// tsum 对于每个人他要增加的发的工资
// tcnt 几次懒标记没有下传 
}tr[N*4];
void print(node u,int x){
    printf("%d  %d %d:\n",x,u.l,u.r);
    printf("%lld %lld %lld %lld %lld\n",u.v,u.sum,u.tv,u.tsum,u.tcnt);
}
void pushup(int u){
	tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
	tr[u].v=tr[u<<1].v+tr[u<<1|1].v;
	tr[u].tv=tr[u].tsum=tr[u].tcnt=0;
}
void pushdown(node &u,node &x,node &y){
 	if(!u.tcnt) return;
	x.sum=(x.sum+x.v*u.tcnt%mod+(x.r-x.l+1)*u.tsum%mod)%mod;
	x.v=(x.v+(x.r-x.l+1)*u.tv%mod)%mod;
	x.tsum=(x.tsum+x.tv*u.tcnt%mod+u.tsum)%mod;
	x.tv=(x.tv+u.tv)%mod;
	x.tcnt+=u.tcnt;
	
	y.sum=(y.sum+y.v*u.tcnt%mod+(y.r-y.l+1)*u.tsum%mod)%mod;
	y.v=(y.v+(y.r-y.l+1)*u.tv%mod)%mod;
	y.tsum=(y.tsum+y.tv*u.tcnt%mod+u.tsum)%mod;
	y.tv=(y.tv+u.tv)%mod;
	y.tcnt+=u.tcnt;
//	print(x,55555); print(y,55555);
	u.tv=u.tsum=u.tcnt=0;
}
void build(int u,int l,int r){
	tr[u].l=l; tr[u].r=r;
	if(l==r){
		tr[u].v=tr[u].sum=tr[u].tv=tr[u].tsum=tr[u].tcnt=0;
		return;
	}
	int mid=l+r>>1;
	build(u<<1,l,mid); build(u<<1|1,mid+1,r);
	pushup(u);
}
void modify(int u,int l,int r,ll c){
	if(tr[u].l>=l&&tr[u].r<=r){
		tr[u].v=(tr[u].v+(tr[u].r-tr[u].l+1)*c%mod)%mod;
		tr[u].sum=(tr[u].sum+tr[u].v)%mod;
		tr[u].tv=(tr[u].tv+c)%mod;
		tr[u].tsum=(tr[u].tsum+tr[u].tv)%mod;
		tr[u].tcnt++;
//        print(tr[u],u);
		return;
	}	
	int mid=tr[u].l+tr[u].r>>1;
	pushdown(tr[u],tr[u<<1],tr[u<<1|1]);
	if(l<=mid) modify(u<<1,l,r,c);
	if(r>mid) modify(u<<1|1,l,r,c);
	pushup(u);
//    print(tr[u],u);
}
ll query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
	int mid=tr[u].l+tr[u].r>>1;
	ll res=0;
	pushdown(tr[u],tr[u<<1],tr[u<<1|1]);
	if(l<=mid) res=(res+query(u<<1,l,r))%mod;
	if(r>mid) res=(res+query(u<<1|1,l,r))%mod;
// 	pushup(u);
	return res;
}
int n,q;
int main(){
	scanf("%d%d",&n,&q);
	build(1,1,n);
	while(q--){
		int op,l,r;
		scanf("%d%d%d",&op,&l,&r);
		if(op==0){
			ll c;
			scanf("%lld",&c);
			if(l-1>=1){
//				printf("modify: %d %d\n",1,l-1);
				modify(1,1,l-1,0);
			}
			if(r+1<=n){
//				printf("modify: %d %d\n",r+1,n);
				modify(1,r+1,n,0);
			}
//			printf("modify: %d %d\n",l,r);
			modify(1,l,r,c);
		}
		else{
//			printf("modify: %d %d\n",1,n);
			modify(1,1,n,0);
			ll ans=query(1,l,r);
			printf("%lld\n",ans);
		}
//		printf("\n");
	}
	return 0;
}

扫描线

ys 表示对 y 坐标进行离散化

建树时每一个叶节点都是一段长为 ys[i+1]-ys[i] 的区间

一共有 ys.size()-1 个端点,所以一共有 ys.size()-2 个区间,即线段,其中第 i 个区间表示 [ys[i],ys[i+1]] ,因此建树时需要少建一个点 build(1,0,ys.size()-2)

对于 u 节点来说,其长度为 ys[tr[u].r+1]-ys[tr[u].l]

例题:

1. AcWing 247. 亚特兰蒂斯

总体思路

将竖着的每一条线段 \left \{x_{1},y_{1},y_{2} \right \} 存储,将 y_{1},y_{2} 存储在 ys 中,每一个矩形的左线段的值为 1,右线段的值为 -1

将线段 seg 以 x 的从小到大排序,并将 ys 去重,对 ys 进行建树

tr[u].cnt 表示 u 节点对应的区间全部都被覆盖的次数

tr[u].len 表示 u 节点对应的区间被覆盖的长度,可能全部被覆盖,可能部分被覆盖,可能没有被覆盖的部分

1. 永远只考虑根节点的信息,其子节点的信息不会继承其父节点的信息

2. 所有操作均是成对出现的,且先加后减

y总的讲解:

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
struct Segment{
	double x,y1,y2;
	int k;
	bool operator < (const Segment &t) const{
		return x<t.x;
	}
}seg[N*2];
struct node{
	int l,r;
	int cnt;
	double len;
}tr[N*8];
int n;
vector<double> ys;
int find(double y){
	return lower_bound(ys.begin(),ys.end(),y)-ys.begin();
}
void pushup(int u){
	if(tr[u].cnt) tr[u].len=ys[tr[u].r+1]-ys[tr[u].l];
	else if(tr[u].l!=tr[u].r) tr[u].len=tr[u<<1].len+tr[u<<1|1].len;
	else tr[u].len=0;
}
void build(int u,int l,int r){
	if(l==r) tr[u]={l,r,0,0};
	else{
		tr[u].l=l; tr[u].r=r;
		int mid=l+r>>1;
		build(u<<1,l,mid); build(u<<1|1,mid+1,r);
	}
}
void modify(int u,int l,int r,int k){
	if(tr[u].l>=l&&tr[u].r<=r){
		tr[u].cnt+=k;
		pushup(u);
	}
	else{
		int mid=tr[u].l+tr[u].r>>1;
		if(l<=mid) modify(u<<1,l,r,k);
		if(r>mid) modify(u<<1|1,l,r,k);
		pushup(u);
	}
}
int main(){
	int K=0;
	while(scanf("%d",&n)){
		if(n==0) break;
		ys.clear();
		for(int i=0,j=0;i<n;i++){
			double x1,x2,y1,y2;
			scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
			ys.push_back(y1); ys.push_back(y2);
			seg[j++]={x1,y1,y2,1}; seg[j++]={x2,y1,y2,-1};
		}
		sort(ys.begin(),ys.end());
		ys.erase(unique(ys.begin(),ys.end()),ys.end());
		build(1,0,ys.size()-2);
		sort(seg,seg+n*2);
		double ans=0;
		for(int i=0;i<n*2;i++){
			if(i>0) ans+=tr[1].len*(seg[i].x-seg[i-1].x);
			modify(1,find(seg[i].y1),find(seg[i].y2)-1,seg[i].k); 
		}
		printf("Test case #%d\n",++K);
		printf("Total explored area: %.2lf\n\n",ans);
	}
	
	return 0;
}
2. P1502 窗口的星星

“延迟标记”+扫描线

将每一个星星转化为一个矩形,星星为矩形的左下角,由于窗口的边界看不见,所以将矩形的四个顶点的横纵坐标都减去 0.5,所以在这个矩形中星星在左下角

如图,星星在左下角,矩形即为窗口,所以对于答案统计,我们只需要知道每一条竖着的线段上的每一个元区间的最大值是多少,其为答案。

我们将 y1=y, y2=y+h-1 存储在 ys 中,其中 y2 是因为边框不算,所以去掉

左右线段则为  seg[j_1]=\left \{ x,y,y+h-1,a\right \} ~~ seg[j_2]=\left \{x+w,y,y+h-1,-a \right \}

其中右线段为 x+w 是因为要减去前一个当前矩形的线段

注意:

由于要减去的右线段可能与左线段重合,所以应该先减去右线段,再加左线段。要在 seg.x 相同时,再将 seg.k 值按从小到大排序

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e4+10;
struct Segment{
	int x,y1,y2;
	ll k;
}seg[N*2];
struct node{
	int l,r;
	ll tmax,add;
}tr[N*8];
int T,n,w,h;
vector<int> ys;
bool cmp(Segment t1,Segment t2){
	if(t1.x==t2.x) return t1.k<t2.k;
	return t1.x<t2.x;
}
int find(int y){
	return lower_bound(ys.begin(),ys.end(),y)-ys.begin();
}
void pushup(int u){
	tr[u].tmax=max(tr[u<<1].tmax,tr[u<<1|1].tmax);
}
void pushdown(int u){
	node &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
	if(root.add){
		left.tmax+=root.add; left.add+=root.add;
		right.tmax+=root.add; right.add+=root.add;
		root.add=0;
	}
}
void build(int u,int l,int r){
	if(l==r) tr[u]={l,r,0,0};
	else{
		tr[u]={l,r,0,0};
		int mid=l+r>>1;
		build(u<<1,l,mid); build(u<<1|1,mid+1,r);
	}
}
void modify(int u,int l,int r,int k){
	if(tr[u].l>=l&&tr[u].r<=r){
		tr[u].tmax+=k;
		tr[u].add+=k;
	}
	else{
		pushdown(u);
		int mid=tr[u].l+tr[u].r>>1;
		if(l<=mid) modify(u<<1,l,r,k);
		if(r>mid) modify(u<<1|1,l,r,k);
		pushup(u);
	}
}
int main(){
	scanf("%d",&T);
	while(T--){
		ys.clear();
		scanf("%d%d%d",&n,&w,&h);
		for(int i=0,j=0;i<n;i++){
			int x,y; ll a;
			scanf("%d%d%lld",&x,&y,&a);
			ys.push_back(y); ys.push_back(y+h-1);
			seg[j++]={x,y,y+h-1,a}; seg[j++]={x+w,y,y+h-1,-a};
		}
		sort(seg,seg+2*n,cmp);
		sort(ys.begin(),ys.end());
		ys.erase(unique(ys.begin(),ys.end()),ys.end());
//		for(int i=0;i<ys.size();i++)
//			printf("%d ",ys[i]);
//		printf("\n");
		build(1,0,ys.size()-1);
		ll ans=-1;
		for(int i=0;i<2*n;i++){
			modify(1,find(seg[i].y1),find(seg[i].y2),seg[i].k);
//			printf("%d\n",tr[1].tmax);
			ans=max(ans,tr[1].tmax);
		}
		printf("%lld\n",ans);
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值