• 线段树


    定义:

    线段树是一种二叉搜索树

    区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

    对于线段树中的每一个非叶子节点[a,b],

    它的左儿子表示的区间为[a,(a+b)/2],

    右儿子表示的区间为[(a+b)/2+1,b]

    因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

    未优化的空间复杂度为2N

    实际应用时一般还要开4N的数组以免越界,

    因此有时需要离散化让空间压缩。

    先把对线段树的操作(用途??)大概的分为两类吧

    一、点修改

    二、区间修改

    一、点修改

    动态范围最小值问题。

    给出一个有n各元素的数组A1,A2,...,An,设计一个数据结构,支持以下两种操作

    *** Update(x,v):把Ax修改为v、

    ***Query(L,R):计算min(AL,AL+1,...AR)。

    欸,看到“min(AL,AL+1,...AR)”的时候是不是会很自然的想到ST表呢

    但是

    如果还是使用ST表算法,每次Update操作都需要重新计算d数组,时间无法承受(肯定会t掉的啊)

    所以,还是需要用线段树啊

    一点点规定:

    每个非叶子结点都有左右两棵子树,分别对应线段的“左半”和“右半”。

    为了方便,按照从上到下,从左到右的顺序给所有的结点编号为1,2,3,...,

    那么,就会找到一个很显然的规律

    编号为i的结点,其左右结点的编号分别为2i和2i+1

    (没必要在这里纠结为什么,直观找规律就可以,而且真的真的很显然)

    看图看图,就会发现一个小小的规律

    分为两种情况:

    1.根结点是一个长度为2h的区间。

      a.第i层 有2i个结点,每个结点对应一个长度为2h-i的区间

      b.最大层编号为h,结点总数为1+2+4+8+...+2h=2h+1-1,略小于区间长度的二倍

    2.当整个区间长度不是2的整数幂时,

      虽然叶子不全在同一层,但输的最大层编号和结点总数仍满足上述结论(即情况1中的b条性质)

      (因为说明对象是最大一层,就可以把这种情况当做,把最下面的那个不完整的一层给忽略掉,就转化成了第一种情况)

    小提示:
    不同题目中,线段可以有不同的含义。

    维护:

    光有这个线段树的图是远远不够了,为了解题,就需要维护一些更重要的东西

    比如要维护“最小值”信息

    就可以用一个数组minv保存这个附加信息

    其中minv[o]表示结点o所对应的区间中所有元素的最小值。

    在比个如(嘻嘻),[5,8]的编号为4,因此minv[3]=min{A5,A6,A7,A8}

    查询

    又一个小小的规律以及规定:

    数的左右各有一条“主线”,虽然有分叉,但每层最多只有两个结点继续向下延伸,因此,“查询边界”结点不超过2h个(h为线段树的最大层编号)。

    这实际上吧带查询线段分解成不超过2个不想交线段的并。

    如图中[2,5]=[2]+[3,4]+[5]。在后文中,凡是遇到这样的区间分解,就把分解得到的各个区间叫做边界区间,因为它们对应于分解过程的递归边界

    更新:

    既要更新[i,i]对应的结点,又要更新他的所有祖先结点

    查询&修改代码如下:

    /*
    o为当前结点的编号 
    L和R是当前结点的左右端点(比如当o = 3的时候,L=5,R=8)
    查询:全局变量ql,qr分别代表查询区间的左右端点 
    修改:p,v分别代表修改点位置和修改后的数值 
    */
    
    int ql,qr;//查询[ql,qr]中的最小值
    int query(int o,int L,int R)
    {
        int M = L + (R - L)/ 2,ans = INf;
        if(ql <= L&&R <= qr)//当前结点完全包含在查询区间内 
            return minv[o];
        if(ql <= M)//往左走 
            ans = min(ans,query(o*2,L,M));
        if(M < qr)//往右走 
            ans = min(ans,query(o*2,M+1,R));
        return ans;
    }
    
    int p,v;//修改:A[p] = v;
    void update(int o,int L,int R)
    {
        int M = L + (R - L)/ 2;
        if(L == M)//叶结点,直接更新minv 
            minv[o] = v;
        else
        {//L<R 
        //先递归更新左子树 
            if(p <= M)
                update(o*2,L,M);
            else
                update(o*2+1,M+1,R);
        //然后计算本结点的minv 
                minv[o] = min(minv[o*2],minv[o*2+1]);
        }         
    }

    建树:

    方法一:

      每读入一个元素x后执行修改操作A[i]=x,

      时间复杂度位O(nlogn)。

    方法二:

      只需要事先设置好每个叶结点的值,自底向上递推(也可以写成递归)

      每个结点只计算了一次,时间复杂度O(n)。

    二、区间修改

    快速序列操作I。

    给出一个n个元素的数组A1,A2,...,An,设计一个数据结构,支持一下两种操作。

    ***Add(L,R,v):把AL,AL+1,...AR全部增加v。

    ***Query(L,R):计算子序列AL,AL+1,...AR的元素和,最小值,最大值。

    这时就会想到维护3个信息:sum,min,max;分别对应三个查询值。

    add操作为区间修改操作。

    为了避免在最坏的情况下,区间修改影响到所有结点;就用lazy标记吧。

    add操作:

    依旧是把add分解成不超过2h个操作,记录在线段树的结点。

    下图展现了执行完了add(L,R,v)的操作之后的情形:

    eg.add(1,7,5)  add(3,6,2)

     

    -----------------------代码分割线---------------------------

    注:以下维护、修改、查询范围均是[y1,y2].

    信息维护:

    sum[o]为“如果只执行结点o及其子孙结点中的add操作,结点o对应区间中所有数之和”

    (这样,每个原始add所影响的结点数目变成了O(h))

    //维护结点o,他对应区间[L,R]
    void maintain(int o,int L,int R)
    {
        int lc = o * 2;//lc为左子树的编号
        int rc = o * 2 + 1;//rc为右子树的编号
        sumv[o] = minv[o] = maxv[o] = 0;//注意:非叶子节点本身是没有值的 
        if(R > L)//不是叶子结点,考虑左右子树 
        {
            sumv[o] = sumv[lc] + sumv[rc];//求和 
            minv[o] = min(minv[lc],minv[rc]);//取左、右子树中的最小值做最小值 
            maxv[o] = max(maxv[lc],maxv[rc]);//取左、右子树中的最大值左最大值 
        }
        minv[o] += addv[o];
        maxn[o] += addv[o];
        sumv[o] += addv[o] * (R - L + 1);
        //考虑add操作 
    }

       修改:

    在执行add操作时,递归访问的结点全部要调用,并且是在递归返回后调用

    //修改结点o,对应区间[L,R]
    void update(int o,int L,int R)
    {
        int lc = o * 2;
        int rc = o * 2 + 1;
        if(y1 <= L && R <= y2)//递归边界 
            addv[o] += v;//累加边界的add值
        else
        {
            int M = L + (R - L)/ 2;
            if(y1 <= M)
                update(lc,L,M);
            if(y > M)
                update(rc,M+1,R);
        }
        maintain(o,L,R);//递归结束前,重新计算本结点的附加信息 
    }

    查询:

    仍然是把查询区间递归分解为若干不相交自区间

    把各个子区间的查询结果加以合并

    同时考虑祖先结点对他的影响

    为了方便,我们在递归查询函数中增加一个参数,用来表示当前区间的所有祖先结点add值之和

    int _min,_max,_sum;//全局变量,目前位置的最小值、最大值、累加和
    void query(int o,int L,int R,int add)
    {
        if(y1 <= L && R <= y2)//递归边界,用边界区间的附加信息更新答案 
        {
            _sum += sumv[o] + add * (R - L + 1);
            _min = min(_min,minv[o] + add);
            _max = max(_max,maxn[o] + add);
        }
        else//递归统计,累加参数add 
        {
            int M = L + (R - L)/ 2;
            if(y1 <= M)
                query(o*2,L,M,add + addv[o]);
            if(y2 > M)
                query(o*2+1,M+1,R,add + addv[o]);
        }
    }

    快速序列操作II。

    给出一个n个元素的数组A1,A2,...,An,设计一个数据结构,支持一下两种操作。

    ***Add(L,R,v):把AL,AL+1,...AR全部修改为v(v>=0)。

    ***Query(L,R):计算子序列AL,AL+1,...AR的元素和,最小值,最大值

    不难想到把set操作也进行分解,但set操作的时间顺序会影响结果

    所以

    除了对本操作进行分解之外

    还要修改以前分解好的操作

    使任意两个set不存在祖先后代关系

    举例说明:

    在一颗根节点为[1,8]的线段树上先执行set(1,8,1)的操作,

    在执行set(1,3,2)操作

    做法:

      首先,set(1,8,1)就简单设置根结点的set值为1

      然后,将set(1,3,2)分为3个步骤

    //标记传递
    void pushdown(int o)
    {
        int lc = o * 2,rc = o * 2 + 1;
        if(setv[o] >= 0)//本结点有标记才传递。本题中的前提为:set值为非负,所以-1代表没有标记 
        {
            setv[lc] = setv[rc] = setv[o];
            setv[o] = -1;//清除本结点标记 
        }
    }
    
    //修改操作代码
    void update(int o,int L,int R)
    {
        int lc = o * 2,rc = o * 2 + 1;
        if(y1 <= L && R <= y2)//标记修饰 
            setv[o] = v;
        else
        {
            pushdown(o);
            int M = L + (R - L)/2;
            if(y1 <= M)
                update(lc,L,M);
            else
                maintain(lc,L,M);
            if(y2 > M)
                update(rc,M+1,R);
            else
                maintain(rc,M+1,R);
        }
        maintain(o,L,R);
    } 

    注意:

      与上一题相比,代码中多了两处maintain的调用。

      这是因为,只要标记下传,该孩子书的附加信息必须重新计算。

      对于本来就要递归访问的子树,递归访问结束之后自然会调用maintain

      因此只需要针对不进行递归访问的子树调用maintain

     

    如果先执行set(1,3,2)在执行set(1,8,1)

    情形如下:

    这违反了前面讲的“任意两个set操作不会存在祖先-后代关系”

    但,我们还可以

    以祖先结点上的操作为准,在递归查询是,一旦碰到一个set操作就立即停止

    代码如下:

    void query(int o,int L,int R)
    {
        if(setv[o]>=0)//递归边界1:有set标记 
        {
            _sum += setv[o] * (min(R,y2) - max(L,y1) + 1);
            _min = min(_min,setv[o]);
            _max = max(_max,maxv[o]);
        }
        else
        if(y1 <= L&&y2 >= R)//递归边界2:边界区间 
        {
            _sum +=sumv[o];//次边界区间没有被任何set操作影响 
            _min = min(_min,minv[o]);
            _max = max(_max,maxv[o]);
        }
        else
        {//递归统计 
            int M = L + (R - L)/2;
            if(y1 <= M)
                query(o * 2,L,M);
            if(y2 < M)
                query(o * 2,M+1,R);
        }
    }
  • 相关阅读:
    在 Java 中遍历 HashMap 的5种最佳方式
    Java 8 Stream.reduce() 使用示例
    Redis 为什么这么快?
    java8 常用代码
    为什么我们越娱乐反而会越无聊?
    Arrays.sort() VS Arrays.parallelSort()
    Java中枚举类型Enum的一种使用方式
    An illegal reflective access operation has occurred
    多线程中常见锁概述
    Java中创建多线程的三种方式
  • 原文地址:https://www.cnblogs.com/darlingroot/p/10384501.html
Copyright © 2020-2023  润新知