zoukankan      html  css  js  c++  java
  • 线段树数据结构详解

    线段树数据结构详解

    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

  • 相关阅读:
    Chrome 无法登录 GitHub,响应时间过长,可行解决办法
    npm install报错 npm ERR! cb() never called! 检查镜像源!
    Win10 移动文件的时候“卡”在“正在暂停/取消”解决办法
    VS code 彻底关闭插件自动更新功能
    箭头函数 函数中的this指向
    ES6 ES6变量的声明
    范围内的拖拽事件
    div拖拽移动事件
    事件对象的属性 div点击移动事件
    tab切换之循环遍历
  • 原文地址:https://www.cnblogs.com/jelly123/p/10743601.html
Copyright © 2011-2022 走看看