title : 线段树基础
date : 2021-8-15
tags : ACM,数据结构
线段树
线段树基础
首先上个板子来复习一下线段树的基本写法。
//基础板 P3372 【模板】线段树 1
#include<bits/stdc++.h>
using namespace std;
int n,m,l,r,k,q;
long long arr[100005],tree[270000],lazy[270000];
void build(int node,int l,int r){ //建树
if(l==r){
tree[node]=arr[l];
return;
}
int mid=(l+r)/2;
build(node*2,l,mid); //左区间建树
build(node*2+1,mid+1,r); //右区间建树
tree[node]=tree[node*2]+tree[node*2+1]; //区间和
}
void pushdown(int node,int start,int end){ //下传操作
int mid=(start+end)/2;
if(lazy[node]){
tree[node*2]+=lazy[node]*(mid-start+1); //更新区间和
tree[node*2+1]+=lazy[node]*(end-mid);
lazy[node*2]+=lazy[node]; //懒标记下传
lazy[node*2+1]+=lazy[node];
}
lazy[node]=0;
}
void update(int node,int start,int end,int l,int r,int c){ //更新操作
if(l<=start&&end<=r){ //如果区间在更新范围内,直接标记返回
tree[node]+=(end-start+1)*c; //区间和加上Len倍的c
lazy[node]+=c; //打标记
return;
}
int mid=(start+end)/2;
pushdown(node,start,end); //下传
if(l<=mid){update(node*2,start,mid,l,r,c);} //更新左区间
if(r>mid){update(node*2+1,mid+1,end,l,r,c);}//更新右区间
tree[node]=tree[node*2]+tree[node*2+1]; //pushup
}
long long query(int node,int start,int end,int l,int r){ //查询操作
int mid=(start+end)/2;
if(l<=start&&end<=r) return tree[node]; //如果在查询范围内,直接返回
pushdown(node,start,end);
long long sum=0;
if(l<=mid) sum=query(node*2,start,mid,l,r); //查询左区间
if(r>mid) sum+=query(node*2+1,mid+1,end,l,r); //查询右区间
return sum;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&arr[i]);
build(1,1,n);
while(m--){
scanf("%d%d%d",&q,&l,&r);
if(q==1){
scanf("%d",&k);
update(1,1,n,l,r,k); //区间修改
}else printf("%lld\n",query(1,1,n,l,r)); //区间查询
}
return 0;
}
延迟标记
延迟标记,也叫lazy tag,是在区间中新增一个标记,在下一次访问该区间时,向左右区间下放(pushdown)节点的标记,以便完成区间修改+区间询问。
区间染色
例题:POJ 2528 Mayor’s posters
区间离散化
对于区间[1,5],[2,7],[7,100],[3,1e7],我们肯定不能直接对区间[1,1e7]进行修改,而是应该先进行排序并离散化,1->1,2->2,3->3,5->4,7->5,100->6,1e7->7,之后区间可以表示为[1,4],[2,5],[5,6],[3,7]。然而这样的表示实际上扩大了访问的区间,因此我们要在间隔大于1的两个元素中间再加数字,正确的表示方法如下:
x
[
1
]
=
1
,
x
[
2
]
=
2
,
x
[
3
]
=
3
,
x
[
4
]
=
4
,
x
[
5
]
=
5
,
x
[
6
]
=
6
,
x
[
7
]
=
7
,
x
[
8
]
=
100
,
x
[
9
]
=
101
,
x
[
10
]
=
1
e
7
x[1]=1,x[2]=2,x[3]=3,x[4]=4,x[5]=5,x[6]=6,x[7]=7,x[8]=100,x[9]=101,x[10]=1e7
x[1]=1,x[2]=2,x[3]=3,x[4]=4,x[5]=5,x[6]=6,x[7]=7,x[8]=100,x[9]=101,x[10]=1e7
发现了新增的节点x[4],x[6]和[x8],这有什么用呢?
比如我要涂色[1,3]->颜色1,[5,7]->颜色2,加点前[1,3]->颜色1,[4,5]颜色2,可以发现最终两种颜色把[1,5]覆盖掉了,事实上中间还有一片(3,4)颜色未处理;我们增加节点后的效果是涂掉[1,3],[5,7],那么数颜色数量的时候就能答案就修正了。
//Mayor's posters
#include<iostream>
#include<vector>
#include<algorithm>
#define MID int mid=(p->l + p->r)>>1
using namespace std;
struct Post{ // 海报
int l,r;
} pst[10100];
int nodenum,cnt,ans;
int t,n,hs[10000010];
vector<int>vt;
struct Node{
int l,r;
bool full; // 区间[l,r]是否被完全覆盖
Node *ls, *rs;
}tr[1000000];
void buildTree(Node *p, int l, int r){ //建树
p->l=l;
p->r=r;
p->full=0; //初始化节点
if(l==r)return; //如果是叶子,直接返回
nodenum++;
p->ls=tr+nodenum; //建立左右子树
nodenum++;
p->rs=tr+nodenum;
MID;
buildTree(p->ls,l,mid); //左区间建树
buildTree(p->rs,mid+1,r); //右区间建树
}
bool check(Node *p,int l,int r){ //判断该区间是否有覆盖
if (p->full) return false; //已经被覆盖了,这份海报就看不到了
if (p->l==l&&p->r==r){
p->full=1; //覆盖此区间
return true;
}
bool res;
MID;
if(r<=mid) res=check(p->ls,l,r); //全在左区间,找左子树
else if(l>mid) res=check(p->rs,l,r);
else{
bool b1=check(p->ls,l,mid);
bool b2=check(p->rs,mid+1,r);
res=b1||b2; //其中一点没覆盖即可
}
if (p->ls->full&&p->rs->full){
p->full=1;//如果左右区间都被覆盖,那么这个区间也被覆盖了
}
return res;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--){
cin>>n;
nodenum=0,cnt=0,ans=0; //清空数据
for(int i=0;i<n;i++) {
cin>>pst[i].l>>pst[i].r;
vt.push_back(pst[i].l); //把区间端点放入容器
vt.push_back(pst[i].r);
}
sort(vt.begin(),vt.end());//排序
vt.erase(unique(vt.begin(),vt.end()),vt.end()); //去重
for(int i=0;i<vt.size();i++){
hs[vt[i]]=cnt++; //记录元素所在数的节点编号
if(i<vt.size()-1){
if(vt[i+1]-vt[i]>1) cnt++; //中间再插一个点
}
}
buildTree(tr,0,cnt);//开始建树
for(int i=n-1;i>=0;i--){ //这里倒序,要从没被覆盖的开始数
if(check(tr,hs[pst[i].l],hs[pst[i].r])){
ans++; //可见海报增加
}
}
cout<<ans<<endl;
}
return 0;
}
区间第K大
由于篇幅问题这个问题另开一篇主席树的博文讲吧。
扫描线线段树
扫描线算法可用于解决多个矩形围成的周长和面积问题。
扫描线算法
用一根线对图像从下往上进行扫描,扫描到边的时候对答案进行计算。相当于给妙计分层,然后计算每一层的面积,最后汇总到答案当中。
矩阵面积并
给定多个矩形,边一定平行于x或y轴。求所有矩形的面积之和。
(
1
)
每
个
矩
形
的
上
下
边
加
入
扫
描
线
并
标
记
,
下
边
标
记
为
1
,
上
边
标
记
为
−
1
(
2
)
让
扫
描
线
从
从
低
到
高
排
序
,
而
矩
形
的
左
右
边
从
小
到
大
排
序
(
3
)
考
虑
数
据
范
围
,
我
们
进
行
离
散
化
处
理
(
3
)
建
立
线
段
树
,
那
么
区
间
可
表
示
为
区
域
的
y
1
,
y
2
,
以
及
长
度
。
(
4
)
遍
历
所
有
相
邻
左
右
边
,
每
次
通
过
线
段
树
询
问
出
高
度
,
然
后
面
积
相
加
。
(1)每个矩形的上下边加入扫描线并标记,下边标记为1,上边标记为-1\\ (2)让扫描线从从低到高排序,而矩形的左右边从小到大排序\\ (3)考虑数据范围,我们进行离散化处理\\ (3)建立线段树,那么区间可表示为区域的y1,y2,以及长度。\\ (4)遍历所有相邻左右边,每次通过线段树询问出高度,然后面积相加。\\
(1)每个矩形的上下边加入扫描线并标记,下边标记为1,上边标记为−1(2)让扫描线从从低到高排序,而矩形的左右边从小到大排序(3)考虑数据范围,我们进行离散化处理(3)建立线段树,那么区间可表示为区域的y1,y2,以及长度。(4)遍历所有相邻左右边,每次通过线段树询问出高度,然后面积相加。
注意我们这里要维护的是线段长度而非点的值,所以我们要对线段树进行修改,即左孩子的右值=右孩子的左值,这样才能使维护不出现缝隙。
// luoguP5490 【模板】扫描线
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+7;
struct L{
ll x,y1,y2,flag;
L(ll X=0,ll Y1=0,ll Y2=0,ll T=0){
x=X,y1=Y1,y2=Y2,flag=T;
}
}line[N<<1];
struct node{
int l,r;
ll len;
ll lazy;
}t[N<<2];
int n,cnt;
ll seg[N];
map<ll,int>val;
ll len(int p){
//获得区域p的高度,即两扫描线相减
return seg[t[p].r+1]-seg[t[p].l];
}
void update(int p){
if(t[p].lazy) t[p].len=len(p); //如果该区间被标记过
else if(t[p].l==t[p].r) t[p].len=0; //区间长度为0
else t[p].len=t[p<<1].len+t[p<<1|1].len; //两区间相加
}
void build(int p,int l,int r){
t[p].l=l; //建树时只需记录区间左右就可以了
t[p].r=r;
if(l==r) return;
int mid=l+r>>1;
build(p<<1,l,mid); //对左边建树
build(p<<1|1,mid+1,r); //对右边建树
}
void change(int p,int l,int r,int k){
if(l<=t[p].l&&t[p].r<=r){ //如果区间在所求上下边中
t[p].lazy+=k; //打上标记
update(p); //更新区间长度
return;
}
int mid=t[p].l+t[p].r>>1;
if(l<=mid) change(p<<1,l,r,k); //向下更新
if(r>mid) change(p<<1|1,l,r,k); //向上更小
update(p); //最后要更新整个区间
}
bool cmp(L a,L b){
return a.x<b.x; //按照x坐标排序
}
signed main(){
ios::sync_with_stdio(0); //读入优化
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
ll x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
seg[++cnt]=y1; //加入扫描线
line[cnt]=L(x1,y1,y2,1); //加入竖线
seg[++cnt]=y2;
line[cnt]=L(x2,y1,y2,-1);
}
sort(line+1,line+cnt+1,cmp); //对竖线排序
sort(seg+1,seg+1+cnt); //扫描线从低到高排序
int m=unique(seg+1,seg+1+cnt)-(seg+1); //去重
for(int i=1;i<=m;i++) val[seg[i]]=i; //离散化
build(1,1,m); //建树
ll ans=0; //记录答案
for(int i=1;i<cnt;i++){ //对于cnt-1条竖边
int x=val[line[i].y1],y=val[line[i].y2]-1; //区域的上下边
change(1,x,y,line[i].flag); //查询并更新区域的高
ans+=t[1].len*(line[i+1].x-line[i].x); //累加区域面积
}
printf("%lld",ans); //输出答案
return 0;
}
区间最大子段和
GSS3 - Can you answer these queries III
#include<bits/stdc++.h>
//#define int long long
#define inf 0x7f7f7f7f
using namespace std;
const int N=1e5+7;
const int mod=1e9+7;
int n,m,idx=0,op,x,y;
#define MID int mid=l+r>>1;
struct Seg{
int ls,rs,sum,mx;
}tr[N<<2];
void pushup(int p){
tr[p].mx=max(tr[p<<1].mx,tr[p<<1|1].mx);
tr[p].mx=max(tr[p].mx,tr[p<<1].rs+tr[p<<1|1].ls);
tr[p].sum=tr[p<<1].sum+tr[p<<1|1].sum;
tr[p].ls=max(tr[p<<1].ls,tr[p<<1].sum+tr[p<<1|1].ls);
tr[p].rs=max(tr[p<<1|1].rs,tr[p<<1|1].sum+tr[p<<1].rs);
}
void build(int p,int l,int r){
if(l==r){
cin>>tr[p].mx;
tr[p].sum=tr[p].ls=tr[p].rs=tr[p].mx;
return;
}
MID;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
pushup(p);
}
void update(int p,int l,int r,int pos,int val){
if(l==r){
tr[p].ls=tr[p].rs=tr[p].mx=tr[p].sum=val;
return;
}
MID;
if(pos<=mid) update(p<<1,l,mid,pos,val);
else update(p<<1|1,mid+1,r,pos,val);
pushup(p);
}
Seg query(int p,int l,int r,int ql,int qr){ //查询最大字段和
if(ql<=l&&r<=qr) return tr[p];
MID;
if(qr<=mid) return query(p<<1,l,mid,ql,qr);
if(ql>mid) return query(p<<1|1,mid+1,r,ql,qr);
Seg res,L=query(p<<1,l,mid,ql,mid),R=query(p<<1|1,mid+1,r,mid+1,qr);
res.sum=L.sum+R.sum;
res.ls=max(L.ls,L.sum+R.ls);
res.rs=max(R.rs,R.sum+L.rs);
res.mx=max(L.mx,R.mx);
res.mx=max(res.mx,L.rs+R.ls);
return res;
}
signed main(){
scanf("%d",&n);
build(1,1,n);
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&op,&x,&y);
if(op){
if(x>y) x^=y^=x^=y;
printf("%d\n",query(1,1,n,x,y).mx);
}else{
update(1,1,n,x,y);
}
}
return 0;
}
动态开点
动态开点线段树可以避免离散化。
如果权值线段树的值域较大,离散化比较麻烦,可以用动态开点的技巧。
省略了建树的步骤,而是在具体操作中加入结点。
参考资料
https://oi-wiki.org/geometry/scanning/
https://mirasire.xyz/2019/11/17/SMX/
https://wmathor.com/index.php/archives/1176/
https://www.bilibili.com/video/BV1FU4y1t79g