大家好,你们的萝卜又双叒叕回来了!(最近在校队里写题有点累)
接下来给大家讲解线段树(话说你都开几个专题了)
Part 1:What is 线段树?
线段树是一种二叉搜索树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(log₂N)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
看到这里你是不是还是蒙的?如果是,我们来看下面一张图:
(图片来自百度,如有侵权请联系作者还不是线段树太难画了)
可以看到,每一个节点都有两个数,即这个节点代表的区间的左下标和右下标,而且每一段都是父亲节点的一半。这样就可以基于分治思想,快速有效地进行合并。
Part 2:线段树适合解决哪一类问题?
线段树的应用范围非常广泛,主要用来解决在实时更新的动态数组中的区间问题。简而言之,就是区间求解。
Part 3:如果我是初学者,线段树中的那些基本代码片段需要我学习?
(此处的部分代码片段的取名为个人喜好,其他题解里或许不一样,无需模仿)
1.创建一个线段树结构体
最基本的线段树包含:左下标,右下标,其他(如:区间和sum,懒标记lazy,最大区间和ans等)。基于这个,可以写出以下代码:
struct Tree {
int l,r,...;
}tree[maxm];
2.初始化线段树
初始化一个线段树需要记录每个结点的左下标,右下标。编写一个函数,分别有三个变量:左下标l,右下标r,以及当前节点位于结构体中的位置id。观察上面的线段树(忘了的同学请往上翻),可以发现,根节点位于1,其两个子节点分别位于2和3。再观察位于2的这个子节点,其两个子节点分别位于5和6。由此得出,位于id的两个子节点分别位于id*2和id*2+1。如果父节点代表的区间为[l,r],其中点为mid的话,其两个子节点代表的区间分别为[l,mid]和[mid+1,r]。由此写出代码:
void build(int l,int r,int id) { // 构建线段树
tree[id].l=l;
tree[id].r=r;
if (l==r) {
...
return;
}
int mid=(l+r)/2;
build(l,mid,id*2);
build(mid+1,r,id*2+1);
...
return;
}
3.进行更改操作
这里需要依题意进行讨论
1.单点修改
单点修改非常简单,只需传入点位于的坐标dis,判断dis位于哪个节点内即可,这里给出模板代码(不同题目的修改操作不一样,故此这里忽略):
void push_down(int id,int dis,int ...) { // 单点修改
...
if (tree[id].l==tree[id].r)
return;
int mid=(tree[id].l+tree[id].r)/2;
if (dis<=mid)
push_down(id*2,k,dis);
else
push_down(id*2+1,k,dis);
...
return;
}
2.区间修改及lazy标记
区间修改也比较简单,但是需要注意lazy标记的操作。如果当前区间被整个区间包含,那么直接更新lazy标记。然后是向下传递,如果左子节点的区间有一部分与区间重合(即tree[id*2].r>=l)那么就传递,右子节点同理。首先是更新lazy的代码:
void push_down(int id,int l,int r,ll dis) { // 更新lazy标记
if (tree[id].l>=l&&tree[id].r<=r) {
...
return;
}
spread(id);
if (tree[id*2].r>=l)
push_down(id*2,l,r,dis);
if (tree[id*2+1].l<=r)
push_down(id*2+1,l,r,dis);
...
return;
}
当我们遇到标记有lazy的节点时,我们需要将lazy节点下放,并根据题目更改答案,最最最重要的是归零(不归零就见祖宗)。这里给出模板(答案修改请根据题目自行编写):
void spread(int id) { // 向下传递
if (tree[id].lazy) {
...
tree[id].lazy=0; // 注意归0
}
return;
}
4.回答
回答需要判断当前区间是否在提问范围内,如果在直接更新就好。否则,判断是否有重叠片段,继续搜索:
void push_up(int id,int l,int r) { // 查询答案
if (tree[id].l>=l&&tree[id].r<=r) {
...
return;
}
...
if (tree[id*2].r>=l)
push_up(id*2,l,r);
if (tree[id*2+1].l<=r)
push_up(id*2+1,l,r);
return;
}
Part 4:总结
总之,线段树应用广泛,但是空间占用较大,对空间复杂度要求较高的题目不建议使用。代码偏长,需要一段时间理解。
喜欢就给个赞再走吧!