线段树-懒标记详解

首先,我们可以想到用线段树来做这道题。不会告诉你其实我不会树状数组

数据太大就会超时,怎么办?这样就要引出我们的好帮手懒标记了!

一个支持区间修改的线段树, 就要用到lazy标记. 用到哪一个结点, 有效数据就更新到哪一个结点, 避免浪费更新那些不必要的结点的时间. 从根部向下找一个结点, 一路下来根据lazy的设定进行相关的更新操作, 就能快速完成任务.

下面就示范一组样例:

8 2

3 3 3 3 3 3 3 3

1 2 7 1

2 3

相信大家对线段树已经有了一定的了解(没了解过的看这里洛谷日报4)那我们就开始构造线段树,如图:
在这里插入图片描述

这就是基本的线段树了,然后开始进行第一步,从2到7,一次给区间加上1:

在这里插入图片描述

来解释这一幅图,首先看我们存线段树的下标1,L(左节点)=1,R(右节点)=8,没有被包含且懒标记=0,所以取中点mid=(1+8)/2=4,查找左右子树。

来到左子树2,L=L(father’s),R=mid(father’s),没有被完全包含且懒标记=0,所以继续查找左右子树。

来到2的左子树4,没有被包含且懒标记=0,所以查找左右子树,因为左子树的右节点大于2,所以不进入。来到右子树9,被包含,所以dis[9]+=1,懒标记+=1,返回。

返回到2,查找右子树5,被包含,所以dis[5]+=(4-3+1)* 1,懒标记+=1,返回。

回到1,查找右节点3……以此类推,dis[6]+=(6-5+1)* 1,懒标记+=1;dis[14]+=1,懒标记+=1;

这样,区间修改就完成了。

来到第二步,查找3:
在这里插入图片描述

从1开始找到2,在找到5,发现有懒标记,所以把懒标记下传到10和11, dis[10]+=1;懒标记[10]+=1;dis[11]+=1,懒标记[11]+=1,懒标记[5]=0; 到10,L=R=3,return dis[10];查询就完成了。

可能有人会说懒标记看起来没有什么用,但数据一旦很大,递归传递就显得尤为方便,可以为我们节省很多时间。

接下来是对代码的详细讲解:

初始化:

struct Tree{
    long long l,r;//l:左节点 r:右节点 
    long long dat,laze_tag;//dat:当前节点的值 laze_tag:懒标记,记录改变的值,递归传值 
}t[2000001];//四倍n 

然后是我们的懒标记传递:


inline void f(long long p,long long k){
    t[p].laze_tag+=k;//懒标记传递 
    t[p].dat+=k*(t[p].r-t[p].l+1);//当前值加上所有节点总数*值 
} 
inline void pushdown(long long p){
    f(p*2,t[p].laze_tag);
    f(p*2+1,t[p].laze_tag);
    //将懒标记的值传给下面的左右儿子节点
    t[p].laze_tag=0;//复原懒标记 
}

(下面会有再查找和修改时的懒标记处理)

开始先要构造我们的线段树

void js(int p,int l,int r){//建树 
    t[p].l=l;t[p].r=r;//记录左右节点 
    if(l==r){//到达底部返回值 
        t[p].dat=a[l];return;
    }
    long long mid=(l+r)/2;//中点 
    js(p*2,l,mid);
    js(p*2+1,mid+1,r);
    //递归初始化 
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
    //加上左右儿子节点 
}

第二步,就要写我们的区间加减了

void pushs(long long p,long long l,long long r,long long v){//区间加减 
    if(t[p].l>=l&&t[p].r<=r){//如果区间被包含就修改并打上懒标记 
        t[p].dat+=v*(t[p].r-t[p].l+1);//加上所有值 
        t[p].laze_tag+=v;//懒标记修改 
        return;
    }
    pushdown(p);//查询懒标记,因为下面要递归 
    long long mid=(t[p].r+t[p].l)/2;//取中点 
    if(l<=mid)pushs(p*2,l,r,v);//修改左边 
    if(r>mid) pushs(p*2+1,l,r,v);//修改右边 
    t[p].dat=t[p*2].dat+t[p*2+1].dat;//回溯时加上左右儿子节点的值 
}

最后是最容易的单点查询


long long outt(long long p,long long l){//单点查询 

    if(t[p].l==l&&t[p].r==l)return t[p].dat;//找到目标点就返回 
    pushdown(p);//先回复懒标记的值再传递,因为下面可能递归(要判断是否到了底部,就是这里出了问题QwQ)
    long long mid=(t[p].l+t[p].r)/2;//记录中点 
    if(l<=mid)return outt(p*2,l);//找左边 
    if(l>mid)return outt(p*2+1,l);//找右边 
}

看完了,那就贴上完整的代码啦!

