数据结构之线段树

一、引例

有M个数排成一列,做N次操作,每次操作包括:
(1)询问指定区间的最大值、最小值
(2)将指定区间的每个数加上一个值
如果按照最朴素的做法,一个个的遍历,时间复杂度:O(MN)。
那么如何解决一个区间求和(最大值,最小值)的问题呢?那么就要用到线段树啦。

二、定义
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

主要用来解决区间查询、区间修改,使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,基本保证每次操作的时间复杂度为O(logN)。

三、实际应用
a.单点更新

1.定义每个结点的信息
线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。

struct node
{
    int left,right,sum;//左端点,右端点,和
} tree[maxn<<2];

2.更新

void maintain(int root)//更新根节点为左右子结点的和
{
    int lnode=root<<1;
    int rnode=root<<1+1;
    tree[root].sum=tree[lnode].sum+tree[rnode].sum;
}

3.递归建树
遇到叶子节点直接赋值,否则递归遍历左右建树,最后回溯即可。

void build(int root,int begin,int end)//树的结点编号 左端点下标 右端点下标
{
    tree[root].left=begin;
    tree[root].right=end;
    if(begin==end)//叶子节点
    {
        scanf("%d",&adj[begin]);
        tree[root].sum=adj[begin];//单元素 直接赋值
        return ;
    }
    int mid=(begin+end)>>1;
    build(root<<1,begin,mid);//更新左子树
    build(root<<1+1,mid+1,end);//更新右子树
    maintain(root);//存储左右子树的和
}

4.单点更新
将一条线段[a,b] 插入到代表线段[l,r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。如果b<mid,那么将线段[a,b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a,b] 也插入到p的右儿子结点中。

void update(int root,int pos,int num)
        //根结点编号 欲修改值的下标 期待的值
{
    if(tree[root].left==tree[root].right&&tree[root].left==pos)
    {//若修改的值在这个节点的左右区间之间那么就直接更改此区间的sum值就可
        tree[root].sum+=num;
        return ;
    }
    int mid=(tree[root].left+tree[root].right)>>1;
    if(pos<=mid)
        update(root<<1,pos,num);
    else
        update(root<<1+1,pos,num);
    maintain(root);//每次都要更新根节点
}

5.求和操作

int query(int root,int begin,int end)//求和
{
    int ans=0;
    if(begin==tree[root].left&&end==tree[root].right)
    {
        return tree[root].sum;
    }
    int mid=(tree[root].left+tree[root].right)>>1;
    if(end<=mid)
        ans+=query(root<<1,begin,end);
    else if(begin>=mid+1)
        ans+=query(root<<1+1,begin,end);
    else
    {
        ans+=query(root<<1,begin,mid);
        ans+=query(root<<1+1,mid+1,end);
    }
    return ans;
}

b.区间更新(成段更新)

比如 从[1,10]每个结点的值都+1,普通单点更新就会超时。
*区间更新:
指更新某个区间内的叶子节点的值,因为涉及到的叶子节点不止一个,而叶子节点会影响其相应的非叶父节点,那么回溯需要更新的非叶子节点也会有很多,如果一次性更新完,操作的时间复杂度肯定不是O(lgn),例如当我们要更新区间[0,3]内的叶子节点时,需要更新出了叶子节点3,9外的所有其他节点。为此引入了线段树中的延迟标记概念,这也是线段树的精华所在。

*延迟标记:
因为更新的数很多,所以我每一步的更新不接着算出来,等到最后需要的时候再去取消标记算出来。

比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,它的节点标记为rt,这时tree[rt].l == a && tree[rt].r == b 这时我们可以一步更新此时rt节点的sum[rt]的值,sum[rt] += c * (tree[rt].r - tree[rt].l + 1),注意关键的时刻来了,如果此时按照常规的线段树的update操作,这时候还应该更新rt子节点的sum[]值,而Lazy思想恰恰是暂时不更新rt子节点的sum[]值,到此就return,直到下次需要用到rt子节点的值的时候才去更新,这样避免许多可能无用的操作,从而节省时间 。

用lazy标记,等到当前区间比我需要的目标区间大的时候,我必须用到下面的值了,必须往下修改了,这时候,我们就把之前堆积起来的懒惰标记pushdown了,于是就有了一个神奇的pushdown操作。

其他的建树什么的和单点更新一样,只是多了lazy标记和pushdown。

void pushdown(LL root)  //向下传递lazy标记 
{
    if (tree[root].lazy)
    {
        tree[root<<1].lazy+=tree[root].lazy;
        tree[root<<1+1].lazy+=tree[root].lazy;
        tree[root<<1].val+=tree[root<<1].len*tree[root].lazy;
       tree[root<<1+1].val+=tree[root<<1+1].len*tree[root].lazy;
        tree[root].lazy=0; 
    }
}

c.区间合并
不过还没做过这方面的题QAQ

参考博客
很有趣很好懂
很官方很全面
来做题吧 超全

ps:之前看过,今天再看像重新学了一遍(>_<)
今天距离省赛过去已经一个星期了,该调整回来了。
还是不够强,继续修炼吧QAQ。

发布了87 篇原创文章 · 获赞 8 · 访问量 3万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章