线段树入门(加法、根号、乘法)

线段树简介

线段树,一种特殊的二叉树——二叉搜索树。它将一段区间划分为若干单位区间,每一个节点都储存着一个区间。它功能强大,支持区间求和,区间最大值,区间修改,单点修改等操作

线段树的每一个节点都储存着一段区间[L,R]的信息,其中叶子节点L=R。它的大致思想是:将一段大区间平均地划分成2个小区间,每一个小区间都再平均分成2个更小区间……以此类推,直到每一个区间的L等于R(这样这个区间仅包含一个节点的信息,无法被划分)。通过对这些区间进行修改、查询,来实现对大区间的修改、查询。每一次修改、查询的时间复杂度都只为O(logn)

在这里插入图片描述

建树

存储方式——结构体数组。由于数组较为简便,因此对于大小为N的区间,存在数组a[N]中,我们只需开4*N大小的数组即可。理论上N的大小不超过1e5

通常线段树上的节点储存的主要是:区间左右边界,区间的和,懒惰标记

typedef long long ll;
const int N=1e5+10;
ll a[N];
struct tree{
    ll l,r;    //区间左右边界
    ll sum;  //区间的和
    int lazy;  //懒惰标记
    tree(){ l=r=sum=lazy=0; }
}sgt[N<<2];

和二叉树的数组表示形式一样,对于编号为k的区间元素,它的左孩子的编号应该为2*k,右孩子是2*k+1,使用位运算优化,就是k << 1和K << 1 | 1(无需加括号),而且我们可以知道父亲节点的sum等于孩子节点的sum之和

建树函数

熟悉了二叉树的递归,相信下面的递归建树很好理解:

void build(int i,int l,int r){//递归建树,i首次传入1
    sgt[i].l=l;
    sgt[i].r=r;
    if(l==r){  //如果这个节点是叶子节点
        sgt[i].sum=a[l];
        return;
    }
    int mid=(l+r)>>1;
    int k=i<<1;
    build(k,l,mid);//分别构造左子树和右子树
    build(k|1,mid+1,r);
    sgt[i].sum=sgt[k].sum+sgt[k|1].sum;
}
修改操作

单点修改

单点修改就是只修改叶子节点,下面代码示例为修改第x个叶子节点为y:

void change(int i,int x,int y){  //i默认从1开始
	if(sgt[i].l==sgt[i].r){ //当前区间只包含一个元素,该元素就是我们要修改的元素,直接修改即可
	    sgt[i].sum=y;
	    return;
	}
	int mid=(sgt[i].l+sgt[i].r)>>1;
	int k=i<<1;
	if(x<=mid) change(k,x,y); //递归到左孩子
	else change(k|1,x,y); //递归到右孩子
	sgt[i].sum=sgt[k].sum+sgt[k|1].sum;
}

区间修改

由前面线段树的定义我们可以知道,我们在修改一个区间时,只需找出恰好完全包含的左右边界并修改该节点的sum值递归向上修改即可。当该子区间恰好在当前总区间中点左边或者右边时,这个子区间刚好在这棵线段树上。然而当子区间包含中点时,我们只需要将该区间按中点分为两半即可,然后分别向中点的左右两边去找。

在这里插入图片描述

如果我们不但修改区间的值,还想着把下面的子节点和叶子节点的sum值全部修改,显然这样还不如线性修改。因此线段树要想省时,就必须只恰好修改包含该区间的子节点及以上的sum值。那么问题又来了,我们查询时如果查到和修改过的区间有交集的区间,显然交集部分的值并没有改变,那怎么办呢?

在这里插入图片描述

这时应该引入一个线段树至关重要的名词——懒惰标记

懒惰标记的含义:本区间已经被更新过了,但是子区间没有被更新,被更新的信息就需要保存在该区间的节点上,当查询时,该标记的作用就体现出来了

有了上面引入的懒惰标记,我们可能还有疑惑,这怎么帮助我们查询?只挂在该区间节点,查询时子节点仍然没有被修改。不要慌,我们引入一个重要的函数——标记下传。

所谓标记下传,就是当我们需要查询到上面提到有交集的被修改的小区间时,把含有该节点的标记一直下传,即修改下面的子节点一直到叶子节点,这样就把我们需要查询的小区间往下更新,而其他没有被修改的区间的标记,就让它挂在那里就行了

我们也许会注意到,为什么区间修改里还有一个pushdown,这个问题我查了很多地方都没清楚的解释(也许还是我太菜总是有一些大家没有的问题),终于我在寒假(2020开年的躺尸假期)想出了答案:

在这里插入图片描述

第一次我们给区间[1,5]打上标记x,这时[1,5]区间保存的sum值是15+x。第二次我们给区间[1,3]打上标记y,这时[1,3]和[1,5]的sum都更新了,其中[1,5]的sum是15+y。但是容易忘记的是,我们第一次更新时已经要求[1,5]区间的都加上x,因此[1,5]区间的正确答案应该是15+x+y。因为我们没有把标记x下传,所以我们的父区间都是由下面两个亲的子区间求和不断更新的。这里我们就要明确下传标记的作用了,就是当前区间比我需要的目标区间大的时候,我必须用到下面的值了,那么就不能再懒惰下去了,必须往下修改了,这时候,我们就把之前堆积起来的懒惰标记全都pushdown,下面区间查询时也是同理

易错提醒:在区间修改时找目的区间时,寻找过程中目的区间的父区间必须pushdown

