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

     


    线段树线段树,这种数据结构的神奇之处就在线段二字上——比较擅长处理区间问题。
    现在假设有一个背景:我们有一个数列,需要知道从第i个到第j个的和(或者最大数、最小数等等,根据问题而定,这里仅仅是为了引入线段树)。并且这个数列是动态的,我们会随时对某一点或者某一个区间的数值进行改变(这样就不能狗使用静态的前缀和解决问题了,显然会爆)。
    如果没有线段树这种数据结构解决这个问题我觉得还是相当棘手的(不知道哪个大神想出来的,真是厉害啊)。
    我们用一个数组存储每个区间的结果 ,这样再查询的时候只需要查询这个区间而不必要去查找每一个元素,而在修改的时候也是修改整个区间(这个还是比较有技巧的,后面会讲如何巧妙实现的)
    我们每次将区间分成两半进行储存,经过观察可以得出父区间k和左子区间以及右子区间的坐标关系为k * 2和k * 2 + 1的关系(如果不理解可以自己画一个二叉树看一下就很容易理解了)。
    我们首先来建树

    struct node
    {
    	int l,r;
    	int sum; //或者是max min等等,这个就是表征区间的数值 
    }tree[maxn<<2];
    void build(int k,int l,int r)
    {
    	tree[k].l=l; tree[k].r=r;
    	if(l==r)
    	{
    		scanf("%d",&tree[k].sum);//叶节点就是元素本身 
    		return;
    	}
    	int mid=(l+r)>>1;//将区间分成两半 
    	build(k<<1,l,mid);//这里用到了位运算,等价于k*2 
    	build(k<<1|1,mid+1,r);//等价于k*2+1,这样写觉得比较高级,嘿嘿
    	tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
    	//最后一步非常重要,相当于从叶节点回溯表征父区间的值 
    }
    

    为什么要开四倍空间?好像是有证明的。反正得注意!!!
    其实为了节省空间存储区间左右端点的量是可以省略的,但是刚开始接触还是写上有利于理解,到后面就可以省略。
    假如我们想要查询某一个点的状态,我们就按照区间进行查询,最后一直搜索到叶节点就好了(有点像二分)。
    代码如下:

    int search_point(int k,int x)
    {
    	if(tree[k].l==tree[k].r && tree[k].l==x)
    	{
    		return tree[k].sum;
    	}
    	int mid=(tree[k].l+tree[k].r)>>1;
    	if(x<=mid) return search_point(k<<1,x);
    	else if(x>mid) return search_point(k<<1|1,x);
    }
    

    那如果我i们想查询某个区间的和(值)呢?首先,我们应该理解任何一个区间都可以用我们存储的线段树的某些区间组成,因此我们只需要找到这些区间并把值返回就可以了(根据想要的值的不同返回方式可能有些许不同)
    如果我们当前区间在我们找的区间的内部,那么可以直接进行返回,否则将当前区间分成两部分,如果目标区间存在一部分在当前区间那么就进行搜索,总会找到刚好处于目标区间的部分(而且他们之间是不重合的,组合起来刚好是目标区间,不用担心漏掉某些部分)。
    代码如下:

    int search_ interval(int k,int x,int y)
    {
    	if(x<=tree[k].l && tree[k].r<=y)
    	{
    		return tree[k].sum;
    	}
    	int mid=(tree[k].l+tree[k].r)>>1;
    	int ans=0;
    	if(x<=mid) ans+=search_interval(k<<1,x,y);
    	if(y>mid) ans+=search_interval(k<<1|1,x,y);
    	return ans;//根据需要的值的不同返回方式可能不同 
    }
    

    但是除了查询,我们还得动态的修改元素的信息,这个如何做到呢?
    修改单点元素的值和查找单点元素的值差不多,差距只不过是一个是找到返回,一个是修改元素,但是需要考虑的是修改叶节点对父区间的影响。
    代码如下:

    void change_point(int k,int x,int y)//将x点的值修改为y
    {
    	if(tree[k].l==tree[k].r && tree[k].l==x)
    	{
    		tree[k].sum=y;
    		return;
    	}
    	int mid=(tree[k].l+tree[k].r)>>1;
    	if(x<=mid) search_point(k<<1,x);
    	else if(x>mid)  search_point(k<<1|1,x);
    	tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; //修改叶节点后对父节点的影响 
    } 
    

    比较复杂的就是区间修改了,很容易理解修改一个区间的复杂度显然会很大,因此不知道哪位牛人想出来一个很牛逼的方法——lazy标记(其实还是挺自然的,之所以修改一个区间的复杂度很高是因为区间很多,一个一个修改会很麻烦,如果引入一个标记先将需要修改的区间一改,然后不处理子区间,等到需要用到子区间的时候再进行修改,修改只是顺手的事,这样就能大大提升效率)
    比如说我们修改了一个父区间,然后将他的值一改,再添加lazy标记,然后再访问其子区间的时候再根据父区间的lazy标记考虑以前对这个区间的改变,并且消除父区间的lazy标记。需要注意的是要注意父节点的lazy标记是叠加还是消除(可能比较抽象,先将就理解,然后多敲多想,慢慢就理解了,很多问题都是这样,如果想要先完全将问题弄清楚再进行实践黄花菜都凉了,刚开始的时候多进行模仿,然后一直思考问什么要这样做,然后再不断理解,这样形成一个正反馈就能理解了——也可能是因为我理解能力比较差所以想出来这样一个办法,哈哈,题外话了)
    而且因为加上了lazy标记的缘故之前所有操作在进行时都得加上一个pushdown 的操作(因为可能有的区间的值还没有改过来,所以要用到的时候要进行修改,这就是养兵千日,用兵一时,哈哈)
    这里贴上完整的已经优化过一定空间的加上区间修改的代码:(因为文章是一口气打出来的,可能代码有小小错误,我回头会再看的)

    struct node
    {
    	int lazy; 
    	int sum; //或者是max min等等,这个就是表征区间的数值 
    }tree[maxn<<2];
    
    void pushdown(int k,int l)
    {
    	tree[k<<1].lazy+=tree[k].lazy; //这里因为lazy标记的影响是叠加的,所以注意+= (如果是求最大值等就不用等号,即现在状态会覆盖原来状态)
    	tree[k<<1|1].lazy+=tree[k].lazy;
    	tree[k<<1].sum+=tree[k].lazy*(l-(l>>1));
    	tree[k<<1|1].sum+=tree[k].lazy*(l>>1);
    	tree[k].lazy=0; 
    }
    void build(int k,int l,int r)
    {
    	if(l==r)
    	{
    		scanf("%d",&tree[k].sum);//叶节点就是元素本身 
    		return;
    	}
    	int mid=(l+r)>>1;//将区间分成两半 
    	build(k<<1,l,mid);//这里用到了位运算,等价于k*2 
    	build(k<<1|1,mid+1,r);//等价于k*2+1,这样写觉得比较高级,嘿嘿
    	tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
    	//最后一步非常重要,相当于从叶节点回溯表征父区间的值 
    }
    int search_point(int k,int x,int l,int r)
    {
    	if(l==r && l==x)
    	{
    		return tree[k].sum;
    	}
    	if(tree[k].lazy) pushdown(k,r-l+1);
    	int mid=(l+r)>>1;
    	if(x<=mid) return search_point(k<<1,x,l,mid);
    	else if(x>mid) return search_point(k<<1|1,x,mid+1,r);
    }
    int search_ interval(int k,int x,int y,int l,int r)
    {
    	if(x<=l && r<=y)
    	{
    		return tree[k].sum;
    	}
    	if(tree[k].lazy) pushdown(k,r-l+1); 
    	int mid=(l+r)>>1;
    	int ans=0;
    	if(x<=mid) ans+=search_interval(k<<1,x,y,l,mid);
    	if(y>mid) ans+=search_interval(k<<1|1,x,y,mid+1,r);
    	return ans;//根据需要的值的不同返回方式可能不同 
    }
    void change_point(int k,int x,int y)//将x点的值修改为y
    {
    	if(l==r && l==x)
    	{
    		tree[k].sum=y;
    		return;
    	}
    	if(tree[k].lazy) pushdown(k,r-l+1); 
    	int mid=(l+r)>>1;
    	if(x<=mid) search_point(k<<1,x,l,mid);
    	else if(x>mid) search_point(k<<1|1,x,mid+1,r);
    	tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum; //修改叶节点后对父节点的影响 
    } 
    void change_interval(int k,int x,int y,int z,int l,int r)
    {
    	if(x<=l && r<=y)
    	{
    		tree[k].lazy+=z;
    		tree[k].sum+=(r-l+1)*z;
    		return;
    	}
    	if(tree[k].lazy) pushdown(k,r-l+1); 
    	int mid=(l+r)>>1;
    	if(x<=mid) search_point(k<<1,x,y,l,mid);
    	if(y>mid) search_point(k<<1|1,x,y,mid+1,r);
    	tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
    }
    

    按道理这样已经可以解决很多问题了,但是当数据很大的时候就不能处理了,需要进行离散化什么的,好像还可以进一步优化空间,以后会再更的,嘿嘿。

  • 相关阅读:
    Windows 8将替换Win32 API
    密码强度检测:passwordStrength
    整数溢出与程序安全
    编程经验谈:如何正确使用内存
    C/C++指针学习的两个经典实例
    VC调试入门
    一些电子书籍的网站
    BMP文件格式分析(zz)
    C/C++ 跨平台I/O操作技巧
    Windows下C语言网络编程快速入门
  • 原文地址:https://www.cnblogs.com/qgmzbry/p/10662101.html
Copyright © 2011-2022 走看看