线段树数据结构详解

线段树数据结构详解

Coded by Jelly_Goat.
All rights reserved.

这一部分是线段树。
线段树,顾名思义,是一种树形数据结构,适用于各种求区间统一算法的动静两平衡的数据结构。
这里什么是统一算法?(自己口胡的统一算法)
比如求最大值or最小值、区间求和,一样的区间都是一样的算法,这也是和动态dp不同的地方。


前置知识1:二叉搜索树

二叉搜索树就是根节点比左儿子大,比右儿子小的一种二叉树。

前置知识2:向量存储

向量存储是用来存完全二叉树儿子和父亲关系的。
如果不满足,我们还可以用链式前向星存
举个例子:
有一颗完全二叉树,节点数是16,然后你会发现:lson标号=root标号*2,rson标号=root标号*2+1
显然可见不是偶然,是二叉树满了导致的。
那么我们可以用下标表示存储的线段树节点。
例如:
tree[100]就是tree[200]tree[201]的root。



今天只讨论最普通的线段树(板子:求和)

操作1:建树

怎样种一棵线段树?Jelly_Goat:需要一条线段

  1. 没问题,真的需要原序列。
  2. 从上往下二分区间长度,递归建树。

代码示范:

//维护根节点的和
inline void update(int rt)
{
    tree[rt].sum=tree[rt*2].sum+tree[rt*2+1].sum;
}
//建树过程
//递归建议不要加inline
//根节点标号,左端点,右端点
void build_tree(int rt,int l,int r)
{
    //为tree复制左右端点
    tree[rt].l=l,tree[rt].r=r;
    if (l==r)
    {
        //如果已经是一个点,就输入数据sum
        scanf("%d",&tree[rt].sum);
        //一个暂时性标记
        tree[rt].tag=0;
        //返回
        return;
    }
    int mid=(r+l)/2;//是中间节点
    build_tree(rt*2,l,mid);//二分区间
    build_tree(rt*2+1,mid+1,r);
    update(rt);//加和
}

树高是logn的,
因此一次建树操作是\(O(n\cdot logn)\)的。

操作2:查询单点、修改单点

充分利用线段树是二叉搜索树的特点。
此话怎讲?
我们可以将点和线段中点比较啊qwq
if 在左半边 搜索半边
else 右半边同理
找到了就返回sum值即可。
修改完了以后可以进行一个update维护线段树的值。
代码示范:

//根节点,点的位置,此点加上num
void change_p(int rt, int p, lli num)
{
    //即现在是一个点,即我们要找的p点
    if (tree[rt].l == tree[rt].r)
    {
        //修改
        tree[rt].sum += num;
        //返回
        return;
    }
    //线段中点
    int mid = (tree[rt].l + tree[rt].r) >> 1;
    if (tree[rt].tag)//如果有缓存,清理一下(待会说这个是怎么回事
        pushdown(rt);
    if (p <= mid)//左半边
        change_p(rt << 1, p, num);
    else//右半边
        change_p((rt << 1) + 1, p, num);
    update(rt);//更新和
}
//根节点标号,点
lli ask_p(int rt, int p)
{
    //同修改的道理,这里就不加注释了
    if (tree[rt].l == tree[rt].r)
    {
        return tree[rt].sum;
    }
    if (tree[rt].tag)
        pushdown(rt);
    int mid = (tree[rt].l + tree[rt].r) >> 1;
    if (p <= mid)
        return ask_p(rt << 1, p);
    else
        return ask_p((rt << 1) + 1, p);
}

因为树高是logn的,所以每一次最多搜到logn次深度。
所以复杂度是\(O(logn)\)的。

操作三:区间修改、区间查询

一开始我们可以暴力一点,将区间拆成一个个点。
但是区间一长了,这个操作就炸了,相当于重新建了一棵树...
所以这里涉及到一个问题:线段树,怎样发挥线段的作用?
是的,整体操作
我们加一个缓存tag,属于lazy算法。
我们每一次匹配到一个线段,都给其进行一个缓存操作而不是向下传递更改,直到这个节点被用到。
被用到,意味着被查看、修改。
这样我们将最坏的时间复杂度降到了\(O(logn)\)级别的,因为最坏情况就是半边覆盖加上一个点进行修改。
代码示范:

