线段树是一个比较高端的数据结构,与树状数组类似,它也具有维护一个数列的功能,建立于二分思想上,它的基础操作有三种:建树,查询,修改。其中修改和查询包括了单点和区间,单点修改和查询非常简单,所以我们这次重点讲的是区间修改和区间查询,以及建树。
所谓线段树,是将一段区间作为节点储存成树形的结构,一般都是二叉树,线段树很好理解,但是在刚刚学习中可能会因为代码过于繁琐而屡屡出错,请初学者耐心调试,坚持就是胜利。
我们需要一个结构体取保存每个节点:
struct line{ long long lo,hi,w,lazy; }tree[400040];
lo表示下界,hi表示上界,w表示这一结点上所有元素值得和,lazy时懒标记(后面会讲)
1-建树操作:
我们想要使用线段树,一定要先建树,由于线段树是一个二叉树结构,所以我们对于建树的函数设置三个局部变量:lo,hi,k 分别表示现在这个节点所代表线段的下界lo,上界hi,和节点编号:k,那么这个节点的子节点怎么表示呢,首先,我们需要一个mid来表示(lo+hi)/2的值,这样我们就可以将一个区间一分为二了(lo-mid,mid+1-hi),因为这个区间包含的点不一定是奇数,所以两个子节点的区间长度不一定相等。而k怎么计算呢,我们观察 1
2 3
4 5 6 7
可以大概看出,一个节点k的两个子节点分别是2*k和2*k+1, 我们用线段树的每个节点表示这个区间中的所有元素的和,所以我们递归建树,知道某个节点的hi和lo相等,说明它是单个元素节点,我们就输入它的值然后return,到它的父亲节点时,父亲节点累加它的两个儿子节点的值,然后不断向上递归,直到根节点。
这就是建树的总过程,下面是代码:
void build(int dwn,int oup,int k){ tree[k].lo=dwn,tree[k].hi=oup; if(dwn==oup){ cin>>tree[k].w; return; } int mid=(dwn+oup)/2; build(dwn,mid,2*k); build(mid+1,oup,2*k+1); tree[k].w=tree[2*k+1].w+tree[2*k].w; }
2-区间查询:
不带懒标记的区间查询非常好理解,我们首先设查询的区间为x,y ,我们从k=1节点开始,向下找子节点,k*2和k*2+1,因为我们已经用结构体记录了哪个节点包括的区间,所以我们判断如果这个节点的整个区间都包含在x,y里,那我们直接累加这个节点的值然后return,如果不在,那么我们计算mid的值,如果x小于mid,那么一定包含了左子节点,我们向下继续找左子节点,在判断如果y大于等于mid+1,那么则包含右子节点,我们就搜右子节点,这样推下去,一定会将每个x,y区间内的节点累计上,此时的结果就是区间所有数和。
下面上代码:
void search(int k){ int dwn=tree[k].lo; int oup=tree[k].hi; if(dwn>=x&&oup<=y){ ans+=tree[k].w; return; } int mid=(dwn+oup)/2; if(x<=mid) search(k*2); if(y>mid) search(k*2+1); }
3-区间修改
我们如果需要区间修改的话,有两种方法,1.我们递归找到每一个单元素节点,然后修改,递归上来时父节点根据子节点的更新来更新自己的值。这种方法虽然可行,但是时间复杂度很高,所以懒标记就诞生了,懒标记懒标记当然懒了,它的宗旨就是,我们修改一个区间的时候,和查询区间一样找到包括在这个区间内的节点,然后打上标记,标记的是这个区间的每个数需要修改的值,我们为了方便,只更新这个区间的值,将这个区间的值加上这个区间内的元素总个数*修改值,这样我们就可以先不让修改向下传,直到使用他们的时候再向下传标记,因为这种方法实在是太懒,所以叫做懒标记。
虽然它懒,但是它确实节省了不少时间。
下面我们来介绍这个懒标记如何传递。
假如k这个点打上了懒标记,然后我们需要访问k的子节点,我们为了不让数据失真,所以必须要让数据下穿,我们将k的子节点2*k,和2*k+1的值加上它们区间内的元素个数*k的懒标记(先假设这个区间修改只有加法),然后给2*k,2*k+1这两个节点打上懒标记然后将k节点的懒标记清零,这就完成了懒标记的传递。
下面上代码:
void down(int k){ long long po=tree[k].lazy; tree[k*2].w+=(tree[k*2].hi-tree[k*2].lo+1)*po; tree[k*2+1].w+=(tree[k*2+1].hi-tree[k*2+1].lo+1)*po; tree[k*2].lazy+=po; tree[k*2+1].lazy+=po; tree[k].lazy=0; }
带懒标记的区间修改也是非常好操作的,我们输入修改的值change,我们就给每个再修改区间内的节点的懒标记都加上change,然后只用将这个区间的值更新就行了,等到用的时候再将懒标记下穿。
下面上代码:
void changed(int k){ int dwn=tree[k].lo; int oup=tree[k].hi; if(dwn>=a&&oup<=b){ tree[k].w+=(tree[k].hi-tree[k].lo+1)*change; tree[k].lazy+=change; return; } if(tree[k].lazy) down(k); int mid=(dwn+oup)/2; if(a<=mid) changed(k*2); if(b>mid) changed(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; //一定记得回来时更新它父亲节点的值 }
而打上懒标记的区间的查询和一般的查询也没什么区别,只用再需要用到这一点的子节点时下穿懒标记就好了。
上代码:
void search(int k){ int dwn=tree[k].lo; int oup=tree[k].hi; if(dwn>=x&&oup<=y){ ans+=tree[k].w; return; } if(tree[k].lazy) down(k); int mid=(dwn+oup)/2; if(x<=mid) search(k*2); if(y>mid) search(k*2+1); }
所以整个带有建树,区间修改,区间查询的线段树代码就是:
#include<iostream> #include<cstdio> using namespace std; struct line{ long long lo,hi,w,lazy; }tree[400040]; int n,m,x,y,a,b,change; long long ans=0; void build(int dwn,int oup,int k){ tree[k].lo=dwn,tree[k].hi=oup; if(dwn==oup){ cin>>tree[k].w; return; } int mid=(dwn+oup)/2; build(dwn,mid,2*k); build(mid+1,oup,2*k+1); tree[k].w=tree[2*k+1].w+tree[2*k].w; } void down(int k){ long long po=tree[k].lazy; tree[k*2].w+=(tree[k*2].hi-tree[k*2].lo+1)*po; tree[k*2+1].w+=(tree[k*2+1].hi-tree[k*2+1].lo+1)*po; tree[k*2].lazy+=po; tree[k*2+1].lazy+=po; tree[k].lazy=0; } void search(int k){ int dwn=tree[k].lo; int oup=tree[k].hi; if(dwn>=x&&oup<=y){ ans+=tree[k].w; return; } if(tree[k].lazy) down(k); int mid=(dwn+oup)/2; if(x<=mid) search(k*2); if(y>mid) search(k*2+1); } void changed(int k){ int dwn=tree[k].lo; int oup=tree[k].hi; if(dwn>=a&&oup<=b){ tree[k].w+=(tree[k].hi-tree[k].lo+1)*change; tree[k].lazy+=change; return; } if(tree[k].lazy) down(k); int mid=(dwn+oup)/2; if(a<=mid) changed(k*2); if(b>mid) changed(k*2+1); tree[k].w=tree[k*2].w+tree[k*2+1].w; } int main(){ ios::sync_with_stdio(false); cin>>n>>m; build(1,n,1); int now; for(int i=1;i<=m;i++){ cin>>now; if(now==1){ cin>>a>>b>>change; changed(1); } else{ ans=0; cin>>x>>y; search(1); cout<<ans<<endl; } } }
谢谢阅读。