2021-07-27 重见线段树

###基础操作

引入:
线段树是一种神奇的数据结构,支持在线高效率(lgn)区间/单点修改/查询。下面用一个经典例题引入线段树基本模版。
————————————————————————————————
有一个长度为n(n<=1e5)的数组,有m(m<=1e5)次操作,操作涉及修改数组中某个元素的值以及查询数组连续区间内的和。
————————————————————————————————
这个题如果把数据量缩小,就是一个简单暴力题,但是数据量上来了,就需要用到线段树了。

随便揪一张图片:
在这里插入图片描述
这个图可以很好地展示线段树为什么可以高效率查询区间信息。

一、基础建树

const int maxn=1e5+7;

int a[maxn];

struct node{
    int l,r;
    int sum;
}tr[maxn*4];

void build(int u,int l,int r){
    tr[u].l=l;
    tr[u].r=r;
    if(l==r){
        //叶节点
        tr[u].sum=a[l];
        return;
    }
    int mid=(l+r)>>1;
    build(u<<1,l,mid);
    build(u<<1|1,mid+1,r);
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
int main(){
    build(1,1,n);//主函数中调用建树函数时,传入参数依次为:根节点,左区间,右区间
}

二、单点修改,区间查询

修改:

void modify(int u,int x,int d){
    //把编号为x的节点加上d,也是从更节点开始向下寻找
    if(tr[u].l==tr[u].r&&tr[u].l==x){
        tr[u].sum+=d;
        return;
    }
    int mid=(tr[u].l+tr[u].r)>>1;
    if(x<=mid){
        modify(u<<1,x,d);
    }else{
        modify(u<<1|1,x,d);
    }
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//pushup,由更新了的子节点更新父节点
}

查询:

int query(int u,int l,int r){
    //从根节点开始,向下寻找符合条件的线段树节点
    if(tr[u].l>=l&&tr[u].r<=r){
        //如果节点区间包含在查询区间内
        return tr[u].sum;
    }
    else if(tr[u].l>r||tr[u].r<l){
        //如果节点区间与查询没有交集
        return 0;
    }
    else{
        //如果查询区间与节点有交集
        int s=0;
        s+=query(u<<1,l,r);
        s+=query(u<<1|1,l,r);
        return s;
    }
}

运用以上的三个模版,就可以轻松解决「引入」中的问题啦!
但是线段树的应用方法远不止于此,接下来继续介绍其它线段树模版。

再引入一个题:
——————————————————————————————————————
P3372 【模板】线段树 1
在这里插入图片描述

——————————————————————————————————————
如果对区间中每个点都做区间单点修改,那么复杂度甚至比暴力模拟还要高。那怎么办?这就要用到线段树另外一种操作——延迟修改技术(lazytag)。
基本原理就是,只要不需要查询带tag的子区间,这个tag就不会下传更新,这样可以大大节省时间。

三、区间修改(lazytag)

修改:

void pushdown(ll u){
    if(tag[u]!=0){
        //更新子节点信息
        tr[u<<1].sum+=(tr[u<<1].r-tr[u<<1].l+1)*tag[u];
        tr[u<<1|1].sum+=(tr[u<<1|1].r-tr[u<<1|1].l+1)*tag[u];
        //下传懒标记
        tag[u<<1]+=tag[u];
        tag[u<<1|1]+=tag[u];
        tag[u]=0;//父节点懒标记归0
    }
}

void modify(ll u,ll l,ll r,ll k){
    //把[l,r]区间内元素加上k
    if(tr[u].l>=l&&tr[u].r<=r){
        tag[u]+=k;
        tr[u].sum+=k*(tr[u].r-tr[u].l+1);
        return;
    }
    if(tr[u].l>r||tr[u].r<l){
        return;
    }
    pushdown(u);//要先把父节点原有到懒标记下传
    modify(u<<1,l,r,k);
    modify(u<<1|1,l,r,k);
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//因为到懒标记确定位置,节点信息才会被更新,所以需要依此更新父节点
}

查询:

ll query(ll u,ll l,ll r){
    ll sum=0;
    if(l<=tr[u].l&&r>=tr[u].r){
        sum+=tr[u].sum;
        return sum;
    }
    if(l>tr[u].r||r<tr[u].l){
        return 0;
    }
    else{
        pushdown(u);//下传懒标记
        sum+=query(u<<1,l,r);
        sum+=query(u<<1|1,l,r);
    }
    return sum;
}

以上就是线段树基础操作啦。
往下的内容将记录线段树相对进阶的应用。
(但是你真的以为线段树基础操作只局限于这一点点东西吗???(笑
对于上述内容的一些补充:线段树常用技巧模版(刷题篇)

###进阶应用

一、扫描线

P5490 【模板】扫描线

扫描线的思想与与微积分类似,都是将不规则的图形转化成规则的图形来求面积。
比如这个例题,要求出所有矩形面积的并,那么就是要求一个不规则图形的面积,如图所示:
在这里插入图片描述
那么可以通过每个矩形的边界,将图形分割成若干块,每块都是一个规则的矩形。

这样一来,只需要查询对于每个X节点,对应y覆盖的长度是多少。

于是就引出线段树啦!

(差不多也就是个线段树区间修改)

记录每个X对应的{y1,y2}(y轴上下界)

从左往右扫过去,遇到的X如果对应某个矩形左边的边,那么说明从这个位置开始,之后的一段内[y1,y2]都是被覆盖的,tag=1,如果X对应的右边的边,tag=-1。

因为本题y的范围很大,所以需要进行离散化,所以线段树操作会有一些细节跟普通的有点区别。


//线段树扫描线
//毒瘤题目,还要离散化。。
#define int ll//必须开ll,一开始写了很多的int所以这次骚一波了
const int maxn=1e5+7;

struct seg{
    int x;
    int y1,y2;
    int k;
    bool operator< (const seg &t)const
    {
        return x<t.x;
    }
}sg[maxn<<1];//记录线段

//离散化模块
vector<int> y;
int find(int yy){
    return (int)(lower_bound(y.begin(),y.end(),yy)-y.begin());
}

//线段树模块
struct node{
    int cnt;//记录这个区间被整体覆盖的次数
    int len;//记录这个区间被标记长度
    int l,r;
}tr[maxn<<2];

void pushup(int u){
    if(tr[u].cnt){
        tr[u].len=y[tr[u].r+1]-y[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){
    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);
}

void modify(int u,int l,int r,int k){
    if(l<=tr[u].l&&r>=tr[u].r){
        tr[u].cnt+=k;
        pushup(u);
        //必须及时pushup,因为懒标记不会下传
        return;
    }
    if(l>tr[u].r||r<tr[u].l){
        return;
    }
    else{
        modify(u<<1,l,r,k);
        modify(u<<1|1,l,r,k);
        pushup(u);
    }
}

signed main(){
    int n;
    cin>>n;
    int cnt=0;
    for(int i=1;i<=n;i++){
        int x1,x2,y1,y2;
        cin>>x1>>y1>>x2>>y2;
        sg[++cnt]={x1,y1,y2,1};
        sg[++cnt]={x2,y1,y2,-1};
        y.pb(y1);
        y.pb(y2);
    }
    
    sort(y.begin(),y.end());
    y.erase(unique(y.begin(),y.end()),y.end());
    
    build(1,0,(int)y.size()-2);
    
    sort(sg+1,sg+cnt+1);
    
    ll res=0;
    for(int i=1;i<=cnt;i++){
        if(i>1){
            res+=tr[1].len*(sg[i].x-sg[i-1].x);
        }
        modify(1,find(sg[i].y1),find(sg[i].y2)-1,sg[i].k);
    }
    cout<<res<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KaaaterinaX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值