目录
1 写在前面
我不会线段树。
暑假啦,我来更新惹!
我也知道这样敲没什么用,所以可能这会变成坑。
先看个大概,等写题用到了再学,用着用着就会了…吧。
为什么我感觉最近遇到好多线段树的题,知道要用这个,却不会,就很难受。
嗯我先学图。
1.1 什么是线段树
线段树是一种高级数据结构。
2 线段树基础操作
2.1 线段树的构建和查询
void build(int l,int r,int x){
if(l==r){sum[x]=a[l];return;}
int mid=(l+r)>>1; build(l,mid,x<<1); build(mid+1,r,x<<1|1);
update(x);
}
int query(int A,int B,int l,int r,int x){
if(A<=l&&r<=B) return sum[x];
int mid=(l+r)>>1,ans=0;
if(A<=mid) ans+=query(A,B,l,mid,x<<1);
if(mid<B) ans+=query(A,B,mid+1,r,x<<1|1)
return ans;
}
线段树的数组储存
类似堆的储存方式。数组大小要开到 4*n ,因为不一定是完全二叉树。
建立线段树
自顶向下。
void build(int l,int r,int x){
if(l==r){ //叶子结点
sum[x]=a[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,x<<1);
build(mid+1,r,x<<1|1);
//建完左右子树,更新当前结点信息
update(x);
}
线段树的查询
也是自顶向下。
//在区间[l,r]中 ,询问区间[A,B]的值。
int query(int A,int B,int l,int r,int x){
//若 A<=l&&r<=B 直接返回此结点信息,停止递归
if(A<=l&&r<=B) return sum[x];
int mid=(l+r)>>1,ans=0;
//询问区间与左子结点有重合
if(A<=mid) ans+=query(A,B,l,mid,x<<1);
//询问区间与右子结点有重合
if(mid<B) ans+=query(A,B,mid+1,r,x<<1|1)
return ans;
}
线段树的链表储存(但我才不用链表)
struct node{
//left和rigtt用来表示该结点代表的区间
//value维护这个区间的信息,比如区间和等等
int left,right,value;
//每个结点同时维护两个孩子的指针。
node *lchild,*rchild;
};
2.2 线段树的单点修改
线段树的单点修改
int change(int pos,int v,int l,int x){
if(l==r){ //找到了叶子结点
sum[x]=v;
return;
}
int mid=(l+r)>>1;
//找pos在左结点还是右结点
if(pos<=mid) change(pos,v,l,mid,x<<1);
else change(pos,v,mid+1,r,x<<1|1);
update(x); //别忘了更新这一整条路的sum
}
离散化
unique();
int cnt=0;
for(int i=0;i<n;++i) bin[++cnt]=a[i];
sort(bin,bin+n);
cnt=unique(bin,bin+n)-bin;
for(int i=0;i<n;++i) a[i]=lower_bound(bin,bin+cnt,a[i])-bin;
2.3 线段树的区间修改
延迟修改(Lazy tag)
直到需要用的时候才修改(标记下传),十分lazy,像我一样…
整体时间复杂度仍然维持在O(logn);
举个例子:把区间 [l,r] 的数都修改为 v ,查询区间和
为了方便,定义两个宏
#define ls (x<<1)
#define rs (x<<1|1)
//例如update()就可以写成:
void update(int x){
sum[x]=sum[ls]+sum[rs];
}
标记下传
void down(int l,int r,int x){
int mid=(l+r)>>1;
if(tag[x]>0){
tag[ls]=tag[rs]=tag[x];
//此处以把区间[l,r]的数都修改为 v 为例
sum[ls]=(mid-l+1)*tag[x];
sum[rs]=(r-mid)*tag[x];
tag[x]=0;
}
}
修改 [A,B] 区间为v
void change(int A,int B,int v,int l,int r,int x){
if(A<=l&&r<=B){ //若[A,B]包含了 [l,r]
tag[x]=v; //延迟修改 停止递归
sum[x]=v*(r-l+1);
return;
}
//继续修改前,要检查是否需要下传标记
down(l,r,x);
//修改子节点,类比查询操作
int mid=(l+r)>>1;
if(A<=mid) change(A,B,v,l,mid,ls);
if(mid<B) change(A,B,v,mid+1,r,rs);
update(x); //记得更新
}
查询 [A,B] 的区间和
int query(int A,int B,int l,int r,int x){
if(A<l&&r<=B) return sum[x];
down(l,r,x); //继续查询之前,先检查是否要下传标记
int mid=(l+r)>>1,ret=0;
if(A<=mid) ret+=query(A,B,l,mid,ls);
if(mid<B) ret+=query(A,B,mid+1,r,rs);
//update(x); //查询就不用更新了,并没有值发生改变
return ret;
}
举二个例子:把区间 [l,r] 的数都 加上 v ,查询区间和
//只需要修改down()和把change()改为add()
void down(int l,int r,int x){
int mid=(l+r)>>1;
if(tag[x]!=0){
tag[ls]+=tag[x];
tsg[rs]+=tag[x];
sum[ls]+=(mid-l+1)*tag[x];
sum[rs]+=(r-mid)*tag[x];
tag[x]=0;
}
}
void add(int A,int B,int v,int l,int r,int x){
if(A<=l&&r<=B){ //若[A,B]包含了 [l,r]
tag[x]+=v; //延迟修改 停止递归
sum[x]+=v*(r-l+1);
return;
}
//继续修改前,要检查是否需要下传标记
down(l,r,x);
//修改子节点,类比查询操作
int mid=(l+r)>>1;
if(A<=mid) add(A,B,v,l,mid,ls);
if(mid<B) add(A,B,v,mid+1,r,rs);
update(x); //记得更新
}
举三个例子:把区间 [l,r] 的数都加上 v ,查询区间最小值
void update(int x){ //只改变了 here
Min[x]=min(Min[ls],Min[rs]);
}
void down(int l,int r,int x){
int mid=(l+r)>>1;
if(tag[x]!=0){ //here
tag[ls]+=tag[x];
tag[rs]+=tag[x];
Min[ls]+=tag[x];
Min[rs]+=tag[x];
tag[x]=0;
}
}
void add(int A,int B,int v,int l,int r,int x){
if(A<=l&&r<=B){
tag[x]+=v;
Min[x]+=v; //here
return;
}
down(l,r,x);
int mid=(l+r)>>1;
if(A<=mid) add(A,B,v,l,mid,ls);
if(mid<B) add(A,B,v,mid+1,r,rs);
update(x);
}
int query(int A,int B,int l,int r,int x){
if(A<l&&r<=B) return Min[x]; //here
down(l,r,x);
int mid=(l+r)>>1,ret=INF; //here
if(A<=mid) ret+=min(ret,query(A,B,l,mid,ls)); //here
if(mid<B) ret+=min(query(A,B,mid+1,r,rs));
return ret;
}
举四个例子:把区间 [l,r] 的数都加上 v ,查询区间最小值的个数
void update(int x){
if(Min[ls]==Min[rs]){ //若左右子结点最小值相等,则把左右子结点的个数相加
Min[x]=Min[ls];
cnt[x]=cnt[ls]+cnt[rs];
}
else{ //反之,取小
if(Min[ls]<Min[rs]) Min[x]=Min[ls],cnt[x]=cnt[ls];
else Min[x]=Min[rs],cnt[x]=cnt[rs];
}
}
void down(int l,int r,int x){
//此处cnt是不用改变的
int mid=(l+r)>>1;
if(tag[x]!=0){
tag[ls]+=tag[x];
tag[rs]+=tag[x];
Min[ls]+=tag[x];
Min[rs]+=tag[x];
tag[x]=0;
}
}
void add(int A,int B,int v,int l,int r,int x){
if(A<=l&&r<=B){
tag[x]+=v;
Min[x]+=v;
return;
}
down(l,r,x);
int mid=(l+r)>>1;
if(A<=mid) add(A,B,v,l,mid,ls);
if(mid<B) add(A,B,v,mid+1,r,rs);
update(x);
}
//但区间查询就有丢丢麻烦了
//此处用pair数对来表示 Min和cnt
pair<int,int> query(int A,int B,int l,int r,int x){
if(A<l&&r<=B) return make_pair(Min[x],cnt[x]);
down(l,r,x);
int mid=(l+r)>>1,ret=make_pair(INF,0);
if(A<=mid) ret=query(A,B,l,mid,ls);
if(mid<B){
pair<int,int> tmp=query(A,B,mid+1,r,rs);
if(tmp.first==ret.first) ret.second+=tmp.second;
else if(tmp.first<ret.first) ret=tmp;
}
return ret;
}
总结: 要想清楚结点的更新与标记的下传
3 线段树常见应用
3.1 扫描线法
经典问题:矩形的面积并。
二维平面上有n个矩形,告诉你第 i i i 个矩形的左上角和右下角坐标( x x x1[ i i i ], y y y1[ i i i ]),( x x x2[ i i i ], y y y2[ i i i ])。求这些矩形的面积并。( n < = 1 e 5 n<=1e5 n<=1e5, 0 < x , y < 3 e 4 0<x,y<3e4 0<x,y<3e4)。
扫描线的思想:枚举第一维,统计第二维。
对于本题:考虑对于每个y,统计x坐标上哪些被矩形覆盖了。