目录
前言
在之前的学习中,关于数组维护,我们已经学习了前缀和,差分以及树状数组,对于不同的体型我们可以运用不同的数据结构来进行维护,今天我们来讲一个更为通用的数据结构–线段树
线段树
1.1 什么是线段树
线段树(Segment Tree) 是一种基于分治思想的二叉树结构(问什么是二叉树请百度),相比于树状数组里运用二进制分解来储存区间,线段树是将一个线性结构(比如数组)表示为一棵树,每个节点表示一个区间,叶节点来表示单个元素,线段树更易理解也更为通用,被广泛使用于算法竞赛中,唯一的缺点可能是他的代码量相当大…
1.2 原理与操作
线段树的主要思想是将原始数据分成若干区间,并用这些区间去构建一颗二叉树,每个节点代表一个区间,包块左右边界和区间内的信息(比如最大值,最小值,区间和等等)
线段树通常用于解决区间查询问题,如查询区间最值,区间和,他还支持区间更新操作,比如更新区间所有元素。
线段树的构建和查询操作的时间复杂度均为O(logN),其中N为原始数组的长度,因此线段树是一种高效的数据结构,对于需要频繁操作的题目有着相当高的优先级。
线段树支持以下操作:
1.单点修改
2.单点查询
3.区间修改
4.区间查询
1.3 线段树的性质
在讲操作实现我们先来看看线段树的构成具有什么性质,来帮助我们更好地理解各个操作的实现
1.线段树每个节点都代表一个区间
2.线段树具有唯一的根节点,代表的区间是整个统计范围, 如 [ 1 , N ]
3.线段树的每个叶节点都代表一个长度为 1 的 “元区间” 如 [ x , x ]
4.对于每个内部节点 [ l , r ] ,它的左子节点是 [ l , mid ] , 右子节点是 [ mid+1 , r ] 其中 mid = ( l + r )/ 2(向下取整)
所以对于一棵线段树,除去树的最后一层,整颗线段树一定是一棵完全二叉树,树的深度为O(logN),因此我们可以按照与二叉堆类似的方式编写节点;
1.根节点编号为1
2.编号为x的节点的左子节点编号为x2 ,右子节点编号为 x2+1
操作
2.1 线段树的创建 build
struct SegmentTree{
int l,r,w;
}tree[N*4];
int a[N];
void build(int p,int left,int right){
tree[p].l=left;
tree[p].r=right;
if(left==right){
tree[p].w=a[left];
return ;
}
int mid=(left+right)/2;
build(p*2 , left , mid);
build(p*2+1 , mid+1 , right);
tree[p].w=max(tree[p*2].w , tree[p*2+1].w);
//记录最值,从下往上传递;
//举一反三,我们同样可以在结构体中定义如.tot 用于记录区间和
}
2.2 线段树的单点修改
单点修改是一条形如 “ c x v ” 的指令,表示把 a[ x ] 的值修改为 v
在线段树中,根节点是执行各种指令的入口。我们需要从根节点出发,递归找到区间 [ x , x ] 的叶节点,然后从下往上更新它以及所有包含它的父节点,时间复杂度为O(logN)
void change(int p,int x,int v){
if(tree[p].l == tree[p].r){
tree[p].w= v;
return ;
}
int mid = (tree[p].l + tree[p].r)/2;
if(x < mid) change(p*2,x,v);
else change(P*2+1,x,v);
tree[p].w = max(tree[p*2].w , tree[p*2+1].w);
//从下向上更新
}
2.3 线段树的区间查询
区间查询是一条形如“ Q l r ”的指令,例如查询序列 a 在区间 [ l , r ] 上的最大值,我们只需要递归执行以下过程
1.若 [ l , r ] 完全覆盖了当前节点代表的区间,即立即回溯(因为我们已经记录了当前区间的最值),并且该节点的w值作为候选答案
2.若左子节点与 [ l , r ] 有重叠部分,则递归访问左子节点
3.若右子节点与 [ l , r ] 有重叠部分,则递归访问右子节点
int ask(int p,int left,int right){
if(left <= tree[p].l && right >= tree[p].r) return tree[p].w;
//完全包含的情况
int mid = (tree[p].l + tree[p].r) / 2;
int val = -INF;//负无穷大
if(left <= mid) val = max(val,ask(p*2,left,right));
if(right > mid) val = max(val,ask(2*p+1,left,right));
return val;
}
2.4 lazytag 懒标记–延迟标记
在线段树的“区间查询”中,根据上面的证明和操作,我们知道能在至多 O(logN)下完成答案查询。但是在区间修改下,当某节点被修改区间 [ l , r ] 完全覆盖时,以这个节点为根的整棵子树的所有节点都会变化,若我们每次都对其进行更新,那么时间复杂度就会上升为O(N)大大降低效率。
我们知道,更新节点是为了查询而服务的,我们设要修改的区间为[ p1 , p2 ] ,其被区间 [ l , r ] 完全覆盖,当我们查询区间 [ l , r ] 时,有时无需用到其子区间,那么我们在修改区间 [ l , r ] ,根本无需更新子区间[ p1 , p2 ] 。
但在需要时又该怎么办,这里我们引入一个“懒标记”(或延迟标记)的概念。
懒标记是线段树的精髓所在。对于区间修改,朴素的想法是用递归的方式一层层修改(类似于线段树的建立),但这样的时间复杂度比较高。使用懒标记后,对于那些正好是线段树节点的区间,我们不继续递归下去,而是打上一个标记,将来要用到它的子区间的时候,再向下传递。
也就是说,除了在修改指令中直接划分的O(logN)个节点之外,对任意节点的修改都延迟到 “后续操作中递归进入其父节点时” 再执行更新操作