数据结构:线段树

@

前言

  最近几天一直在线段树上面死磕,现在大概弄懂了较简单的线段树(只支持加减法的那种),来这里写篇大约的笔记,等以后学习了更多还会更新。


什么是线段树

  线段树是一种二叉树,每个节点都代表一个区间的和(或者最大值等等,可以自己定义。以下用和作为例子)。比如说根节点就代表整个区间的和,下图展示了一棵完整的线段树在这里插入图片描述
  在这张图中,原始序列是\(1,2,3,4\),上面的树形结构就是根据这个数组构建出来的一棵线段树。
  根节点的下标为\(1\),第\(i\)个节点的两个儿子的下标分别为\(i*2\)\(i*2+1\),叶节点没有儿子。每个节点的\(l\)\(r\)表示这个节点所表示的区间的左右端点,\(data\)表示这个区间中所有数的和。可以发现,一个父节点就等于两个子节点的和(当然只是对于表示和的线段树而言),根节点就代表整个数组的和。
  了解了线段树是什么后,我们就来了解一下线段树的基本操作。


线段树的基本操作

\(sco\)为数据范围
大家也可以根据各自的喜好使用不同的变量名。
要注意的是线段树要开四倍大小,不然会数组越界。

怎么建造一棵线段树

线段树的结构体

  如上一章所说,每个节点有三个变量分别为\(l,r,data\)。实际上还有一个变量叫做lazy,这个后文再进行介绍。所以说结构体应该这样写:

struct tree{
	ll l,r,data,lz;
}tr[sco*4];

数据更新函数

  前文说过,当前节点当然就是左右子节点的和嘛,作者习惯将它写成一个函数,看起来简洁,要修改时也容易,只要修改一处。

void updata(int i){
	tr[i].data=tr[i*2].data+tr[i*2+1].data;
}

  建树的过程可以分为三步,首先判定当前位置是不是叶子节点,是的话就赋值,否则继续建立左右子树,这个过程可以用一个递归函数实现:

void build(int i,int l,int r){
	tr[i].l=l,tr[i].r=r;//确定左右区间端点
	if(l==r){//判断是否为叶节点
		tr[i].data=a[l];return;//是叶节点就赋值,a数组为原始序列
	}
	int mid((l+r)>>1);//区间的中间下标
	build(i*2,l,mid);//建立左子树
	build(i*2+1,mid+1,r);//建立右子树
	updata(i);//更新当前节点
}

  \(i\)值初始为\(1\),所以在主函数里面调用时只要写成\(build(1,1,n)\)就好了(假定原始序列左端点为\(1\),右端点为\(n\))。

单点修改与区间查询

单点修改

  这个挺简单的,只要从根节点开始,判断要找的节点的下标在左子树还是右子树,直到找到该节点,然后修改完以后回溯,顺路把沿途的节点重新更新一下就好了。

void modify(int i,int dis,int k){
	if(tr[i].l==tr[i].r){//如果当前为叶节点,说明找到了
		tr[i].data+=k;return;//修改值然后返回
	}
	if(dis<=tr[i*2].r)modify(i*2,dis,k);//如果要修改的点在左子树中,就递归左节点
	else modify(i*2+1,dis,k);//否则就在右子树中,递归右节点
	updata(i);//修改完后更新当前节点
}

区间查询

  对于每个节点,判断要查询的区间对于当前节点的左右区间是否有交集,有的话就递归,如果当前区间是要查询区间的子集,直接加上去即可,如果完全不相交,直接跳过。

int search(int i,int l,int r){
	if(l<=tr[i].l && tr[i].r<=r){//如果是子集
		return tr[i].data;//直接全部累加
	}
	if(tr[i].l>r || tr[i].r<l)return 0;//如果完全不相交,直接跳过
	int s(0);//等价于 int s=0;
	if(tr[i*2].r>=l)s+=search(i*2,l,r);//累加左子树的寻找结果
	if(tr[i*2+1].l<=r)s+=search(i*2+1,l,r);//累加右子树的寻找结果
	return s;//返回结果
}

