数据结构之线段树

来源:互联网 发布:淘宝上黑曜石是真的吗 编辑:程序博客网 时间:2024/06/08 19:34

一、引例

有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。

原创粉丝点击