• 【Study】线段树


    这几天终于认真搞懂了线段树,就赶紧生产了篇学习笔记来热热知识。(我真的好菜啊)

    线段树呢,是一种很有用的数据结构(点头),对于一些区间查询、修改都能方便实现,还有好多好多的奇异功能(哇),也是学习一些高级du liu数据结构的基础,对于一名OIer还是 有认真学习的必要的。

    线段树是一种二叉搜索树,什么叫做二叉搜索树,首先满足二叉树,每个结点度小于等于二,即每个结点最多有两颗子树,何为搜索,我们要知道,线段树的每个结点都存储了一个区间,也可以理解成一个线段,而搜索,就是在这些线段上进行搜索操作得到你想要的答案。

    我们先来看看这么一个栗子:

    这是一个比较简单的求区间最大值的线段树。每个节点红色部分是这个节点所维护的区间,蓝色部分是这个区间里的最大值,并且每个叶子结点的值就是原数组的值。

    所以我们可以发现,每个非叶子结点的度都为二,且左右两个孩子分别存储父亲一半的区间。每个父亲的存储的值也就是两个孩子存储的值的最大值。

    明白了上面这个道理,我们接下来就捋一捋每个节点如何来储存区间,还有如何快速找到非根节点的父亲和非叶子节点的儿子。

    对于线段树的储值,我们一般不会去记录l和r的值,看一下上面的图,我们可以发现,根节点记录的是整个数组的最大值,它的两个儿子分别记录的是 1--3 和 4--6 的值,仔细看,你是不是想到什么?

    没错,就是二分。当根节点记录的是 1--n 的值时,它的左儿子就记录的是区间 1--(1+n)/2 的值,右儿子就记录的是区间 (1+n)/2+1--n 的值,以此类推,根节点的左右儿子也是如此,就这样分到 l=r ,此时就是叶子节点了,也就可以将其赋值。

    赋值之后呢,我们现在只给叶子节点赋值而已,那其他的节点呢?

    所以,我们通常使用 递归 来实现整棵树的修改,只要不断地递归左右子树,直到 l=r 停止,返回的时候顺便进行修改即可。

    好的,到这里,如果你捋清楚了如何进行整棵树的修改,那么我们接下来就来琢磨琢磨如何来给每个节点标一下下标。

    下标可是很重要的。

    对于上述线段树,我们增加绿色数字为每个结点的下标。

    “ 1、2、3、4、5、6、7、8、9、12?!! ”

    “ 怎么直接从 9 跳到了 12 ? ”

    “ 噢!原来中间其实是有两个空间的呀! ”

    虽然没有用到,但是一定要开,因为你不会知道这 5 这个节点到底是不是叶子节点,所以以防万一。这也是无优化线段树的缺点,空间好大,常M别怕

    一般没有优化过的线段树,是需要 (2 * 2^{k} ( 2^{k-1} < n < 2^k)) 个空间的,所以一般会开到 4×n 的空间防止RE。

    仔细观察上图,我们应该不难发现,左儿子的下标是父亲下标的两倍,而右儿子的下标就是左儿子的再加上1。

    总结一下:

    • l = x × 2

    • r = x × 2 + 1

    (x是父亲的下标,l是左儿子的下标,r是右儿子的下标)

    具体证明也不太难,请大佬自行百度,这里不做细讲

    一般,我们常用位运算来计算:

    • l = x << 1

    • r = x << 1 | 1

    好了,讲了那么久,终于能开心地打一打代码了!

    #define ls (x<<1)
    #define rs (x<<1|1)
    #define mid (l+r>>1)
    #define MAXN 50010
    
    int a[MAXN],t[MAXN<<2]; //a为原来区间,t为线段树
    
    void pushup(int x) //更新函数,这里是实现最大值,同理可以变成最小值,区间和等
    {
        t[x]=max(t[ls],t[rs]);
    }
    
    //递归方式建树 build(1,n,1);
    void build(int l,int r,int x) //l表示当前区间的左端点,r表示当前区间的右端点,x表示当前结点的下标
    {
        if(l==r) //左端点等于右端点,即为叶子节点,直接赋值即可
        {
        	t[x]=a[l];
        	return ;
        }
        build(l,mid,ls); //递归构造左儿子结点
        buile(mid+1,r,rs); //递归构造右儿子结点
        //这里的mid是l和r的中点,目的是为了将l--r分为两个区间
        pushup(x); //更新操作
    }
    
    

    下面我们就来讲一下区间的查询修改和单点的修改。

    区间修改,顾名思义,对某个区间的值进行更新。那么要如何更新呢,可以想一想,其实和建树是差不多的(点头)

    (Three minutes later~)

    好,其实我们可以发现,上面的建树,换个说法,就是对整个区间的更新,那么为了可以实现其中一部分区间的更新,我们需要在上面的建树代码中,当前节点往下跑的方向给一些限制。

    举个例子,我们要将 [ 3 , 5 ] 这个区间的每一个值改为5,如下图。

    其实我们可以发现,每次对于向下跑的方向,我们可以根据现在左右儿子管理的区间,里面有没有包含要修改的区间,有的话就可以朝着那个方向继续跑下去。

    刚刚在 build 函数里面,有这么两个变量—> l和r,这两个变量的意思就是当前节点管理的区间范围,我们可以将最左(l)和最右(r)分别与要修改区间的左和右作比较就行了,到最后一定会跑到一个要修改的点上。

    口胡大师当然是不行的,所以现在我们来动手操作一下:

    #define ls (x<<1)
    #define rs (x<<1|1)
    #define mid (l+r>>1)
    #define MAXN 50010
    
    int a[MAXN],t[MAXN<<2]; 
    
    void pushup(int x){t[x]=max(t[ls],t[rs]);}
    
    //同样用递归的方式修改区间
    void updata(int L,int R,int v,int l,int r,int x)
    //L,R表示修改区间的左右端点,v表示要修改的数值
    //l,r,x的含义见build
    {
    	if(l==r)
    	{
    		t[x]=v;//将第x个数修改为v
    		return ;
    	}
    	if(L<=mid)
        //左儿子管理的区间有包括部分要修改的区间
    		updata(L,R,l,mid,ls(x));
    	if(R>mid)
        //右儿子管理的区间有包括部分要修改的区间
    		updata(L,R,mid+1,r,rs(x));
    	pushup(x);
    }
    

    如果还不太懂的话,可以自己画图模拟一下(汗)。

    然后对于单点修改,其实也是差不多的。因为修改的只有一个点,所以,对于当前非叶子结点,如果左儿子管理的区间没有包括这个节点的话,就一定在右儿子那边。

    (丢你代码(大雾

    void updata(int i,int v,int l,int r,int x)
    //i表示要修改的节点
    {
    	if(l==r)
    	{
    		t[x]=v;//将第x个数修改为v
    		return ;
    	}
    	if(i<=mid)
    		updata(i,v,l,mid,ls);
    	else
    		updata(i,v,mid+1,r,rs);
    	pushup(x);
    }
    

    好的,现在我们已经完成了单点/区间修改,如果你已经完全理解了,那么对于接下来区间查询就不会太难理解了。
    (单点修改的话直接找a数组查询,记得在修改的时候带上a数组qwq)

    (话说哪个出题人那么无聊出单点修改)(大雾)

    对于区间查询,其实和区间修改差不多,至少精髓是一样的(话说这不废话吗)

    假如我们要查询 [ 2 , 5 ] 的最大值,就像下面这样

    仔细瞧,说是一共要查询2、3、4、5这四个节点,其实我们只要查询 [ 2 , 2 ] 、 [ 3 , 3 ] 、[ 4 , 5 ] 这三个区间的最值,然后再来进行比较,就可以缩短一部分查询的时间,对于大数据的话就不止一点了(排开某些毒瘤数据、最坏情况),所以这也是线段树的其中一处优越。

    大家要先把思路捋明了,代码也就清晰了(温馨提示(大雾

    int query(int L,int R,int l,int r,int x)
    //几个变量含义和updata的一样qwq
    {
    	if(L<=l&&r<=R) return t[x];
       //如果要查询的区间包括当前的区间,就直接返回当前区间的最大值
    	ll ans=-19260817;
       //一般这里用long long,以防万一
    	if(L<=mid)
    		ans=max(ans,query(L,R,l,mid,ls));
    	if(R>mid)
    		ans=max(ans,query(L,R,mid+1,r,rs));
       //此处两个if和updata的用意一样
    	return ans;
    }
    
    

    读到这,恭喜你,你已经掌握了最基本的线段树了(大佬orz)

    然后你就迫不及待地去切题了。

    P3372 【模板】线段树 1

    嗯?模板?你怒打了一棵线段树,交了上去。

    qwq

    什么呀,怎么还有3个黑色的东西(大雾),然后你就仔细地调自己代码,看看有哪里写的不一样。

    其实,线段树对于查询修改的复杂度都是 O(logN) , 但是上文提过,有些毒瘤出题人,弄了些最坏情况,把线段树的查询修改卡到 O(NlogN) (比如让你对所有区间进行修改),那么现在的话就比暴力还慢了,所以聪明的人们发明了 lazy_tag (懒标记),顾名思义,就是在更新区间的时候偷懒,如何偷懒呢,且听我一一道来。

    通过上面对区间修改的讲解,我们可以发现,每次修改的时候总是要跑到叶子节点后才来进行修改,然而这样会比直接暴力扫区间还慢。我们是不是可以不用跑到叶子节点来修改区间呢?

    当我们从根节点往下跑的时候,如果这个非叶子节点管理的区间包含在修改区间的范围内时,我们可以直接对这个节点进行修改,然后开一个数组标记修改的值,这个数组称为lazy数组,我们已经将这部分区间修改,接着就可以直接返回了。

    如果当我们需要更下面的节点怎么办?我们只修改了下面节点的父节点,并没有将下面的节点更新。此时我们刚刚开的那个lazy数组就起作用了。

    我们可以利用父节点的lazy值,来修改子节点的值,然后将子节点的lazy值改为父节点的lazy值,接着把父节点的lazy值清零就行了。

    (说白了就是将lazy下传)

    好了,如果现在已经理解lazy_tag的作用和实现方法的话,就来coding一下qwq

    (这里我们以刚才的模板为例子qwq)

    void pushdown(ll i,ll k)//将lazy值下传
    //i表示当前节点,k表示i节点管理区间长度
    {
    	if(lazy[i])//如果有lazy值的话
    	{
    		lazy[ls(i)]+=lazy[i];
    		lazy[rs(i)]+=lazy[i];
            //将lazy值赋给左右子节点
    		t[ls(i)]+=(k-(k>>1))*lazy[i];
            //因为左子节点里面有(k-k/2)个叶子节点,每个叶子节点都要加上lazy值
    		t[rs(i)]+=(k>>1)*lazy[i];
            //同上
    		lazy[i]=0;
            //修改完之后就要把lazy值归0,否则会重复计算
    	}
    }
    

    因为这是在修改是的lazy,所以updata也要相对应进行修改。

    void updata(int L,int R,int v,int l,int r,int x)
    //L,R表示修改区间的左右端点,v表示要修改的数值
    {
    	if(L<=l&&r<=R)//如果要修改的区间包含当前区间
    	{
    		lazy[i]+=v;
    		t[i]+=v*(r-l+1);
            //思路和pushdown一样
            return ;
    	}
        pushdown(x,r-l+1);//因为下面的节点可能之前没有修改过,所以我们要pushdown一下
    	if(L<=mid)
        //左儿子管理的区间有包括部分要修改的区间
    		updata(L,R,v,l,mid,ls(x));
    	if(R>mid)
        //右儿子管理的区间有包括部分要修改的区间
    		updata(L,R,v,,mid+1,r,rs(x));
    	pushup(x);
    }
    

    同样的,query也要进行相对应的修改

    int query(int L,int R,int l,int r,int x)
    {
    	if(L<=l&&r<=R) return t[x];
        pushdown(x,r-l+1);//和updata同样,查询之前先更新
    	ll ans=-19260817;
    	if(L<=mid)
    		ans=max(ans,query(L,R,l,mid,ls));
    	if(R>mid)
    		ans=max(ans,query(L,R,mid+1,r,rs));
    	return ans;
    }
    
    

    恭喜你,到这里你就已经掌握一个线段树的基本写法了,现在你就可以开心切题了!

    P3372 【模板】线段树 1

    P2357 守墓人

    P3372 【模板】线段树 2

    线段树只是一种较基础的数据结构,对于一些关于线段树的 高级算法,等以后有时间再讲吧qwq(


    写在最后:

    害,这次的文章咕了好久,咕了差不多大半年的时间了(sta-> 3-29 / fin-> 8-02),剩下的lazy_tag还是在jzsc的闲余时间完善的,才发现我差不多都完干净了qwq。其实我写这篇博客主要是为了给以后自己复习用 (翻译:我太菜了),如果说,这篇博客可以帮助到想学线段树的大佬的话,我是非常高兴的,我将一些较难懂的点给简单化详细化 (话说好像没什么难懂的点(大雾 ),所以对于像我一样的蒟蒻还是挺容易理解的qwq

    参考资料:线段树详解 - Xenny

  • 相关阅读:
    FastAdmin CMS 插件下载
    使用 Python 连接到 PADS Layout
    Javascript undefined 和 null
    反馈给又拍云需要以下信息
    Web前端性能优化之图片优化
    nodejs--实现跨域抓取数据
    HTML6 展望
    cSS3 伪类:nth-child 的使用方法
    css3中的几何图形shape研究
    iScroll5 API速查随记
  • 原文地址:https://www.cnblogs.com/zhangjiayang/p/13463702.html
Copyright © 2020-2023  润新知