线段树数据结构详解

Posted jelly123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线段树数据结构详解相关的知识,希望对你有一定的参考价值。

线段树数据结构详解

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(ncdot 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

以上是关于线段树数据结构详解的主要内容,如果未能解决你的问题,请参考以下文章

数据结构 线段树--权值线段树 详解

详解权值线段树

线段树数据结构详解

数据结构-ZKW线段树 详解

线段树详解

详解主席树(可持久化线段树)