线段树入门

线段树线段树,这种数据结构的神奇之处就在线段二字上——比较擅长处理区间问题。
现在假设有一个背景:我们有一个数列,需要知道从第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 l,int r,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) change_point(k<<1,l,mid,x,y);
	else if(x>mid) change_point(k<<1|1,mid+1,r,x,y);
	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) change_interval(k<<1,x,y,l,mid);
	if(y>mid) change_interval(k<<1|1,x,y,mid+1,r);
	tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}

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

一开始一直觉得离散化难以理解,其实是没有静下心来去理解,其实很简单,我们数据的范围是很大的,但是我们的数字个数是有限的,这就保证了很多数据范围内的数字都不会出现,如果我们对不会出现的数字还进行维护的话是不必要的,为此,我们将数组排序,去重,然后将每一个数字在这个没有重复元素的数组中的位置作为这个数字新的值,这样需要维护的区间就小了很多。如上面所说,我们维护的是数值区间,不在是物理上的数组区间,因此离散化常常用在权值线段树中。权值线段树的意思就是说线段树保存的是数字出现次数(也就是权值,也可能是其他的值,但一般都是指数字出现的个数),权值线段树的根节点保存的区间范围就是数据范围,而进行离散化以后数据范围已经大大缩小,就能进行维护了。
有的时候数据范围很大的时候我们不一定一定要盯着权值线段树+离散化不放,我们还可以用map数组+树状数组方便的进行维护,例如CodeForces - 641ELittle Artem and Time Machine——map+树状数组

单个的权值线段树很少见,更多的是在主席树里面用到这个,如果有兴趣可以移步我之前写的博客主席树入门,当然你也可以在网上进行百度,资源很多。

线段树的运用很灵活,需要维护的东西很多的时候不同lazy标记之间的关系也很麻烦,比如HDU - 4578Transformation——线段树+区间加法修改+区间乘法修改+区间置数+区间和查询+区间平方和查询+区间立方和查询,需要对线段树有一个深刻的理解才能面对复杂多变的问题。
就酱

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值