线段树算法学习

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]),就相当于单节点更新,单节点更新只是区间更新的特例

 

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

posted @ 2016-07-12 20:44  Shawn_Ji  阅读(1256)  评论(0编辑  收藏  举报