ACM学习笔记:线段树

Posted LINNO

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ACM学习笔记:线段树相关的知识,希望对你有一定的参考价值。

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]=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)建立线段树,那么区间可表示为区域的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;
}

 

动态开点

动态开点线段树可以避免离散化。

如果权值线段树的值域较大,离散化比较麻烦,可以用动态开点的技巧。

省略了建树的步骤,而是在具体操作中加入结点。

 

参考资料

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

以上是关于ACM学习笔记:线段树的主要内容,如果未能解决你的问题,请参考以下文章

线段树学习笔记

关于线段树的一些学习笔记——(无限施工中)

线段树学习笔记

[学习笔记]主席树

线段树 学习笔记

主席树学习笔记