线段树(超简单)

原文链接:https://www.cnblogs.com/jason2003/p/9676729.html

参考的这位大佬的文章,不过加了一点自己的理解。有些地方存在问题望指正。

线段树(RMQ问题)

线段树是一种二叉树,本质上是我们用一个二叉树来储存一个线段,树的每个节点都记录的是一段区间的长度。

举个例子

(图片来源于网络,侵删)

如图我们就建立起来一个二叉树每个节点用来储存区间的长度。解释一下,就是根节点储存的的是1——4的和,他的左儿子存放的是1——2的和,右儿子存放的是3——4的和。由上述介绍可知,一个节点的值等于他的左儿子加上右儿子的和。

学习了线段树是什么就要开始了解如何建立一个线段树。

(1)建立线段树

首先我们应该有一个结构体tree用来存放树,里面有tree_l,tree_r代表树的左边界与右边界,还要用一个sum记录该节点所存放的值。建立线段树依靠的就是tree[i].sum=tree[2 * i].sum+tree[2 * i+1]。然后我们就可以依靠递归的方式进行建树;

现在展示建立线段树的代码;

void build(int i;int l;int r){//递归的方式进行建树
    tree[i].l=l;tree[i].r=r;//确定节点的左右边界
    if(l==r){//判断是否是叶子节点;
        tree[i].sum=input[l];//input数组就是你输入的那个数组;、
        return;
    }
    int mid=(l+r)/2;//根据描述就可以知左右儿子得值是从中心点分开的
    bulid(2*i,l,mid);//构建左子树
    build(2*i+1,mid+1,r);//构建右子树
    tree[i].sum=tree[2*i].sum+tree[2*i+1].sum;//层层递归得到每个节点的值
}

注意:因为线段树是使用了数组去存放值,所需的数据量很大,所以tree的数组大小一般我们要开到input的四倍;

建立完线段树我们就要开始去使用它了。

(2)单点修改和区间查询

区间查询:

还是以刚才的图为例,例如我们要查询1——3区间的和是我们先将其与根节点对比,发现没有将其全部覆盖,但是发现他和根节点的左右儿子都有交集,所以我们先跑到左儿子哪里,发现左儿子被1——3这个区间全部包含所以我们直接返回该节点的值3,然后我们在跑到右儿子哪里,发现没有将右儿子完全覆盖,但是却和他的左儿子有交集,跑到哪里发现可以全覆盖就在返回他的值3;由此我们就得到了1——3的值;

总结一下区间查询的操作

1、当线段树上的一个区间被我们所求的区间完全覆盖,我们就返回该区间的值

2、当区间不能被完全覆盖时,我们就去找他的左右儿子看看那个与所求区间有交集,就跳到那个区间重复操作

代码实现:

int seach(int i;int l;int r){//递归的方式查询l和r就是我所求得区间
    if(tree[i].r<=r&&tree[i].l>=l){//判断该区间是否被完全覆盖
        return tree[i].sum;
    }
    if(tree[2*i].r>=l){
        s+=seach(2*i,l,r);//判断与左儿子是否有交点
    }
    if(tree[2*r+1].l<=r){
        s+=seach(2*i+1,l,r);//判断与右儿子是否有交点
    }
    return s;//s记录的就是区间的值
}   

单点修改

单点修改比较简单,就是要修改一个点的话就从根节点开始,判断这个点是在他的左儿子还是右儿子。然后就跳到那个区间。递归这去找知道找到了那个点之后就返回,对所有经过的点都进行修改就完事儿。参考图像。

代码如下:

void add(int i,int dis,int k){//dis是要修改的点,k是要增加值
    if(tree[i].l==tree[i].r){//判断是否是叶子节点
        tree[i].sum+=k;
        return ;
    }
    if(dis<=tree[2*i].r)add(2*i,dis,k);//判断要是修改的点在那个区间,在那个就跳转到那个
    else
        add(2*i+1,dis,k);
    tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;
}

(3)区间修改与单点查询

区间修改与单点查询的操作其实与之前的操作差不多,但是我们会发现一个问题 区间修改的目的是为了之后的查询,但是在具体的题中我们有的时候某些曾经修改过的区间我们使用不到的,或者是有些线段树上有些区间是用不到的。但是我们在进行区间修改的时候还是会将其修改,这样就会让我们无用的时间复杂度增加。所以为了避免这个问题,我们引入了一个新的知识“懒标记”

(4)懒标记

懒标记顾名思义就是他很懒,只有我们推他他才走。线段树为什么这莫牛逼,就是因为存在这个懒标记。

接下来我们使用懒标记来对区间修改和单点查询进行操作

区间修改:我们遵循以下原则

1、如果当前区间被完全覆盖在目标区间里,讲这个区间的sum+k*(tree[i].r-tree[i].l+1)

2、如果没有完全覆盖,则先下传懒标记

3、如果这个区间的左儿子和目标区间有交集,那么搜索左儿子

4、如果这个区间的右儿子和目标区间有交集,那么搜索右儿子

演示代码如下:

void add(int i,int l,int r,int k){
    if(tree[i].l>=l&&tree[i].r<=r){
        tree[i].sum+=k*(tree[i].r-tree[i].l+1);
        tree[i].lz+=k;
    }
    push_down(i);
    if(tree[2*i].r>=l){
        add( 2*i, l, r, k);
    }
    if(tree[2*i+1].l<=r){
        add(2*i+1,l,r,k);
    }
    tree[i].sum=tree[2*i].sum+tree[2*i+1].sum;
    return;
}

其中的push_down的·作用就是将父亲的lz归零,然后将lz传递给左右儿子。线段树的精髓就是这个所以很重要。

演示代码如下:

void push_down(int i){
    if(tree[i].lz!=0){
        tree[2*i].sum+=tree[i].lz*(tree[2*i].r-tree[2*i].l+1);
        tree[2*i].lz+=k;
        tree[2*i+1].sum+=tree[i].lz*(tree[2*i+1].r-tree[2*i+1].l+1);
        tree[2*i+1].lz+=k;
        tree[i].k=0;
    }
    return ;
}

具体解释一下,当进行区间修改的时候我们没必要将所有的tree节点都进行修改,比如我们修改1——3区间时,由于1——2区间以及3——3区间就已经可以将1——3区间全部代替那我们就没有必要将1——1区间和2——2区间的值进行修改,但是未来我们很有可能能回被要求查询到1——1和2——2区间的值,所以我们就用lz标记去记录我们曾经所要进行的操作。等我们要进行操作的时候在将其加上去。这样就可以节约一点时间。

由于单点查询过于简单我就直接展示有了lz标记的区间查询的代码吧

代码展示:

int seach(int i;int l;int r){
    if(tree[i].l>=l&&tree[i].r<=r){
        return tree[i].sum;
    }
    push_down(i);
    if(tree[2*i].r>=l){
        s+=seach(2*i,l,r);
    }
    if(tree[2*i+1].l<=r){
        s+=seach(2*i+1,l,r);
    }
    return s;
}

线段树就先学到这吧,等以后有心情了在学!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值