是什么?
一种数据结构,通过类似于二叉树的形式存储数据,能够快速de获取任意区间的可以合并的信息(最大/小值,和等)。不能合并的信息有区间中位数,区间和,众数等等。时间复杂度为O(log n)
原理?
通过每数组中每两个数字组成一小组,每两小组为一个中组,每两中组为一大组等等来形成一个二叉树,如图所示:
(这张图在整篇文章都会用到)
那么[1,2]这个区间的合并信息就能够被树节点4记录,[3,4]的合并信息被树节点5,对于区间[5,6],[7,8]同理。接着这四个小组的合并信息又会往上传,树节点4和5的合并信息能被树节点2记录,也就是说树节点2记录了区间[1,4]的信息。以这种模式一直上传到根节点为止。通过二叉树的方式总结信息,能够更快得获取信息。例如如果我们要知道数组1-4中最大值是多少,我们只需要查看树节点2储存的信息是多少,而不是通过遍历去比较,节省了很多时间。因此,一个记录二叉树的一维数组就会基于原数组诞生。
对于修改而言,由于有一个新的二叉树数组,不仅仅只是在原数组修改一个数组那么简单了。由于信息是从子节点向上传递的,那么修改的信息自然也需要从下往上传递。
如何用代码实现?
此代码使用洛谷P3372作为例子
创建二叉树数组
之前讲过,一个树节点有这么几个信息:代表的区间、所蕴含的信息(这里的例子是和)、再加上之后会讲到的懒标记(节省时间),那么为了方便,我比较喜欢用struct来创建二叉树数组:
struct tree{
int l,r;//左,右端点
long w, hold;//这个点的属性和懒标记,在这个题中属性是和
}t[400020];
接着,从上方图中可知,对于节点x来说,它的子节点分别为和(根节点从1开始),并且每次区间从父节点到子节点都是一分为二。那么就可以用递归的方式从下到上填入二叉树数组里的数据:
void build(int l, int r, int p){ //建树
//l、r代表左右节点,p代表此时在处理二叉树数组的那个位置元素
//一般从根结点往下走,所以l=1, r=n, p=1
t[p].l = l;//左节点
t[p].r = r;//右节点
if(l == r) {//当左、右节点一样时,这个二叉树数组上的包含的区间就只有一个点,那么就直接填入信息
t[p].w = a[l];
return;
}
int mid = (l+r)/2;//一分为二
build(l,mid,p*2);//递归,从最底部往上传信息
build(mid+1,r,p*2+1);
t[p].w = t[p*2].w + t[p*2+1].w;//合并传上来的信息
}
可以看到,因为每次都需要乘2,所以一般二叉树的数组会开到4*MAXN。
懒标记?
之前讲到修改二叉树数组需要从最底下的子节点开始修改,接着往上修改信息,但这样效率会很低,浪费时间。于是为了优化,懒标记就诞生了!(所以这就是为什么叫懒标记吧哈哈哈哈)根据刚刚创建二叉树的代码中不难推出,修改二叉树的代码也需要用到递归。因此,懒标记的作用就是使递归不再如“build”代码中一直递归到最底下的节点再开始返回,而是递归到包含所需要修改的区间就开始返回,并记录修改的信息。(注意:修改的信息需要可以合并,如加减乘除,赋值,翻转,等差数列,等比数列)举个例子,如果我们想要修改区间[1,4],使用懒标记就不再是需要先修改树节点4和5,再修改树节点2。而是只修改到树节点2,并记录修改的内容,等到需要用到树节点4和5时,再把懒标记蕴含的信息传下去(注意:两个子节点都要传)。讲个通俗易懂的例子:每个月领工资的时候都钱先存进银行,等要用的时候再取出来;而不是直接都拿到手里:
void lazy(int p){ //p是二叉树数组的位置
if(t[p].hold){//如果懒标记有内容,就下传一次
t[p*2].hold += t[p].hold;//更新左儿子的懒标记
t[p*2].w += t[p].hold * (t[p*2].r - t[p*2].l+1);//更新左儿子的信息
t[p*2+1].hold += t[p].hold;//更新右儿子的懒标记
t[p*2+1].w += t[p].hold * (t[p*2+1].r - t[p*2+1].l+1);//更新右儿子的信息
t[p].hold = 0;//这样自己这里的懒标记就没有用了,清零
}
}
区间修改
使用递归和懒标记进行修改,但很多时候需要修改的区间并不是用一个树节点就能表示出来的,这里的解决方法就相当于把要修改的区间分成不同的小块进行修改,再组合信息。分成小块时需要注意优先用能代表更大区间的树节点。举个例子,如果要修改[1,6]这个区间,因为没有一个树节点是直接代表[1,6],所以我们需要把这个区间分成[1,4]和[5,6]两个区间,也就是树节点2和6进行修改。因为我们要优先使用代表更大区间的树节点,所以这里我们使用[1,4],而不是[1,2]和[3,4]:
void change(int p, int nl, int nr, int k){//分成小块再相加
//p是二叉树数组中正在处理的位置,nl、nr要修改区间的左、右端点,k是要修改多少
if(t[p].l >= nl && t[p].r <= nr){//如果这个树节点的区间被包含在要修改区间中,就修改;如果不包含,就继续一分为二
t[p].w += (long)k * (t[p].r - t[p].l +1);//修改
t[p].hold += k;//更新懒标记
return;//
}
lazy(p);//因为p位置不满足条件,就继续分成更小的两个小块,那么就需要使用懒标记
int mid = (t[p].l+t[p].r)/2;//一分为二
if(nl <= mid){
change(p*2, nl, nr, k);
}
if(nr > mid){
change(p*2+1, nl, nr, k);
}
t[p].w = t[p*2].w + t[p*2+1].w;//从下往上更新信息
}
区间询问
询问的原理大致与修改相同,只不过不是修改信息而是提取信息:
long query(int p, int nl, int nr){ //p是二叉树数组正在处理的位置,nl、nr是要询问区间的左右端点
if(t[p].l >= nl && t[p].r <= nr){ //分成小块询问
return t[p].w;
}
lazy(p);//使用懒标记
int mid = (t[p].l+t[p].r)/2;//一分为二
long ans = 0;
if(nl <= mid){
ans += query(p*2, nl, nr);
}
if(nr > mid){
ans += query(p*2+1, nl, nr);
}
return ans;
}
完整代码
那么对于P3372的题解为:
#include <iostream>
using namespace std;
int n,m;
int MAXN = 100000;
int a[100020];
struct tree{
int l,r;//左,右端点
long w, hold;//这个点的属性和懒标记,在这个题中属性是和
}t[400020];
void build(int l, int r, int p){ //建树
t[p].l = l;
t[p].r = r;
if(l == r) {
t[p].w = a[l];
return;
}
int mid = (l+r)/2;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
t[p].w = t[p*2].w + t[p*2+1].w;
}
void lazy(int p){//懒标记
if(t[p].hold){
t[p*2].hold += t[p].hold;
t[p*2].w += t[p].hold * (t[p*2].r - t[p*2].l+1);
t[p*2+1].hold += t[p].hold;
t[p*2+1].w += t[p].hold * (t[p*2+1].r - t[p*2+1].l+1);
t[p].hold = 0;
}
}
void change(int p, int nl, int nr, int k){//区间修改
if(t[p].l >= nl && t[p].r <= nr){
t[p].w += (long)k * (t[p].r - t[p].l +1);
t[p].hold += k;
return;
}
lazy(p);
int mid = (t[p].l+t[p].r)/2;
if(nl <= mid){
change(p*2, nl, nr, k);
}
if(nr > mid){
change(p*2+1, nl, nr, k);
}
t[p].w = t[p*2].w + t[p*2+1].w;
}
long query(int p, int nl, int nr){//区间询问
if(t[p].l >= nl && t[p].r <= nr){
return t[p].w;
}
lazy(p);
int mid = (t[p].l+t[p].r)/2;
long ans = 0;
if(nl <= mid){
ans += query(p*2, nl, nr);
}
if(nr > mid){
ans += query(p*2+1, nl, nr);
}
return ans;
}
int main(int argc, const char argv[]){
cin>>n>>m;
for(int i = 1;i<=n;i++){
cin>>a[i];
}
build(1, n, 1);
for(int i = 1;i<=m;i++){
int t;
cin>>t;
if(t == 1){
int nl,nr,k;
cin>>nl>>nr>>k;
change(1, nl, nr, k);
}
else{
int nl, nr;
cin>>nl>>nr;
long ans1 = query(1, nl, nr);
cout<<ans1<<endl;
}
}
return 0;
}
什么时候用?
树状数组只能进行单点修改和区间询问,树状数组+差分只能进行区间修改和单点询问,那么线段数组的优处就在于可以进行区间的修改和询问(tip:长度为1的区间就是单点!)