一、问题——一定要开四倍空间啊啊啊
1)query
过程中区间的情况讨论
设 T l , T r T_l,T_r Tl,Tr为线段树中的区间左右端点, l , r l,r l,r为查询的区间左右端点
-
[ l , r ] ⊃ [ T l , T r ] [l,r]\supset[T_l,T_r] [l,r]⊃[Tl,Tr],直接返回,不再进行往下搜索——由于这一步的剪枝可以时
query
操作控制在== O ( 4 ∗ l o g N ) O(4*logN) O(4∗logN)==的复杂度 -
[ l , r ] ∩ [ T l , T r ] ≠ ∅ [l,r]\cap[T_l,T_r]\neq\emptyset [l,r]∩[Tl,Tr]=∅
-
T l < = l < = T r < = r , T m i d = T l + T r > > 1 T_l<=l<=T_r<=r,T_{mid}=T_l+T_r>>1 Tl<=l<=Tr<=r,Tmid=Tl+Tr>>1,
若 l > T m i d l>T_{mid} l>Tmid,递归右区间 [ T m i d + 1 , T r ] [T_{mid+1},T_r] [Tmid+1,Tr]
若 l < = T m i d l<=T_{mid} l<=Tmid,递归左区间、右区间
-
l < = T l < = r < = T r l<=T_l<=r<=T_r l<=Tl<=r<=Tr
-
T l < = l < = r < = T r , T m i d = T l + T r > > 1 T_l<=l<=r<=T_r,T_{mid}=T_l+T_r>>1 Tl<=l<=r<=Tr,Tmid=Tl+Tr>>1
若 r < = T m i d r<=T_{mid} r<=Tmid,递归左区间
若 l > T m i d l>T_{mid} l>Tmid,递归右区间
其他情况,递归左区间、右区间
-
-
[ l , r ] ∩ [ T l , T r ] = ∅ [l,r]\cap[T_l,T_r]=\emptyset [l,r]∩[Tl,Tr]=∅——不存在,因为
query
操作是从根结点向下遍历的,每次递归都只会涉及由交集的区间,所以不会产生交集为空集的情况
2)线段树数组tr
应开到点个数的四倍大小
当线段树时满二叉树时,显然结点个数时 2 ∗ n − 1 2*n-1 2∗n−1,若不是满二叉树,则结点编号有可能会超过 2 ∗ n − 1 2*n-1 2∗n−1(因为最后一层不一定是按照完全二叉树的状态存点的,所以编号可能会很大,超过 2 ∗ n − 1 2*n-1 2∗n−1)
3)注意区间左右端点有可能会需要互换
(谁知道出题人会不会恶心人呢)
4)懒标记、pushdown
操作
只有对区间进行加减可以利用差分的技巧维护线段树,就不用懒标记了
懒标记是一个从上往下的操作,类似于query操作的区间查找,复杂度在== O ( 4 ∗ l o g N ) O(4*logN) O(4∗logN)==
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TQVUhbs6-1652749238520)(C:\Users\19322\Pictures\Saved Pictures\懒标记.png)]
懒标记含义是若当前结点有懒标记,则对当前结点为根的子树中的每一个结点进行操作(add),且不包含根节点
我们维护的懒标记,是方便在query
或modify
过程中对儿子结点进行信息的更新
pushdown
- 在
query
过程中,我们枚举线段树中对应的区间时需要将懒标记传到下方 - 在
modify
过程中,我们也需要将之前modify
所添加的懒标记传给下方结点——若不执行pushdown操作的话,不能对线段树中结点信息进行及时修改,若进行查询操作,则返回的仍是未修改的旧值
pushup
- 在
modify
过程中,修改完信息之后需要将信息更新到父节点
5)权值线段树
6)主席树
二、例题:
最大数
题意:
单点修改,查询区间最大值
题解:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
int m,p;
struct Node{
int l,r;
int v;
}tr[N*4];
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,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=query(u<<1,l,r);
if(r>mid) v=max(v,query(u<<1|1,l,r));
return v;
}
void modfiy(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) modfiy(u<<1,x,v);
else modfiy(u<<1|1,x,v);
pushup(u);
}
}
int main(){
int 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=='Q'){
last=query(1,n-x+1,n);
printf("%d\n",last);
}
else{
modfiy(1,n+1,((ll)x+last)%p);
n++;
}
}
return 0;
}
你能回答这些问题吗
题意:
单点修改,查询一个区间中连续的最大连续子段和
题解:
区间最值有可能横跨两个子区间,所以需要维护前缀最值和后缀最值
在更新前缀后缀最值是需要知道区间和,所以还需要维护区间和
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=5e5+10;
struct P{
int l,r;
int sum,lmax,rmax,tmax;//区间和,前缀最值,后缀最值,区间最值
}tr[4*N];
int n,m;
int w[N];
void pushup(P& u,P& l,P& r){//由儿子结点更新而来,所以不能与之前的值取max
u.sum=l.sum+r.sum;
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);//区间最值为左儿子的区间最值、右儿子的区间最值、左右儿子相连接的部分三者取最大值
u.lmax=max(l.sum+r.lmax,l.lmax);
u.rmax=max(r.sum+l.rmax,r.rmax);
}
void pushup(int u){
pushup(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);
}
}
P query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
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{
P root,left,right;
left=query(u<<1,l,r);
right=query(u<<1|1,l,r);
pushup(root,left,right);
return root;
}
}
void modify(int u,int pos,int v){
if(tr[u].l==pos&&tr[u].r==pos) tr[u]={pos,pos,v,v,v,v};//修改时修改结点的所有信息
else{
int mid=tr[u].l+tr[u].r>>1;
if(pos<=mid) modify(u<<1,pos,v);
else modify(u<<1|1,pos,v);
pushup(u);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
while(m--){
int x,l,r; cin>>x>>l>>r;
if(x==1){
if(l>r) swap(l,r);
auto t=query(1,l,r);
cout<<t.tmax<<endl;
}
else modify(1,l,r);
}
return 0;
}
区间最大公约数
题意:
区间加减,查询区间的最大公约数
题解:
g c d ( a 1 , a 2 , . . . , a n ) = g c d ( a 1 , a 2 − a 1 , . . . , a n − a n − 1 ) gcd(a_1,a_2,...,a_n)=gcd(a_1,a_2-a_1,...,a_n-a_{n-1}) gcd(a1,a2,...,an)=gcd(a1,a2−a1,...,an−an−1)
证明:
- 证明: g c d ( a 1 , a 2 , . . . , a n ) < = g c d ( a 1 , a 2 − a 1 , . . . , a n − a n − 1 ) gcd(a_1,a_2,...,a_n)<=gcd(a_1,a_2-a_1,...,a_n-a_{n-1}) gcd(a1,a2,...,an)<=gcd(a1,a2−a1,...,an−an−1)
设 d d d为左式的最大公约数,显然有 d ∣ a 1 , d ∣ a 2 d|a_1,d|a_2 d∣a1,d∣a2,所以有 d ∣ ( a 2 − a 1 ) d|(a_2-a_1) d∣(a2−a1),所以d一定是右式的一个公约数
- 证明: g c d ( a 1 , a 2 , . . . , a n ) > = g c d ( a 1 , a 2 − a 1 , . . . , a n − a n − 1 ) gcd(a_1,a_2,...,a_n)>=gcd(a_1,a_2-a_1,...,a_n-a_{n-1}) gcd(a1,a2,...,an)>=gcd(a1,a2−a1,...,an−an−1)
设 d d d为右式的最大公约数,显然有 d ∣ a 1 , d ∣ ( a 2 − a 1 ) d|a_1,d|(a_2-a_1) d∣a1,d∣(a2−a1),所以有 d ∣ ( a 2 − a 1 + a 1 ) ⇒ d ∣ a 2 d|(a_2-a_1+a_1)\ \Rightarrow \ d|a_2 d∣(a2−a1+a1) ⇒ d∣a2,所以 d d d一定是左式的一个公约数
所以可以将查询区间最大公约数变为:
查询 a [ l ] a[l] a[l]和 b [ l + 1 ] b[l+1] b[l+1]~ b [ r ] b[r] b[r]的最大公约数(b为原序列a的差分数组)
所以只需要用线段树维护b的区间和以及区间最大公约数即可
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;//注意数据都要ll
const int N=5e5+10;
struct P{
int l,r;
ll sum,gcd;
}tr[4*N];
int n,m;
ll w[N];
ll gcd(ll a,ll b){
if(!b) return a;
return gcd(b,a%b);
}
void pushup(P &u,P &l,P &r){
u.sum=l.sum+r.sum;
u.gcd=gcd(l.gcd,r.gcd);
}
void pushup(int u){
pushup(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[r]};
else{
tr[u]={l,r};
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
P query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
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{
P root,left,right;
left=query(u<<1,l,r);
right=query(u<<1|1,l,r);
pushup(root,left,right);
return root;
}
}
void modify(int u,int pos,ll v){
if(tr[u].l==pos&&tr[u].r==pos){
v+=tr[u].sum;
tr[u]={pos,pos,v,v};
}
else{
int mid=tr[u].l+tr[u].r>>1;
if(pos<=mid) modify(u<<1,pos,v);
else modify(u<<1|1,pos,v);
pushup(u);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=n;i>=1;i--) w[i]-=w[i-1];
build(1,1,n);
while(m--){
string s;int l,r;
cin>>s>>l>>r;
if(s=="C"){
ll d; cin>>d;
modify(1,l,d);
if(r+1<=n) modify(1,r+1,-d);
}
else{
P a=query(1,1,l),b={0,0,0,0};
if(l+1<=r) b=query(1,l+1,r);
cout<<abs(gcd(a.sum,b.gcd))<<endl;
}
}
return 0;
}
亚特兰蒂斯(扫描线+离散化+线段树)
题意:
求n个可能有交集的矩形的面积和
题解:
从左往右枚举坐标x,用线段树维护坐标y,扫描线分块求每个小块的面积和
1)线段树叶子节点的是一段区间
2)我们每次取的是根节点的区间长度,所以不需要query
操作
3)为何不需要懒标记?
query
操作只查询根节点,不会用到pushdown
- 由于扫描线的特殊性,每次区间查询都会自行抵消,所以
modify
操作可以不用进行pushdown
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
//存储线段的信息
struct P1{
double x,y1,y2;
int k;
bool operator<(const P1 &t)const{
return x<t.x;
}
}g[N*2];
// 线段树的每个节点 保存的为线段,0号点为y[0]到y[1],以此类推
struct P2{
int l,r;
double len;//区间长度
int cnt;//判断是否算作长度
}tr[8*N];
//离散化后的y坐标位置
vector<double>ve;
int n;
int find(double x){
return lower_bound(ve.begin(),ve.end(),x)-ve.begin();
}
void pushup(int u){
if(tr[u].cnt) tr[u].len=ve[tr[u].r+1]-ve[tr[u].l];//如果cnt>0,注意不能+1(区间长度)
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,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 v){
if(tr[u].l>=l&&tr[u].r<=r){
tr[u].cnt+=v;
pushup(u);//u节点cnt的改变可能会导致len的改变,所以需要在这个地方pushup,在之前的题目中,u的值只与他的子孙节点有关,而本题中len可能由自身cnt的取值更新。
}
else{
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,v);
if(r>mid) modify(u<<1|1,l,r,v);
pushup(u);
}
}
int main(){
int T=1;
while(cin>>n,n){
ve.clear();
for(int i=0,j=0;i<n;i++){
double x1,x2,y1,y2; cin>>x1>>y1>>x2>>y2;
g[j++]={x1,y1,y2,1};
g[j++]={x2,y1,y2,-1};
ve.push_back(y1),ve.push_back(y2);//离散化存放y轴坐标
}
//离散化
sort(ve.begin(),ve.end());
ve.erase(unique(ve.begin(),ve.end()),ve.end());
build(1,0,ve.size()-2);//每个叶子节点tr[i]代表一个区间[ve[i],vr[i+1]]
//对x轴排序
sort(g,g+2*n);
double ans=0;
for(int i=0;i<2*n;i++){
if(i) ans+=(g[i].x-g[i-1].x)*tr[1].len;//判断i点之前的那个区间的面积
modify(1,find(g[i].y1),find(g[i].y2)-1,g[i].k);//注意find(g[i].y2)-1,因为线段树的叶子节点代表的是一个区间
}
printf("Test case #%d\n",T++);
printf("Total explored area: %.2lf\n\n",ans);
}
return 0;
}
一个简单的整数问题2
题意:
区间修改,查询区间和
题解:
1)线段树含懒标记
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct P{
int l,r;
ll sum,add;
}tr[4*N];
int n,m;
int w[N];
void pushdown(int u){
auto &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
if(root.add){
left.sum+=(left.r-left.l+1)*root.add,left.add+=root.add;
right.sum+=(right.r-right.l+1)*root.add,right.add+=root.add;
root.add=0;
}
}
void pushup(int u){
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r){
if(l==r) tr[u]={l,r,w[l],0};
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 l,int r,int v){
if(tr[u].l>=l&&tr[u].r<=r){
tr[u].sum+=(ll)(tr[u].r-tr[u].l+1)*v;
tr[u].add+=v;
}
else{
pushdown(u);//向下修改时要pushdown
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,v);
if(r>mid) modify(u<<1|1,l,r,v);
pushup(u);
}
}
ll query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
pushdown(u);//向下查询时要pushdown
int mid=tr[u].l+tr[u].r>>1;
ll d=0;
if(l<=mid) d=query(u<<1,l,r);
if(r>mid) d+=query(u<<1|1,l,r);
return d;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
while(m--){
string s;int l,r,d;
cin>>s>>l>>r;
if(s=="C"){
cin>>d;
modify(1,l,r,d);
}
else cout<<query(1,l,r)<<endl;
}
return 0;
}
2)线段树无懒标记——类似树状数组做法
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct P{
int l,r;
ll sumb,suma;
}tr[4*N];
int n,m;
int w[N];
void pushup(P &root,P &left,P &right){
root.sumb=left.sumb+right.sumb;
root.suma=left.suma+right.suma;
}
void pushup(int u){
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
if(l==r) tr[u]={l,r,(ll)w[l]*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 pos,int v){
if(tr[u].l==pos&&tr[u].r==pos){
tr[u].sumb+=(ll)v*pos;
tr[u].suma+=v;
}
else{
int mid=tr[u].l+tr[u].r>>1;
if(pos<=mid) modify(u<<1,pos,v);
else modify(u<<1|1,pos,v);
pushup(u);
}
}
P query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
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{
P root,left,right;
left=query(u<<1,l,r);
right=query(u<<1|1,l,r);
pushup(root,left,right);
return root;
}
}
ll res(int x){
return query(1,1,x).suma*(x+1)-query(1,1,x).sumb;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=n;i>=1;i--) w[i]-=w[i-1];
build(1,1,n);
while(m--){
string s;int l,r,d;
cin>>s>>l>>r;
if(s=="C"){
cin>>d;
modify(1,l,d);
if(r+1<=n) modify(1,r+1,-d);
}
else{
ll ans=res(r);
if(l-1>=1) ans-=res(l-1);
cout<<ans<<endl;
}
}
return 0;
}
维护序列
题意:
区间修改(加、乘),查询区间和
题解:
如何计算modify
之后的值?(计算顺序)
-
若采用先加后乘的方式,即需要将每一个节点变成 ( x + a d d ) ∗ m u l (x+add)*mul (x+add)∗mul的形式,若之后加的值不能被 m u l mul mul整除,则不好维护成 ( x + a d d ) ∗ m u l (x+add)*mul (x+add)∗mul的形式
-
若采用先乘后加的方式,即需要将每一个节点变成 x ∗ m u l + a d d x*mul+add x∗mul+add,若后面加一个数,可将式子变为 x ∗ m u l + ( a d d 1 + a d d 2 ) x*mul+(add^1+add^2) x∗mul+(add1+add2)的形式,若乘一个数可变为 x ∗ m u l 1 ∗ m u l 2 + a d d ∗ m u l 2 x*mul^1*mul^2+add*mul^2 x∗mul1∗mul2+add∗mul2的形式
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct P{
int l,r;
int sum,add,mul;
}tr[4*N];
int n,m,mod;
int w[N];
void work(P &t,int add,int mul){//更新
t.sum=((ll)t.sum*mul+(ll)(t.r-t.l+1)*add)%mod;
t.add=((ll)t.add*mul+add)%mod;
t.mul=(ll)t.mul*mul%mod;
}
void pushdown(int u){//维护懒标记
work(tr[u<<1],tr[u].add,tr[u].mul);
work(tr[u<<1|1],tr[u].add,tr[u].mul);
tr[u].add=0,tr[u].mul=1;
}
void pushup(int u){
tr[u].sum=((ll)tr[u<<1].sum+tr[u<<1|1].sum)%mod;
}
void build(int u,int l,int r){
if(l==r) tr[u]={l,r,w[l],0,1};//注意非叶子节点乘的懒标记也要记为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) work(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 mid=tr[u].l+tr[u].r>>1;
int res=0;
if(l<=mid) res=query(u<<1,l,r);
if(r>mid) res=(res+query(u<<1|1,l,r))%mod;
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>mod;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
cin>>m;
while(m--){
int f,l,r,d;
cin>>f>>l>>r;
if(f==3) cout<<query(1,l,r)<<endl;
else if(f==1){
cin>>d;
modify(1,l,r,0,d);
}
else{
cin>>d;
modify(1,l,r,d,1);
}
}
return 0;
}