线段树(模板)

 

 

 

   线段树它是一棵高度平衡的二叉树,很多二叉树的性质它是完美继承的

我们来看一道题:

HDU1166敌兵布阵

这道题如果用常规暴力的做法,就把所有营地的士兵存在一个数组里面,然后对于每次操作直接更新对应位置的数,对于每次询问直接从i到j加起来。然而这么操作下来,对于极限数据50000个人,40000条命令,显然是会超时的,那么一种新的数据结构线段树就应运而生了。

首先第一个疑问:为什么线段树会快?

显然对于m个点n次询问,暴力的做法时间复杂度是O(m*n)的。然而线段树作为一棵二叉树,继承了二叉树O(logn)的优良品质,对于这道题最坏的复杂度也是O(m*logn)的,这个量显然是符合时间要求的。

第二:线段树如何处理?

倘若节点x(x为奇数)记录的是第1个点的数据,节点x+1记录的是第2个点的数据,那么节点x/2记录的就是区间[1,2]上的有效数据,以此类推,最顶端的父节点记录的就是区间[1,n]上的有效数据,那么对于每个点的数据,有且仅有logn个节点的数据会被它影响,因此每次更新只用更新logn个点,查询亦然,这样就有效地节约了时间。

对于每个节点,其代表的是区间[x,y]之间的值,那么其左儿子节点代表的就是[x,(x+y)/2]区间的值,右儿子节点代表的是区间[(x+y)/2+1,y]上的值,既保证了无重复,又保证了树的层数最短,查询效率最高。

第三:线段树的具体实现呢?

那么我们就跟着刚才拿到题目来详细讲解。

 

首先是建树,在这里num存的是下标,而le和ri表示的是这个区间的左右端点,那么每往下一层num*2,区间则折半,保证了最少的层数,而此时内存占用大约为4倍的点数,所以开数组的时候开tre[4*N]。这个题因为需要读入每个点,作为二叉树的先序遍历,很好地保证了第x个点正好读入在le=ri=x的那个tre[num]里面。而父亲节点所代表的区间包含了子节点所代表的区间,所以子节点的值又会影响父节点,因此每次建立完儿子节点之后,又会通过tre[num]=tre[num*2]+tre[num*2+1];操作将父亲节点初始化,当然此处为求和操作所以是+,不同的题可以选择取最值等不同运算符。当然不同的题根据需求可以采取对tre[num]赋值或者memset等方法来建树以及初始化。

 

int tre[N*4];
void build(int num,int le,int ri)//num是线段树下标,le,ri是区间的左右端点
{
    if(le==ri)
    {
        scanf("%d",&tre[num]);
        return ;
    }
    int mid=(le+ri)/2;
    build(num*2,le,mid);
    build(num*2+1,mid+1,ri);
    tre[num]=tre[num*2]+tre[num*2+1];//通过子节点初始化父亲节点
}

 

 

 

 

  接下来是修改操作,继承了上面的num,le,ri,保证了一致性,同时此处做的是对于第x个点增加y个人的操作,所以寻找到x所对应的tre[num],然后操作,并回退。而此时需要注意的是,对于x操作了之后,所有包含x的区间的tre[num]都需要被修改,因此也就有了在回退前的tre[num]=tre[num*2]+tre[num*2+1];操作。而这个题操作的是增加减少(减少直接传-x),而其他的诸如取最大最小值、取异或值等等都只用对于对应的运算符做修改即可。

  区间更新与单点更新最大的不同就是:它多了一个lazy数组!!!!!!!!!!重要的地方要打10个感叹号。

laz,全称lazy,中文叫懒惰标记或者延迟更新标记。

因为我们知道,如果我们每次都把段更新到节点上,那么操作次数和每次对区间里面的每个点单点更新是完全一样的哇!那么怎么办呢?仔细观察线段树,你会发现一个非常神奇的地方:每个节点表示的值都是区间[le,ri]之间的值有木有!!!!!!!!!!为什么说它神奇呢?更新的区间的值,存的区间的值!简直就是天作之合,我每次更新到对应区间了我就放着,我等下次需要往下更新更小的区间的时候,再把两次的值一起更新下去有木有啊!可以节约非常多时间啊有木有啊!

对,这就是laz[num]的作用。下面我们跟着题再来逐步感受。

首先在最最最最最最开始,是没有进行过更新操作的,那么laz[num]自然是全部置为0(当然有的题有额外的初始化要求,大家根据题目自行定夺)。

那么初始化结束之后,就开始更新操作。

 

一、单点更新

void update(int num,int le,int ri,int x,int y)//num是线段树下标,le,ri是区间范围,将位置x的值更新为y
{
    if(le==ri)
    {
        tre[num]+=y;
        return ;
    }
    int mid=(le+ri)/2;
    if(x<=mid)
        update(num*2,le,mid,x,y);
    else
        update(num*2+1,mid+1,ri,x,y);
    tre[num]=tre[num*2]+tre[num*2+1];
}

 

 

二、区间更新

 

void update(int num,int le,int ri,int x,int y)//更新区间[x,y]上的值,[le,ri]是当前正在更新的子区间
{
    if(x<=le&&y>=ri)
    {
        tre[num]++;
        laz[num]++;
        return ;
    }
    pushdown(num);
    int mid=(le+ri)/2;
    if(x<=mid)
        update(num*2,le,mid,x,y);
    if(y>mid)
        update(num*2+1,mid+1,ri,x,y);
}

 

三、懒人标记

void pushdown(int num)
{
    if(laz[num]!=0)
    {
        tre[num*2]+=laz[num];
        tre[num*2+1]+=laz[num];
        laz[num*2]+=laz[num];
        laz[num*2+1]+=laz[num];
        laz[num]=0;
    }
}

 

  最后是查询操作,依然继承了num,le,ri。而此处做的是区间查询,(其实如果x=y就成了单点查询)那么如果查询区间[x,y]包含了目前的区间[le,ri],即x<=le&&y>=ri,那么此时的tre[num]就已经是这一部分的有效数据了,所以直接return即可,否则继续分区间查询。同样,此时根据题意所做的求和操作可以对应替换为异或、取最值等操作。

 

int query(int num,int le,int ri,int x,int y)
{
    if(x<=le&&y>=ri)
    {
        return tre[num];
    }
    int mid=(le+ri)/2;
    int ans=0;
    if(x<=mid)
        ans+=query(num*2,le,mid,x,y);
    if(y>mid)
        ans+=query(num*2+1,mid+1,ri,x,y);
    return ans;
}

 

 

以上转载自:https://blog.csdn.net/zip_fan/article/details/46775633

posted @ 2019-07-28 21:26  知道了呀~  阅读(626)  评论(0编辑  收藏  举报