#include<bits/stdc++.h>
using namespace std;
long long n,m;//n:长度 m: 询问 
long long a[500001];
struct Tree{
    long long l,r;//l:左节点 r:右节点 
    long long dat,laze_tag;
}t[2000001];
inline long long read(){
    long long f=1,outt=0;char a=getchar();
    while(a>'9'||a<'0'){if(a=='-')f=-1;a=getchar();}
    while(a<='9'&&a>='0'){outt*=10;outt+=a-'0';a=getchar();}
    return f*outt;
}//读入优化 
inline void f(long long p,long long k){
    t[p].laze_tag+=k;
    t[p].dat+=k*(t[p].r-t[p].l+1);
} 
inline void pushdown(long long p){
    f(p*2,t[p].laze_tag);
    f(p*2+1,t[p].laze_tag);
    t[p].laze_tag=0;
}
void js(int p,int l,int r){
    t[p].l=l;t[p].r=r;
    if(l==r){
        t[p].dat=a[l];return;
    }
    long long mid=(l+r)/2;//中点 
    js(p*2,l,mid);
    js(p*2+1,mid+1,r);
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
}
long long outt(long long p,long long l){
    if(t[p].l==l&&t[p].r==l)return t[p].dat;
    pushdown(p);
    long long mid=(t[p].l+t[p].r)/2;
    if(l<=mid)return outt(p*2,l);
    if(l>mid)return outt(p*2+1,l);
}
void pushs(long long p,long long l,long long r,long long v){
    if(t[p].l>=l&&t[p].r<=r){
        t[p].dat+=v*(t[p].r-t[p].l+1);
        t[p].laze_tag+=v;
        return;
    }
    pushdown(p);
    long long mid=(t[p].r+t[p].l)/2;
    if(l<=mid)pushs(p*2,l,r,v); 
    if(r>mid) pushs(p*2+1,l,r,v);
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
}
void change(long long p,int x,int v){//单点修改,不必在意,是区间修改的子问题,连标记都不用(而且本题不需要)
    if(t[p].l==t[p].r){
        t[p].dat+=v;return;
    }
    int mid=(t[p].r+t[p].l)/2;
    if(x<=mid)change(p*2,x,v);
    else change(p*2+1,x,v);
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
}
int main(){
    n=read();m=read();//读入 
    for(int i=1;i<=n;i++)
        a[i]=read();
    js(1,1,n);//建树 
    for(int i=1;i<=m;i++){
        long long pd=read();
        if(pd==2){
            long long ll=read();
            printf("%lld\n",outt(1,ll));//查询ll的值 
        }
        else 
        if(pd==1){
            long long ll=read(),rr=read(),x=read();
            pushs(1,ll,rr,x);//修改从ll到rr的值加上x 
        }
        else
        if(pd==3){
            int k=read(),y=read();
            change(1,k,y);
        }
    }
    return 0;//华丽丽的结束,可以A掉树状数组2了!!! 
}
  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
廊桥分配问题是指给定一座长度为n的廊桥以及m个人,每个人需要跨过廊桥到对面。廊桥每次只能让两个人同时通过,且只有两个人的速度加和不超过廊桥长度时才能通过。每个人过桥所需的时间不同,要求找到一种过桥方案使得所有人的总过桥时间最短。 该问题可以通过使用线段树的解法。首先,将n个位置看作是一棵树,每个节点对应一个位置。然后,我们将所有人按照过桥时间从小到大排序,并按照排序结果为每个节点分配一个排序编号。接下来,从左到右遍历排序后的人员列表,对于每个人,我们找到其对应的节点,并为该节点分配一个值,表示该位置可以被占用。 这样,在分配完所有人的节点后,我们得到了一个线段树,每个非叶子节点表示一个廊桥位置,叶子节点表示一个人,其父节点的值表示桥上人员的速度加和。通过遍历这颗树,可以计算出所有人过桥的最短总时间。 具体操作如下: 1. 根据所有人的过桥时间从小到大排序。 2. 为每个节点分配排序编号。 3. 初始化线段树的所有节点为空(未占用)。 4. 从左到右遍历排序后的人员列表,对于每个人: a. 找到对应的节点。 b. 判断该节点是否为空,如果为空,表示该位置可以被占用,否则找到该节点的兄弟节点(该节点的父节点的其他子节点)。 c. 将该节点或其兄弟节点标记为占用,并更新父节点的值。 5. 遍历线段树,计算所有人过桥的总时间。 使用线段树解决廊桥分配问题的时间复杂度为O(nlogn),因为排序的时间复杂度为O(nlogn),遍历人员列表的时间复杂度为O(n),遍历线段树的时间复杂度为O(nlogn)。总的空间复杂度为O(n)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值