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了。这就 是线段树的神奇之处。
  • 相关阅读:
    【C++】资源管理
    【Shell脚本】逐行处理文本文件
    【算法题】rand5()产生rand7()
    【Shell脚本】字符串处理
    Apple iOS产品硬件参数. 不及格的程序员
    与iPhone的差距! 不及格的程序员
    iPhone游戏 Mr.Karoshi"过劳死"通关. 不及格的程序员
    XCode V4 发布了, 苹果的却是个变态. 不及格的程序员
    何时readonly 字段不是 readonly 的?结果出呼你想象!!! 不及格的程序员
    object file format unrecognized, invalid, or unsuitable Command 不及格的程序员
  • 原文地址:https://www.cnblogs.com/neopenx/p/4004369.html
Copyright © 2011-2022 走看看