• 线段树算法学习


    http://www.cnblogs.com/TenosDoIt/p/3453089.html#b

    线段树的思想是 将一整个区间二分拆成多个不重合的2段区间,知道不能拆分为止。

    例如对于数组[2, 5, 1, 4, 9, 3]可以构造如下的二叉树(背景为白色表示叶子节点,非叶子节点的值是其对应数组区间内的最小值,例如根节点表示数组区间arr[0...5]内的最小值是1): 

     

    空间消耗: 如果假定原数组的长度为n,那么线段树的节点数就设为4*n。  原理:假设n=2^h,则 从第0行(i=0)开始,第i行有2^i个节点,一共有h行,所以节点总数为1+2+4+8+...+2^h=2^(h+1)-1=2*2^h - 1     等于2n-1 , 这时候难道线段树的空间复杂度就是O(2n-1)吗?  不是的。 这里我们假设了n=2^h,但是题目中的n可没说一定是2的幂次,这导致最后一行有一些节点没有用到(如上图最后一行所示),不过这没有关系,我们可以多开辟一些空间,这不影响

    这时我们可以找一个最小且满足n<=2^h  的h,这时候就可以说线段数的空间复杂度是(2*2^h - 1)了。

    但是常用的并不是这个空间复杂度,而是4*n,这是为什么呢?

    因为开辟空间的时候去判断最大的h为多少略显麻烦,事实上我们的空间不用这么精打细算,可不可以找到一个方便得到的“可行”的空间复杂度呢?

    可以这样考虑:当我们找到一个最小且满足n<=2^h  的h时,即 2^(h-1) <= n <= 2^h , 所以我们得到 2^h <= 2n , 所以接着上面的结论 复杂度(2*2^h - 1)可以拓展成(2*2n - 1) 即(4*n)。

     

    算法分几个部分:

    const int maxn=2e2+9;
    int arr[maxn];
    
    struct  seg //segtree下标从1开始
    {
        int val;
    }segTree[4*maxn];   //4倍空间

     

    1、build建树

    void build(int node, int istart, int iend)
    {
        if(istart == iend)
            segTree[node].val = arr[istart];
        else
        {
            int mid=istart+(iend-istart)/2 ;
            build(2*node, istart, mid);
            build(2*node+1, mid+1, iend);
    //        回溯计算当前节点的val
            segTree[node].val = min(segTree[2*node].val, segTree[2*node+1].val);
        }
    }

    2、查询指定区间的最值

    //qstart qend     待查询区间起始位置
    int query(int node, int nstart, int nend, int qstart, int qend)
    {
        if(qend < nstart || qstart > nend)
            return INF;  // 无效,置无穷大
        if(qstart <= nstart && qend >= nend)
            return segTree[node].val;
        //即将用到子节点,更新子节点的add
        return min( query(2*node, nstart, mid, qstart, qend),
                    query(2*node+1, mid+1, nend, qstart, qend));
    }

     

     

    3、单节点更新,这是线段树相比RMQ等算法的优势地方,他可以在O(logn)的时间复杂度下修改元素值并更新所有相关的最值。

    增加某个元素值,并更新最值

    //index   指定增加值的数组下标
    void UpdateOne(int node, int nstart, int nend, int index, int addval)
    {
        if(nstart == nend)
        {
            if(nstart == index)
                segTree[node].val += addval;
            return ;
        }
        int mid=nstart+(nend-nstart)/2 ;
        UpdateOne(2*node, nstart, mid, index, addval);
        UpdateOne(2*node+1, mid+1, nend, index, addval);
    //    自下向上回溯节点
        segTree[node].val = min( segTree[2*node].val , segTree[2*node+1].val);
    }

     

    4、区间更新   这也是线段树的优势,并且,算法通过增加一个延迟更新标记的变量AddMark,大大减少了更新最值的时间复杂度

    考虑当我们需要修改了一个区间(a,b)所有元素的val时,理论上来说,需要更新所有子节点的val (segTree[node].val) 【父节点可以通过回溯更新】,但是如果这个区间的子节点比较多的时候,如果一次性全部更新完所有子节点,复杂度肯定是O( (b-a)lgn ),这复杂度是比较高的,往往也是不必要的,(后续程序不一定会用到这些子节点,可能查询到他们的父节点时已经完成查询)。我们可以 需要的用到这些子节点时候再更新这些子节点。 当我们找到一个节点p,并且决定考虑其子节点时,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。

     

    因此需要在线段树结构中加入延迟更新标记,我的程序中是加入addmark,表示节点的子孙节点在原来的值的基础上加上addMark的值,同时还需要修改创建函数build 和 查询函数 query,修改的代码用红色字体表示,其中区间更新的函数为update,代码如下:

    struct  seg //segtree下标从1开始
    {
        int AddMark;
        int val;
    }segTree[4*maxn];   //4倍空间
    
    void build(int node, int istart, int iend)
    {
    //    初始化addmark = 0
        segTree[node].AddMark = 0;
        if(istart == iend)
            segTree[node].val = arr[istart];
        else
        {
            int mid=istart+(iend-istart)/2 ;
            build(2*node, istart, mid);
            build(2*node+1, mid+1, iend);
    //        回溯计算当前节点的val
            segTree[node].val = min(segTree[2*node].val, segTree[2*node+1].val);
        }
    }
    
    void pushDown(int node)
    {
        if(segTree[node].AddMark != 0)
        {
    //        子节点增加addmark
            segTree[2*node].AddMark += segTree[node].AddMark;
            segTree[2*node+1].AddMark += segTree[node].AddMark;
    //        子节点增加value
            segTree[2*node].val += segTree[node].AddMark;
            segTree[2*node+1].val += segTree[node].AddMark;
    //        当前节点addmark取消
            segTree[node].AddMark = 0;
        }
    }
    
    //qstart qend     待查询区间起始位置
    int query(int node, int nstart, int nend, int qstart, int qend)
    {
        if(qend < nstart || qstart > nend)
            return INF;  // 无效,置无穷大
        if(qstart <= nstart && qend >= nend)
            return segTree[node].val;
        //即将用到子节点,更新子节点的add
        pushDown(node);
        int mid=nstart+(nend-nstart)/2 ;
        return min( query(2*node, nstart, mid, qstart, qend),
                    query(2*node+1, mid+1, nend, qstart, qend));
    }
    
    //index   指定增加值的数组下标
    void UpdateOne(int node, int nstart, int nend, int index, int addval)
    {
        if(nstart == nend)
        {
            if(nstart == index)
                segTree[node].val += addval;
            return ;
        }
        int mid=nstart+(nend-nstart)/2 ;
        UpdateOne(2*node, nstart, mid, index, addval);
        UpdateOne(2*node+1, mid+1, nend, index, addval);
    //    自下向上回溯节点
        segTree[node].val = min( segTree[2*node].val , segTree[2*node+1].val);
    }
    
    //nstart nend 当前区间起始位置
    //astart aend   指定的更新区间起始位置
    void updateArea(int node, int nstart, int nend, int astart, int aend, int addval)
    {
        if(nstart > aend || nend < astart)
            return ;
        if(nstart >= astart && nend <= aend)
        {
            //先标记  不急着更新子节点的值,等需要用到子节点时再更新
            segTree[node].AddMark += addval;
            segTree[node].val += addval;
            return ;
        }
        else
        {
    //        需要用到子节点了,调用pushdown更新
            pushDown(node);
            int mid=nstart+(nend-nstart)/2 ;
            updateArea(2*node, nstart, mid, astart, aend, addval);
            updateArea(2*node+1, mid+1, nend, astart, aend, addval);
            segTree[node].val=min(segTree[2*node].val, segTree[2*node+1].val);
        }
    }

    举个例子:

    当我们要对区间[0,2]的叶子节点增加2,利用区间查询的方法从根节点开始找到了非叶子节点[0-2],把它的值设置为1+2 = 3,并且把它的延迟标记设置为2,更新完毕;当我们要查询区间[0,1]内的最小值时,查找到区间[0,2]时,发现它的标记不为0,并且还要向下搜索,因此要把标记向下传递,把节点[0-1]的值设置为2+2 = 4,标记设置为2,节点[2-2]的值设置为1+2 = 3,标记设置为2(其实叶子节点的标志是不起作用的,这里是为了操作的一致性),然后返回查询结果:[0-1]节点的值4;当我们再次更新区间[0,1](增加3)时,查询到节点[0-1],发现它的标记值为2,因此把它的标记值设置为2+3 = 5,节点的值设置为4+3 = 7;

    其实当区间更新的区间左右值相等时([i,i]),就相当于单节点更新,单节点更新只是区间更新的特例

     

    ------------------------------以上----------------------------------------------------

  • 相关阅读:
    2019-9-2-简单搭建自己的博客
    2018-7-15-WPF-在-DrawingContext-的-push-如何使用
    2018-7-15-WPF-在-DrawingContext-的-push-如何使用
    2019-7-3-Roslyn-理解-msbuild-的清理过程
    2019-7-3-Roslyn-理解-msbuild-的清理过程
    MySQL数据库事务详解
    求一个Map中最大的value值,同时列出键,值
    Struts1入门实例(简单登录)
    java字符流操作flush()方法及其注意事项
    HDU 1874 畅通工程续 2008浙大研究生复试热身赛(2)
  • 原文地址:https://www.cnblogs.com/shawn-ji/p/5664809.html
Copyright © 2020-2023  润新知