//根节点标号,左端点,右端点,加上num
void change_seg(int rt, int l, int r, lli num)
{
    //如果区间完全覆盖,则进行缓存
    if (tree[rt].l == l && tree[rt].r == r)
    {
        tree[rt].tag += num;
        //加上缓存
        tree[rt].sum += (tree[rt].r - tree[rt].l + 1) * num;
        //整体的和即加上区间长度*num
        return;
    }
    if (tree[rt].tag)//有缓存就清空
        pushdown(rt);
    int mid = (tree[rt].l + tree[rt].r) >> 1;//中点
    if (r <= mid)//完全都在左半边
        change_seg(rt << 1, l, r, num);
    else if (l > mid)//完全都在右半边
        change_seg((rt << 1) + 1, l, r, num);
    else//两边都有
    {
        change_seg(rt << 1, l, mid, num);
        change_seg((rt << 1) + 1, mid + 1, r, num);
    }
    update(rt);//更新和
}
//根节点标号,左端点,右端点
lli ask_seg(int rt, int l, int r)
{
    //类似查询不再赘述
    if (tree[rt].l == l && tree[rt].r == r)
    {
        return tree[rt].sum;
    }
    if (tree[rt].tag)
        pushdown(rt);
    int mid = (tree[rt].l + tree[rt].r) >> 1;
    if (r <= mid)
        return ask_seg(rt << 1, l, r);
    else if (l > mid)
        return ask_seg((rt << 1) + 1, l, r);
    else
        return ask_seg(rt << 1, l, mid) + ask_seg((rt << 1) + 1, mid + 1, r);
}

操作4:清除缓存

那当然\(O(1)\)处理这个问题。
直接上代码,自己去理解。

inline void pushdown(int rt)
{
    int lson = rt << 1, rson = lson + 1;
    tree[lson].tag += tree[rt].tag;
    tree[rson].tag += tree[rt].tag;
    tree[lson].sum += (tree[lson].r - tree[lson].l + 1) * tree[rt].tag;
    tree[rson].sum += (tree[rson].r - tree[rson].l + 1) * tree[rt].tag;
    tree[rt].tag = 0;
}


完成。
完整的代码在GitHub开源:transport

原文地址:https://www.cnblogs.com/jelly123/p/10743601.html

时间: 2024-10-01 06:30:27

线段树数据结构详解的相关文章

线段树 入门详解

概念(copy度娘): 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN).而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩. 通俗地讲: 线段树就是把一个线段转变为一个二叉树,如下所示: 一个线段长度为4,则把它变成线段树,就是这个样子 这样如果你想改变一个区间的值,只用O(logn).比一般算法快了许多. 但是空间复杂度就会高一些,比如本来

JQuery Easy Ui (Tree树)详解(转)

JQuery Easy Ui (Tree树)详解(转) 第一讲:JQuery Easy Ui到底是什么呢? 首先咱们知道JQuery是对Java Script的封装,是一个js库,主要提供的功能是选择器,属性修改和事件绑定等等.. JQuery ui是在jQuery的基础上,利用jQuery的扩展性,设计的插件. 那么JQuery Easy Ui到底是什么呢? 我的理解就是比JQuery ui更强大,同样都是实现绚丽的多功能效果! jQuery Easy UI的目的就是帮助Web开发者更轻松的打

Python中的高级数据结构详解

这篇文章主要介绍了Python中的高级数据结构详解,本文讲解了Collection.Array.Heapq.Bisect.Weakref.Copy以及Pprint这些数据结构的用法,需要的朋友可以参考下 数据结构 数据结构的概念很好理解,就是用来将数据组织在一起的结构.换句话说,数据结构是用来存储一系列关联数据的东西.在Python中有四种内建的数据结构,分别是List.Tuple.Dictionary以及Set.大部分的应用程序不需要其他类型的数据结构,但若是真需要也有很多高级数据结构可供选择

