• 线段树 学习笔记


    类似于区间树,在各个节点保存的是一条线段(子数组),可高效解决连续区间动态查询问题。

    *单点或区间的修改 区间的最值以及求和

    可基本保持单次操作为log的复杂度。

    线段树的每个节点表示一个区间,子节点则分别表示父亲节点的左半区间和右半区间。如果父亲节点是[a,b],那么令c = (a+b) / 2,有左儿子[a,c],右儿子[c+1,b]。

    可以用来求解形似下面的问题:

    给定一个数列,要求你查找某个区间内的最小值,支持元素的更新。

    朴素的做法显然,但是时间复杂度高达O(n),尽管所需额外空间复杂度只有O(1),但时间复杂度增长太高,这是我们不能接受的。

    还有一种做法是用一个二维数组提前处理好区间[i,j]的最小值,这样可以O(1)查询,但当数据很大时,O(n^2)的空间开销无法承受。并且这样做在有更改操作时会变得非常麻烦。

    线段树的做法。有一个O(n)的预处理,查询和更新操作均为O(logn),额外的空间复杂度是O(n)。

    比如有一个[1,6]的二叉树。

       

     

    叶节点是原始数组中的元素,非叶节点代表所有子孙节点所在区间的最小值。由于线段树的父节点平均分割左右子树,所以线段树是完全二叉树。

    *线段树的创建

    数组模拟存储与链式存储。这里使用前一种。

    定义包含n个节点的线段树int segtree_val[maxn],segtree_val[0]表示根节点,对于节点segtree_val[i],它的左儿子是segtree_val[2*i+1],右儿子是segtree[2*i+2]。

    从根节点开始,平分区间, 递归的创建线段树。

    这是优化之后的代码。

     1 const int INF = 0x3f3f3f;
     2 const int N = 1000;
     3 int a[N];
     4 struct segment_tree{
     5 #define lson (o<<1)
     6 #define rson (o<<1|1)
     7     int sumv[N*4];
     8     int minv[N<<2],addv[N<<2];
     9     //存储线段树的区间和,一般二倍大即可,但有部分情况会超过二倍大小,所以开四倍大比较保险
    10     inline void push_up(int o){
    11         sumv[o] = sumv[lson] + sumv[rson];
    12     }
    13     inline void pushdown(int o){
    14         if (!addv[o])
    15             return;
    16     }
    17     inline void build(int o,int l,int r){
    18         if (l==r){
    19             sumv[o] == a[l];
    20             return ;
    21         }
    22         int mid = (l+r) >> 1;
    23         build(lson,l,mid);
    24         builf(rson,mid+1,r);
    25         push_up(o);
    26 
    27     }
    28     inline int querymin(int o,int l,int r,int ql,int qr){
    29         if (ql <= l && r <= qr)
    30             return sumv[o];
    31         int mid = (l+r) >> 1;
    32         int ans = INF;
    33         pushdown(o);
    34         if (ql <= mid)
    35             ans = min(ans,querymin(lson,l,mid,ql,qr));
    36         if (qr > mid)
    37             ans = min(ans,querymin(rson,mid+1,r,ql,qr));
    38         return ans;
    39     }
    40 
    41     inline void change(int o,int l,int r,int q,int v){
    42         if (l==r){
    43             minv[o] += v;
    44             return;
    45         }
    46         int mid = (l+r) >> 1;
    47         if (q<=mid)
    48             change(lson,l,mid,q,v);
    49         else
    50             change(rson,mid+1,r,q,v);
    51         push_up(o);
    52     }
    53     
    54     inline void optadd(int o,int l,int r,int ql,int qr,int v){
    55         if (ql <= l && r <= qr){
    56             minv[o]+=v;
    57             addv[o]+=v;
    58             return ;
    59         }
    60         int mid = (l+r) >> 1;
    61         pushdown(o);
    62         if (ql <= mid)
    63             optadd(lson,l,mid,ql,qr,v);
    64         if (qr > mid)
    65             optadd(rson,mid+1,r,ql,qr,v);
    66         push_up(o);
    67     }
    68 };

    *查询线段树

    我们的任务是查找某个区间上的最小值,查询的思想是选出一些区间,让它们相连后恰好覆盖整个覆盖整个查询区间,因此线段树适合解决“相邻的区间的信息可以被合并成两个区间的并区间的信息”的问题。

    *单节点更新

    指只更新线段树的某个叶子节点的值,但是更新叶子节点会对其父节点产生一些影响,因此更新子节点后,要一并更新父节点的值。

    *区间更新

    指更新某个区间内的叶节点的值,因为涉及到的叶节点不止一个,而叶节点会影响相应的父节点,那么需要更新的父节点就会有很多,如果一次性更新完,性能是达不到要求的。

    为此引入了线段树的懒惰标记概念。(lazy tag),这是线段树的精华所在。

    懒惰标记:每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改会影响子节点),对于任意区间的修改,先按照区间查询的方式将其划分成线段树的节点,然后去修改这些节点的信息,并给这些节点打上修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。

    因此需要在线段树结构中加入延迟标记区域,我们加入标记addv,表示节点的子孙节点在原来的值的基础上加上addv的值,同时还需要修改创建函数build和查询函数querymin,区间更新的函数为optadd。

  • 相关阅读:
    每天删除和创建表
    一种直接取数的建表语句
    正则替换函数regexp_replace()
    将同个id对应的多行数据,转为一行数组类型数据。使用collect_set() group by函数
    根据客户端标识加载不同的css文件
    配置npm镜像
    解决方案
    oracle virtualbox设置asm虚拟硬盘
    scala基础
    excel合并sheet内容
  • 原文地址:https://www.cnblogs.com/OIerShawnZhou/p/7636810.html
Copyright © 2020-2023  润新知