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

    对于提高组,线段树的重要性不言而喻

    什么是线段树?

    线段树是一颗二叉搜索树。之所以叫线段树,是因为线段树上每个节点维护的是序列的一段区间。

    线段树可以广泛地解决各种区间问题。相比于朴素算法(O(n^2))的复杂度,线段树可以在(O(nlogn))的复杂度下解决问题。

    线段树的结构

    线段树利用了分治的思想

    avatar

    对于一颗线段树

    每个节点维护一个闭区间([l,r])的信息,根节点维护([1,n])的信息

    如果(l=r)就是叶子节点;如果(l<r)就是内部节点,它有两个节点([l,frac{l+r}{2}])([frac{l+r}{2}+1,r])

    同时,我们设 1 为根节点的下标

    下标为(x)的左儿子的下标为(2x),右儿子为(2x+1)

    (sum[x])记录节点(x)代表的区间里所有数的和

    对于叶子节点,因为(l=r),所以(sum[x]=a[l])

    运用分治的思想,我们可以这样维护(sum[x])

    struct tree{
    	int l,r,sum;
    }c[N<<2];
    void update(int x)
    {
    	c[x].sum=c[x<<1].sum+c[x<<1|1].sum;
        //这里使用位运算速度更快,其中(x<<1)=(x*2),(x<<1|1)=(x*2+1)
    }
    

    在了解线段树的结构之后,不难想出,可以用简单的递归得到一颗初始的线段树。需要注意的是,线段树的数组要开到(4 imes n)的级别

    void build(int l,int r,int x)//l,r为当前节点所代表的区间,x为当前节点编号
    {
    	c[x].l=l,c[x].r=r;//记录区间
    	if(l==r){ //当前节点为叶节点
    		c[x].sum=a[l]; //直接赋值
    		return;
    	}
    	int mid=(l+r)>>1;
    	build(l,mid,x<<1); //构造左子树
    	build(mid+1,r,x<<1|1); //构造右子树
    	update(x); //更新sum值
    }
    

    线段树基本操作

    线段树的区间划分

    想要理解线段树修改和查询操作,首先要理解线段树是如何划分区间的

    划分([3,8])
    avatar
    划分([2,10])
    avatar

    对于被划分的区间,我们用一些尽量大的区间去表示它

    区间查询

    以查询([3,8])为例,我们只需要用的标黄的三个节点的信息

    我们需要从上到下的找出所查询区间的几个组成部分

    avatar

    假设询问区间是 ([A,B]),现在所在的节点表示的区间为([l,r])

    计算 (mid = frac{l+r}{2}),左子节点的区间为 ([l,mid]),右子节点的区间为 ([mid+1,r]).

    如果 (A leq mid),即询问区间与左子节点有重合,需要递归到左子节点。

    如果 (B geq mid + 1),即询问区间与右子节点有重合,需要递归到右子节点。

    递归完之后,需要把两个孩子询问的结果加起来作为返回值。

    代码实现:

    int query(int l,int r,int x)//l,r为询问区间,x为当前节点编号
    {
    	if(c[x].l>=l&&c[x].r<=r) // 已经是询问区间的子区间
    		return c[x].sum;
    	int mid=(l+r)>>1,ans=0;
    	if(l<=mid)ans+=query(l,r,x<<1); //查询左儿子
    	if(r>mid)ans+=query(l,r,x<<1|1); //查询右儿子
    	return ans;
            //此处ans已经是左右儿子之和,无需update
    }
    

    单点修改

    由于修改是对单个元素进行修改。

    比如修改第 (i) 个元素。

    我们先从上往下找到 ([i,i]) 所在的节点,然后修改它的 (sum),然后一路向上更新每个祖先的 (sum) 即可。

    void modify(int l,int r,int p,int z,int x) //l,r整个区间,p为需要修改的点,z为要改为的值 
    {
    	if(l==r){ //找到所求叶子节点 
    		c[p].sum=z;
    		return;
    	}
    	int mid=(l+r)>>1;
    	if(p<=mid)modify(l,mid,p,z,x<<1); //p在左子树 
    	else modify(mid+1,r,p,z,x<<1|1); //p在右子树 
    	update(x); //sum值改变且没有向上累加,需要update 
    }
    

    区间修改和延迟修改技术(Lazytag)

    如果把区间修改拆成 (r - l + 1) 个单点修改,效率非常的低。

    我们希望区间修改和区间查询一样,先把区间分成线段树上的若干个区间。然后分别修改这几个区间。

    avatar

    如果要将区间([3,8])都加上z

    把黄色的节点都打上一个tag,表示这个子树里都加上(z*(r-l+1))

    此时如果要访问到([5,5]),路上会碰到([4,5])

    ([4,5])的tag是z,说明子树内的值发生了改变,但是还没有应用更改

    此时将([4,5])的标记下传给它的左右儿子,并清空自身的标记,完成标记下传

    通过延迟修改操作,可以避免修改中多余的递归,复杂度降为(O(logn))

    void pushdown(int x)
    {
    	if(c[x].tag){ //如果有标记 
    		c[x<<1].sum+=c[x].tag*(c[x<<1].r-c[x<<1].l+1); //更新左儿子sum 
    		c[x<<1|1].sum+=c[x].tag*(c[x<<1|1].r-c[x<<1|1].l+1); //更新右儿子sum 
    		c[x<<1].tag+=c[x].tag; //标记下传 
    		c[x<<1|1].tag+=c[x].tag; //标记下传 
    		c[x].tag=0; //清空当前标记 
    	}
    }
    void change(int l,int r,int z,int x)
    {
    	if(c[x].l>=l&&c[x].r<=r){ //与区间查询类似 
    		c[x].sum+=z*(c[x].r-c[x].l+1);
    		c[x].tag+=z; //打lazytag 
    		return;
    	}	
    	pushdown(x); //检查标记并下传 
    	int mid=(c[x].l+c[x].r)>>1;
    	if(l<=mid)change(l,r,z,x<<1);
    	if(r>mid)change(l,r,z,x<<1|1);
    	update(x);
    } 
    

    使用lazytag后,区间查询也需要检查标记

    int query(int l,int r,int x)//l,r为询问区间,x为当前节点编号
    {
    	if(c[x].l>=l&&c[x].r<=r) // 已经是询问区间的子区间
    		return c[x].sum;
            pushdown(x); //检查标记并下传
    	int mid=(l+r)>>1,ans=0;
    	if(l<=mid)ans+=query(l,r,x<<1); //查询左儿子
    	if(r>mid)ans+=query(l,r,x<<1|1); //查询右儿子
    	return ans;
            //此处ans已经是左右儿子之和,无需update
    }
    

    一些例题

    P3372 线段树1

    线段树裸题,考察区间修改和区间查询

    P3373 线段树2

    线段树维护多个tag

    GSS系列

    SPOJ的一系列线段树好题,是线段树的综合应用,这里放出我在洛谷上的题单

  • 相关阅读:
    加密CMD使电脑溢出也拿不到CMD权限
    全面提升Linux服务器的安全
    ping 源码,详细解释
    伤心一百回
    聊聊我对黑客技术的思考
    一个网管员的真实成长经历
    通过命令限制上网用户的权限
    防范黑客的简单办法
    “黑客”人生
    黑客现身讲话
  • 原文地址:https://www.cnblogs.com/Wuhen-GSL/p/13571485.html
Copyright © 2011-2022 走看看