zoukankan      html  css  js  c++  java
  • 线段树

        线段树与BST不同,它维护的是区间信息,树高越低,区间范围越大,而最后一层就是单点信息。线段树的价值于其维护的区间信息,如果不能有效利用,那么线段树就是一颗废树。

    一、单点更新

    线段树按照结点更新方式的不同,分为单点更新和成段更新。单点更新是线段树最简单的结构。通常由Push_Up, Build,Update,Query四个操作构成。其中Push_Up代码最简单,但却是线段树的核心,它负责区间信息的向上更新,换言之,如果设计不 出有效的Push_Up,那么线段树就是颗废树。

     
    #include "iostream"
    #include "string"
    #include "vector"
    #include "cstring"
    #include "fstream"
    #include "cstdio"
    using namespace std;
    #define M 50001
    #define lson l,mid,root<<1
    #define rson mid+1,r,root<<1|1
    int sum[M<<2];
    void PushUp(int root)
    {
        sum[root]=sum[root<<1]+sum[root<<1|1];
    }
    void build(int l,int r,int root)
    {
        if(l==r)
        {
            scanf("%d",&sum[root]);
            return;
        }
        int mid=(l+r)>>1;
        build(lson);
        build(rson);
        PushUp(root);
    }
    void update(int p,int value,int l,int r,int root)
    {
        if(l==r)
        {
            sum[root]=value;
            return;
        }
        int mid=(l+r)>>1;
        if(p<=mid) update(p,value,lson);
        else update(p,value,rson);
        PushUp(root);
    }
    int qurry(int L,int R,int l,int r,int root)
    {
        if(L<=l&&r<=R) return sum[root];
        int mid=(l+r)>>1;
        int ret=0;
        if(L<=mid) ret+=qurry(L,R,lson);
        if(R>mid) ret+=qurry(L,R,rson);
        return ret;
    }
    单点更新的基本操作

    上面的线段树风格来自Hdu的神犇NotOnlySuccess,他的那篇线段树总结写的很好,不过个人主页经常挂。orz。

    线段树是大量数据时代,首先得抛弃使用cin的坏习惯。Windows下,不关sync的cin时间是scanf的10倍,关了也有2~3倍,100%会超时。Linux下关了sync的cin倒是比scanf快。

    根据Push_Up操作,大致可以分为以下几类。

     
    1. 区间求和
     sum[root]=sum[root<<1]+sum[root<<1|1]
     

    @练习题

    HDU 1166, 线段树求和模板题,更新之后查询即可。

    XCOJ 1019, 阉割掉Update的线段树,其实用树状数组做更好。坑的地方是有重复点,要叠加。

    2. 区间RMQ(范围最值查询)

    RMQ[root]=max(RMQ[root<<1],RMQ[root<<1|1]);

    @练习题

    HDU 1754,线段树RMQ模板题,Query操作记得改下。

    HDU 2795, 很有趣的一个题,利用的是RMQ的性质,去放东西。如果大小<=左子树区间最大值,那么就往左走。左子树放不了,再考虑右子树。亮点是Update和Query一体化,这也是非裸线段树的一大难点,一般我们认为这题是线段树首先看有没有明显Update、Query,但是这题藏得很隐蔽。
     
    3. 区间覆盖(HASH)
     
    特殊类问题,不需要Push_Up,但是依赖于离散数据结构。

    @练习题
     
    POJ 2828,排队插队问题。维护一个区间剩余容量。按顺序给出的是入队后的位置(可能原始位置,可能插过队)。我一开是顺序考虑,如果那个位置有人就把原来 的人后移,但是万一后面也有人呢?得整体后移,但是如果有空位的又不能整体后移,得把空位填掉,实在太麻烦。后来看题解,才明白应该逆序处理,逆序之后 update,只要不重叠,那就是最终位置。如果重叠,那就利用pos-左右区间剩余容量确定新的空位置,因为逆序,只要这个位置是空的,就是最终位置,不用考虑邻近点偏移了。维护(update)区间容量的时候,每次到一个区间都要-1(100%会插在这个区间)。由于逆序,要记录每次插的位置,最后顺序输出。
     
     

    二、成段更新

    成段更新的Update操作发生了变化,对指定L..R区间进行更新。由于点更新量可能是单点更新的几十倍,若对每个点采取自下而上的Push_Up,那么在Update超庞大情况下势必会完蛋。所以成段更新使用了Lazy思想,即暂时不更新,做个可以叠加更新的标记,到不得不更新的情况下再更新,不过这次是往下更新(上面已经被更新过了),这个操作称为Push_Down。 Lazy标记的使用使得线段树的结构异常复杂,尤其在有多个操作时(如加减乘除运算混合),因为其涉及到操作的合并问题,如何设计Lazy标记成为难点。
     
    void build(int l,int r,int root)
    {
        col[root]=-1; //多组数据,标记清除
    }
    
    void PushDown(int root,int m)
    {
        if(col[root]!=-1)
        {
        col[root<<1]=col[root<<1|1]=col[root];
        sum[root<<1]=col[root]*(m-(m>>1));
        sum[root<<1|1]=col[root]*(m>>1);
        col[root]=-1;
        }
    }
    
    void update(int L,int R,int c,int l,int r,int root)
    {
        if(L<=l&&r<=R)
        {
            col[root]=c;
            sum[root]=c*(r-l+1);
            return;
        }
        PushDown(root,r-l+1);
        int mid=(l+r)>>1;
        if(L<=mid) update(L,R,c,lson);
        if(R>mid) update(L,R,c,rson);
        PushUp(root);
    }
    
    int query(int L,int R,int l,int r,int root)
    {
        if(L<=l&&r<=R) return sum[root];
        PushDown(root,r-l+1); //这里的PushDown很重要
        //下同单点更新
    }
    成段更新的基本操作
     
    1. 区间求和

    @练习题
     
    HDU 1698, 求和裸题,注意Push_Down和Update修改sum[root]的时候得算上整个区间。

    POJ 3468,Lazy标记从覆盖变成了叠加,注意PushDown左右子标记的时候+=col[root],而不是覆盖,对待sum也一样。

    XCOJ 1025,安徽OI 09年省选题,加和乘两种优先级不同的运算符给这题的PushDown带来了巨大麻烦。全程的mod让人蛋疼。首先明确一点,乘法标记mul优先级大于加 法标记add,所以必然是sum[root]=sum[root]*mul[root]+add[root]*区间,更新add左右标记的时候,要先乘以 mul标记再加,更新mul标记则直接乘。mul标记重置是1,add标记重置是0
     
    2. 区间覆盖(HASH)
     
    @练习题
     
    POJ2528, 首先得清楚常规线段树的叶子结点容量大概是10^6,这题达到了10^7,所以我们要将离散的区间“压缩”。

    通俗点说,离散化就是压缩区间,使原有的长区间映射到新的短区间,但是区间压缩前后的覆盖关系不变。举个例子:

    有一条1到10的数轴(长度为9),给定4个区间[2,4] [3,6] [8,10] [6,9],覆盖关系就是后者覆盖前者,每个区间染色依次为 1 2 3 4。

    现在我们抽取这4个区间的8个端点,2 4 3 6 8 10 6 9

    然后删除相同的端点,这里相同的端点为6,则剩下2 4 3 6 8 10 9

    对其升序排序,得2 3 4 6 8 9 10

    然后建立映射

    2     3     4     6     8     9   10

    ↓     ↓      ↓     ↓     ↓     ↓     ↓

    1     2     3     4     5     6     7

    那么新的4个区间为 [1,3] [2,4] [5,7] [4,6],覆盖关系没有被改变。新数轴为1到7,即原数轴的长度从9压缩到6,显然构造[1,7]的线段树比构造[1,10]的线段树更省空间,搜索也更快,但是求解的结果却是一致的。

    离散化时有一点必须要注意的,就是必须先剔除相同端点后再排序,这样可以减少参与排序元素的个数,节省时间。


    压缩完区间后,按照每张海报的id对其区域编号修改(update)。但是在Query的时候,作出了很大修改。

    void query(int *col,bool *Hash,int l,int r,int root)
    {
        if (col[root])
        {
            if (!Hash[col[root]]) cnt++;
            Hash[col[root]] = true;
            return;
        }
        PushDown(col,root,r-l+1);
        if (l==r) return ;
        int mid = (l + r) >> 1;
        query(col,Hash,lson);
        query(col,Hash,rson);
    }
    Query的操作

    注意这里利用线段树中保留的Lazy标记来HASH,首先你得明白,叶子结点必然保留着标记,但是如果只是对叶子结点HASH,那么线段树就没 有存在意义了。幸运的是,叶子结点上层存在着大量未被消除的线段Lazy标记,只要把这些标记用起来,我们就没必要下到每个叶子结点进行HASH了。这就 是线段树的神奇之处。
  • 相关阅读:
    hdu 2665 划分树
    概率模型与条件随机场
    shell中各种括号的作用()、(())、[]、[[]]、{}
    今日BBC
    小贝_mysql主从复制作用以及案例
    c++ builder 版CreateAnonymousThread用法
    安卓UI适配限定符
    编译3.10内核 出现错误 “undefined reference to....&quot; 解决方法
    iptables的4表5链(未完)
    已有iptables表的查看
  • 原文地址:https://www.cnblogs.com/neopenx/p/4004369.html
Copyright © 2011-2022 走看看