void pushdown(int i){ //将i点的懒惰标记下传
    if(sgt[i].lazy){
        int k=i<<1;
        sgt[k].sum+=sgt[i].lazy*(sgt[k].r-sgt[k].l+1);
        sgt[k|1].sum+=sgt[i].lazy*(sgt[k|1].r-sgt[k|1].l+1);
        sgt[k].lazy+=sgt[i].lazy;
        sgt[k|1].lazy+=sgt[i].lazy;
        sgt[i].lazy=0;
    }
    return;
}
void add(int i,int l,int r,int x){ //当前为i节点,把(l,r)区间内的数都加上x
    if(sgt[i].l==l&&sgt[i].r==r){  //如果当前区间已经找到,将这个区间的sum加上区间长度乘以x
        sgt[i].sum+=x*(sgt[i].r-sgt[i].l+1);
        sgt[i].lazy+=x;//记录lazy
        return;
    }
    pushdown(i);  //向下传递,重要理解
    int mid=(sgt[i].l+sgt[i].r)>>1;
    int k=i<<1;
	if(r<=mid) add(k,l,r,x);  //如果被修改区间完全在左区间
	else if(l>mid) add(k|1,l,r,x);  //如果被修改区间完全在右区间
	else add(k,l,mid,x),add(k|1,mid+1,r,x);  //如果都不在,就要把修改区间分解成两块,分别往左右区间递归
    sgt[i].sum=sgt[k].sum+sgt[k|1].sum;
    return;
}
查询操作

区间查询

有了上面的懒惰标记和标记下传,我们在查询时,如果到达的节点有标记就下传,千万不要忘记!当查找区间在当前区间的左子区间时,递归到左子区间;当查找区间在当前区间的右子区间时,递归到右子区间;否则,我们就把它分成两块,分在两个子区间查询。最后递归求和即可

ll query(int i,int l,int r){
	if(sgt[i].l==l&&sgt[i].r==r) 
	    return sgt[i].sum;
	pushdown(i);  //如果当前节点被打上了懒惰标记,那么就把这个标记下传
	int mid=(sgt[i].l+sgt[i].r)>·>1;
	int k=i<<1;
	if(r<=mid) return query(k,l,r);
	else if(l>mid) return query(k|1,l,r);
	else return query(k,l,mid)+query(k|1,mid+1,r);
}

单点查询

单点查询和单点修改一样,就是从根节点开始找,类似二分查找,直到查找到叶子节点

void Search(int i,int x){
	if(sgt[i].l==sgt[i].r){ //当前区间只包含一个元素,该元素就是我们要修改的元素,直接修改即可
	    return sgt[i].sum;   
	}
	int mid=(sgt[i].l+sgt[i].r)>>1;
	int k=i<<1;
	if(x<=mid) change(k,x,y); //递归到左孩子
	else change(k|1,x,y); //递归到右孩子
}
根号线段树

所谓根号线段树,就是把整个区间的数都开根号。我们知道开根号不同于加减,根号a加根号b不等于根号下a+b,因此我们必须访问到修改区间的每一个叶子节点修改。但是我们容易知道根号运算使数衰减的很快,即使是long long的最大值连续开根号10次以内也能到1(这里说的是使用sqrt()函数)。因此某段区间如果达到1就直接return,否则就直接递归直到叶子节点更新sum的值,所以我们就不需要pushdown了

void update(int i,int l,int r){ //当前为i节点,把(l,r)区间内的数开根号
    if(sgt[i].l==l&&sgt[i].r==r){ //如果当前区间都被开根号到1
        if(sgt[i].sum==sgt[i].len) return;
    }
    if(sgt[i].l==sgt[i].r){
        sgt[i].sum=(ll)sqrt(1.0*sgt[i].sum);
        return;
    }
    int mid=(sgt[i].l+sgt[i].r)>>1;
    int k=i<<1;
	if(r<=mid) update(k,l,r);
	else if(l>mid) update(k|1,l,r); 
	else update(k,l,mid),update(k|1,mid+1,r);
    sgt[i].sum=sgt[k].sum+sgt[k|1].sum;
}
乘法线段树

如果只是区间乘以一个数,那么类似于区间加法,只需要稍微更新mutiply函数和pushdown函数即可,此外lazy标记更新时要初始化为1:

typedef long long ll;
const int N=1e5+10;
ll a[N];
struct tree{
    ll l,r;  
    ll sum; 
    int lazy; 
    tree(){ l=r=sum=0,lazy=1; }
}sgt[N<<2];

void pushdown(int i){
	int k=i<<1;
	sgt[k].sum*=sgt[i].lazy;
	sgt[k|1].sum*=sgt[i].lazy;
	sgt[k].lazy*=sgt[i].lazy;
	sgt[k|1].lazy*=sgt[i].lazy;
	sgt[i].lazy=1;
	return ;
}

void mutiply(int i,int l,int r,int x){
    if(sgt[i].l==l&&sgt[i].r==r){
        sgt[i].sum*=x;
        sgt[i].lazy*=x;
        return;
    }
    pushdown(i);
    int mid=(sgt[i].l+sgt[i].r)>>1;
    int k=i<<1;
	if(r<=mid) mutiply(k,l,r,x);
	else if(l>mid) mutiply(k|1,l,r,x);
	else mutiply(k,l,mid,x),mutiply(k|1,mid+1,r,x);
    sgt[i].sum=sgt[k].sum+sgt[k|1].sum;
    return;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值