[转]Redis内部数据结构详解-sds

本文是<Redis内部数据结构详解>系列的第二篇,讲述Redis中使用最多的一个基础数据结构:sds. 不管在哪门编程语言当中,字符串都几乎是使用最多的数据结构.sds正是在Redis中被广泛使用的字符串结构,它的全称是Simple Dynamic String.与其它语言环境中出现的字符串相比,它具有如下显著的特点: 可动态扩展内存.sds表示的字符串其内容可以修改,也可以追加.在很多语言中字符串会分为mutable和immutable两种,显然sds属于mutable类型的. 二进制安全(

【转】Redis内部数据结构详解——ziplist

本文是<Redis内部数据结构详解>系列的第四篇.在本文中,我们首先介绍一个新的Redis内部数据结构--ziplist,然后在文章后半部分我们会讨论一下在robj, dict和ziplist的基础上,Redis对外暴露的hash结构是怎样构建起来的. 我们在讨论中还会涉及到两个Redis配置(在redis.conf中的ADVANCED CONFIG部分): hash-max-ziplist-entries 512 hash-max-ziplist-value 64 本文的后半部分会对这两个配

【转】Redis内部数据结构详解 -- skiplist

本文是<Redis内部数据结构详解>系列的第六篇.在本文中,我们围绕一个Redis的内部数据结构--skiplist展开讨论. Redis里面使用skiplist是为了实现sorted set这种对外的数据结构.sorted set提供的操作非常丰富,可以满足非常多的应用场景.这也意味着,sorted set相对来说实现比较复杂.同时,skiplist这种数据结构对于很多人来说都比较陌生,因为大部分学校里的算法课都没有对这种数据结构进行过详细的介绍.因此,为了介绍得足够清楚,本文会比这个系列的

线段树--数据结构专题学习

这两周是数据结构专题的学习,,被专题的题目虐得死去活来== 线段树:简单的说就是把[1,n]的区间二分,[1,(1+n)/2]左子树,[(1+n)/2+1,n]右子树 就这样一直分下去,直到都是[x,x]这样的区间.这样就构成了一颗树了^-^ 有这样一棵树,我们就可以在节点中储存区间的和啊,区间内的最大值啊,最小值等等..这就是线段树的附加信息了,也是题目中的重点.. 我们可以用一个数组(长度为k)储存原区间的初始值,然后根据这个建树,所以这个树的节点数最多为4*K: 对于每个节点i,其左子树为

Redis数据结构详解之List(二)

序言 思来想去感觉redis中的list没什么好写的,如果单写几个命令的操作过于乏味,所以本篇最后我会根据redis中list数据类型的特殊属性,同时对比成熟的消息队列产品rabbitmq,使用redis实现一个消息队列. 为啦让本篇更有魅力,我再介绍下redis中list的基本属性,以及为什么使用redis中list列表类型,为什么使用消息队列,为什么不用rabbitmq而使用redis实现消息队列?呢,到这里为止,如果你是大咖,大牛,大神,大爷!不要听我吹牛逼啦,Close Page and

主席树入门详解+题目推荐

主席树学名可持久化线段树,就是这个可持久化,衍生了多少数据结构 为什么会有主席树这个数据结构呢?它被发明是用来解决什么问题的呢? 给定n个数,m个操作,操作类型有在某个历史版本下单点修改,输出某个历史版本下某个位置的值的值,n和m小于等于1e6 乍一看是不是一点头绪也没有.我们先来想想暴力怎么做,暴力存储第i个状态下每个数的值,显然这样做不是TLE就是MLE,我们不妨管这种状态叫做TM双LE. 如果没有这个历史状态显然处理很简单,一个线段树就解决了.那么加上历史状态呢?如果我们优化一下暴力,我们