线段树
线段树是一种基于分治思想的二叉树,它的每个节点对应一个区间[L,R],叶子节点的区间L=R。非叶子节点[L,R]的左孩子区间为[L,(L+R)/2],右孩子区间为[(L+R)/2+1,R]。 [1,10]区间的线段树:

「1、线段树的存储方式」
对于区间最值(最大值或最小值)查询问题,线段树的每个节点包含三个域:l、r、mx,其中l和r表示区间的左右端点,mx表示[l, r]区间最值。 线段树除了最后一层,其他层构成一颗满二叉树,因此采用顺序存储方式,用一个数组tree[]存储节点。

以最大值为例,10个元素a[1..10]={5,3,7,2,12,1,6,4,8,15},线段树如下。

线段树需要开辟4n的空间
线段树的叶子结点有n个,那么这n个叶子结点最坏情况会在不同的两层上,即倒数2层,设线段树共有k层,以第k层只有2个叶子结点为例。如图所示,则第k-1层有n-2个叶子结点和1个非叶子结点,共n-1个结点,则第k层应有2(n-1)个空间结点,由满二叉树性质可知,前k-2层应有n-2个结点空间,所以总空间为 个。忽略少数空间,所以开辟空间时需要4n个空间。

「2、创建线段树」
可以采用递归的方法创建线段树。 算法步骤:
-
若是叶子节点(l=r),则节点的最值就是对应位置的元素值。 -
若是非叶子节点,则递归创建左子树和右子树。 -
节点的区间最值等于该节点左右子树最值的最大值。
「代码」
#define lc k<<1 //左孩子下标 2*k
#define rc k<<1|1 //右孩子下标 2*k+1
const int maxn=10005;
const int inf=0x7fffffff;
int n,a[maxn];
struct node{
int l,r,mx;//l,r表示区间的左右端点,mx表示区间[l,r]的最大值
}tree[maxn*4];
//构建线段树
void build(int k,int l,int r){ //k:线段树数组下标,l,r分别为结点的左右区间
tree[k].l=l;
tree[k].r=r;
if(l==r){
tree[k].mx=a[l];
return;
}
int mid=l+(r-l>>1);
build(lc,l,mid);
build(rc,mid+1,r);
tree[k].mx=max(tree[lc].mx,tree[rc].mx);
}
「3、点更新」
点更新指修改一个元素的值,例如将a[i]修改为v。 算法步骤: (1)若是叶子节点,满足l=r且l=i,则修改该节点的最值为v。 (2)若是非叶子节点,则判断是在左子树中更新还是在右子树中更新。 (3)返回时更新节点的最值。
「代码」
#define lc k<<1 //左孩子下标 2*k
#define rc k<<1|1 //右孩子下标 2*k+1
const int maxn=10005;
const int inf=0x7fffffff;
int n,a[maxn];
struct node{
int l,r,mx;//l,r表示区间的左右端点,mx表示区间[l,r]的最大值
}tree[maxn*4];
//更新线段树
void update(int k,int i,int v){ //k:线段树数组下标,i:更新的数组下标,v:更新后的值
if(tree[k].l==tree[k].r&&tree[k].l==i){
tree[k].mx=v;
return;
}
int mid=tree[k].l+(tree[k].r-tree[k].l>>1);
if(i<=mid) update(lc,i,v);
else update(rc,i,v);
tree[k].mx=max(tree[lc].mx,tree[rc].mx);
}
例如,修改第5个节点的值为14,先从树根向下找第5个元素所在的叶子节点,将其最值修改为14,返回时更新路径上所有节点的最值(左右子节点最值的最大值)。

「4.区间查询」
区间查询指查询一个[l,r]区间的最值。
算法步骤:
(1)若节点所在的区间被查询区间[1,r]覆盖,则返回该节点的最值。
(2)判断是在左子树中查询,还是在右子树中查询。
(3)返回最值。
「代码」
#define lc k<<1 //左孩子下标 2*k
#define rc k<<1|1 //右孩子下标 2*k+1
const int maxn=10005;
const int inf=0x7fffffff;
int n,a[maxn];
struct node{
int l,r,mx;//l,r表示区间的左右端点,mx表示区间[l,r]的最大值
}tree[maxn*4];
//查询区间最值(区间覆盖)
int query1(int k,int l,int r){ //k:线段树数组下标,l,r为查询区间范围
if(tree[k].l>=l&&tree[k].r<=r) return tree[k].mx;
int mid=tree[k].l+(tree[k].r-tree[k].l>>1);
int Max=-inf;
if(l<=mid) Max=max(Max,query1(lc,l,r));
if(r>mid) Max=max(Max,query1(rc,l,r));
return Max;
}
//查询区间最值(区间分割)
int query2(int k,int l,int r){ //k:线段树数组下标,l,r为查询区间范围
if(tree[k].l==l&&tree[k].r==r) return tree[k].mx;
int mid=tree[k].l+(tree[k].r-tree[k].l>>1);
int Max=-inf;
if(l<=mid) Max=max(Max,query2(lc,l,mid));
if(r>mid) Max=max(Max,query2(rc,mid+1,r));
return Max;
}
「算法分析:」
树上的操作大多与树高相关。因为线段树对区间进行二分,是一棵平衡二叉树,树高为O(logn),查询和更新的时间复杂度均为O(logn),空间复杂度为O(n)。线段树主要用于更新和查询,一般至少有一个是区间更新或查询。更新和查询的种类变化多样,灵活运用线段树可以解决多种问题。
「拓展:懒标记(难点)」
❝假设有数组a,现有2种操作,一种是将数组中区间[l,r]的值改为k,另一种是查询区间[l,r]的和,并且这2中操作次数足够多,我们应该怎么解决呢?
❞
如果用原有线段树,那么每次都要把区间[l,r]所有值都修改,如果再进行一次修改,那么上一次修改可能就无济于事了。
在看一种情况,首先进行修改区间[x,y]的值为d,再查询区间[x,y]的和。聪明的你一定能想到,直接用(y-x+1)*d就好了,对!那这样做的话,是不是只要修改这个区间值为d,然后用 就好了。那你可能会问,这是巧了,如果我查询的区间和修改的区间不同呢?你是不是就要把每个区间值都修改呢?不!我们还是不用全部修改,只需要修改我们用到的区间即可。为了便于理解我们以下面例子展开介绍。
现有数组a[]={1,2,3,4,5,6,7,8,9,10},(下标从1开始)先要进行4次操作(设修改的值大于0),分别是:
(1)将区间[1,5]的值改为3。
(2)求区间[1,5]的和。
(3)将区间[1,3]的值改为4。
(4)查询区间[3,4]的值。
「解析」
<<< 左右滑动见更多 >>>
上述演示的是下发(pushdown)的案例,于此同时也有回归(pushup)。回归就类似于递归,当子结点发生改变,同时子节点的改变会影响父节点的值,因此要回归。
举个例子,记录区间[1,2]最大值max=3,当其子区间[1,1]值改为5时,则父区间[1,2]的最大值也因发生改变,因此需要回归改变区间[1,2]的最大值为5。
「例题」
本文由 mdnice 多平台发布