区间修改

第四个节点变量:\(lazy\)

为什么要用\(lazy\)

  为什么不能直接修改?为什么会有个\(lazy\)
  假如我们要修改\(1 \sim 3\)区间,直接的思想就是跟区间查询一样,子集就直接加,不相交就跳过,交集就继续递归。
  但这为什么是错的?
  譬如原始序列是\(1,2,3,4\),我们要把\(1 \sim 3\)这个区间每个区间加上\(2\),修改完后再查询\(2 \sim 4\)的区间,过程如下图
请添加图片描述
  红色为区间修改,紫色为查询。得到的答案是\(9\),可是实际上:
  \(1,2,3,4\)
  \(1 \sim 3\)每个\(+2\)
  \(3,4,5,4\)
  查询\(2 \sim 4\)的和
  \(4+5+4=13\)
  这与我们使用线段树求出的结果不一致,原因在于哪里?在于我们区间修改\(1 \sim 2\)时只修改了节点\(2\)的值,但是节点\(4\)\(5\)没有任何变化,所以在查询节点\(5\)时就得到了错误的答案。所以我们需要用\(lazy\)来填补这个错误。

\(lazy\)的作用

  在区间修改到一个即将被修改节点时(路上经过的节点不算),打上标记,在查询到它时把这个标记向下推动,在保持时间复杂度的同时又能确保正确性。

void down(int i){
	if(tr[i].lz!=0){
		tr[i*2].lz+=tr[i].lz;//lazy传给左节点
		tr[i*2+1].lz+=tr[i].lz;//lazy传给右节点
		tr[i*2].data+=tr[i].lz*(tr[i*2].r-tr[i*2].l+1);
		tr[i*2+1].data+=tr[i].lz*(tr[i*2+1].r-tr[i*2+1].l+1);
		//因为是这个区间每个数同时+k,所以data要加上(这个区间的长度*父节点的lazy);
		tr[i].lz=0;//父节点lazy处理完毕,归零
	}
	updata(i);//更新父节点
}

  下图即为上文的例子加上lazy后的演示:

请添加图片描述
  红色为区间修改,紫色为查询,绿色为\(lazy\)下传的过程。得到的答案是\(13\),正确。
  在查询和修改时都要pushdown哦。

区间修改

void modi(int i,int l,int r,int k){
	if(l<=tr[i].l && tr[i].r<=r){//如果是子集,直接用该节点累加
		tr[i].data+=(tr[i].r-tr[i].l+1)*k;//加上区间长度乘乘k的值
		tr[i].lz+=k;//lz加上k
		return;
	}
	down(i);//lz下传
	if(tr[i*2].r>=l)modi(i*2,l,r,k);//如果该区间与左节点有交集,递归
	if(tr[i*2+1].l<=r)modi(i*2+1,l,r,k);//如果该区间与右节点有交集,递归
	updata(i);
}
//将l~r每个数全部加上k

区间查询

int search(int i,int l,int r){
	if(l<=tr[i].l && tr[i].r<=r){//如果是子集,直接累加
		return tr[i].data;
	}
	if(tr[i].l>r || tr[i].r<l)return 0;//如果没有相交,返回0
	down(i);//lz下传
	int s(0);
	if(tr[i*2].r>=l)s+=search(i*2,l,r);//如果该区间与左节点有交集,累加递归
	if(tr[i*2+1].l<=r)s+=search(i*2+1,l,r);//如果该区间与右节点有交集,累加递归
	return s;
}
//求l~r的和

写在最后

作者还没有学习支持乘法运算的线段树,会尽快学习,然后再来更新。有任何问题欢迎在下方评论,谢谢阅读——

posted @ 2021-08-18 22:01  ssl_lhj  阅读(51)  评论(0)    收藏  举报