该来的还是要来的.理解了树状数组之后想要解决区间更新及询问的问题,就要用到线段树.当然非常骚的树状数组可以过,不过那就是大佬做的事了.线段树其实不是一个很难的东西,因为递归二分的原因,除了push_down操作比较费事,其它都很好背下来.只要理解一点点就好.
update:由于以前写的线段树实在太丑了,不符合我现在的码风,故此把以前的代码全部删掉改成新的.
线段树是什么
先引入一个题:洛谷模板题3372.
就是给一个数组,有两种操作:给一个区间的每个数加k,或让你求一个区间的和.
暴力! n*m,gg.那肯定不行.但是用线段树可以.它的预处理复杂度nlogn,操作复杂度logn,空间复杂度n,就能过了.
我再一次作为灵魂画师为大家讲解一下线段树.假设有一个数组a[11],存储数据的位置为1-10.
我又画得很骚,不要在意.
也就是说线段树就是一棵完全二叉树,它的每个叶子结点存储数组元素的值,而它的父节点是两个子节点的和.一共有2*n-1个节点,所以处理的时候数组要用2xN的大小,甚至有些时候要开到4xN.
怎么构建
好的.首先我们先建立线段树.可以知道,每一棵子树根节点是segtree[i]
,左孩子是segtree[i*2]
,右孩子是segtree[i*2+1]
.我们可以用位运算加速计算左孩子和右孩子.
据此可写出函数build.
/*定义yuki这种类型是大小为N<<2|13这么大的数组*/
typedef ll yuki[yuzu<<2|13];
/*以后很多地方都要用到这些定义,我们不妨define一下让代码更简洁.*/
#define le rt<<1
#define ri le|1
#define ls le,l,mid
#define rs ri,mid+1,r
yuki val,lazy;//表示一个结点所储存的值和它的标记(标记到时再说)
/*三个参数分别表示线段树结点下标,目前所build的区间的左端点和右端点.*/
void build(int rt=1,int l=1,int r=n){//大多数情况下所需要build的区间都是1 to n
if (l==r) val[rt]=a[l];//如果rt代表的区间只有一个结点就是原数组中的第l个.
else{
int mid=l+r>>1;
build(ls),build(rs);//分别递归建造左子树和右子树.
val[rt]=val[le]+val[ri];//显然这个结点的值就是左边的值加上右边的值.
}
}
不会也不要紧,背出之.
然后是询问操作.
ll query(int ql,int qr,int rt=1,int l=1,int r=n){
if (ql>r||qr<l) return 0;//询问的区间和查找到的区间并没有交集,返回0
if (ql<=l&&qr>=r) return val[rt];//千万不能反,询问的区间完全包含查找区间,这时直接返回查找区间的和.
int mid=l+r>>1;
push_down(rt,l,r);
return query(ql,qr,ls)+query(ql,qr,rs);
}
}
诶?这个push_down是什么玩意?
void push_down(int rt,int l,int r){
if (lazy[rt]){
int mid=l+r>>1;
lazy[le]+=lazy[rt],lazy[ri]+=lazy[rt];
val[le]+=(mid-l+1)*lazy[rt];
val[ri]+=(r-mid)*lazy[rt];
lazy[rt]=0;
}
}
接下来才是高潮呢!终于要解释lazy是什么了.
为了更新区间,线段树中引入了延迟标记这个概念.这才是线段树的精华所在.
每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。
区间更新举例说明:当我们要对区间[0,2]的叶子节点增加2,利用区间查询的方法从根节点开始找到了非叶子节点[0-2],把它的值设置为1+2 = 3,并且把它的延迟标记设置为2,更新完毕;当我们要查询区间[0,1]内的最小值时,查找到区间[0,2]时,发现它的标记不为0,并且还要向下搜索,因此要把标记向下传递,把节点[0-1]的值设置为2+2 = 4,标记设置为2,节点[2-2]的值设置为1+2 = 3,标记设置为2(其实叶子节点的标志是不起作用的,这里是为了操作的一致性),然后返回查询结果:[0-1]节点的值4;当我们再次更新区间[0,1](增加3)时,查询到节点[0-1],发现它的标记值为2,因此把它的标记值设置为2+3 = 5,节点的值设置为4+3 = 7;
//此处摘至http://blog.sina.com.cn/s/blog_a2dce6b30101l8bi.html
我用自己的话来解释一下吧.
其实就是更新的时候,我们不用直接将修改更新到每一个节点,因为有一些结点可能再也不会被询问到了.
这时我们引入延迟标记,在修改完全覆盖整个区间时只是改一下延迟标记,也就相当于把这棵子树的子节点全都收起来而只留下了根节点.
那么如果下一次修改或询问的时候仅仅是部分覆盖了这棵子树,我们就再把延迟标记下放,把它的子树重新伸出来,就可以继续修改或者询问.注意不同的题的标记也是不同的.`
解释完了以后就可以看区间更新了.
void update(int ql,int qr,int v,int rt=1,int l=1,int r=n){
if (ql>r||qr<l) return;
if (ql<=l&&qr>=r){
val[rt]+=(r-l+1)*v;//本区间的所有数的个数乘v
lazy[rt]+=v;//标记一下
}else{
int mid=l+r>>1;
push_down(rt,l,r);
update(ql,qr,v,ls),update(ql,qr,v,rs);
val[rt]=val[le]+val[ri];
}
}
模板
最后是AC洛谷3372模板的代码.
#include<bits/stdc++.h> //Ithea Myse Valgulious
namespace chtholly{
typedef long long ll;
#define re0 register int
#define rec register char
#define rel register ll
#define gc getchar
#define pc putchar
#define p32 pc(' ')
#define pl puts("")
/*By Citrus*/
inline int read(){
re0 x=0,f=1;rec c=gc();
for (;!isdigit(c);c=gc()) f^=c=='-';
for (;isdigit(c);c=gc()) x=x*10+c-'0';
return x*(f?1:-1);
}
inline void read(rel &x){
x=0;re0 f=1;rec c=gc();
for (;!isdigit(c);c=gc()) f^=c=='-';
for (;isdigit(c);c=gc()) x=x*10+c-'0';
x*=f?1:-1;
}
template <typename mitsuha>
inline int write(mitsuha x){
if (!x) return pc(48);
if (x<0) x=-x,pc('-');
re0 bit[20],i,p=0;
for (;x;x/=10) bit[++p]=x%10;
for (i=p;i;--i) pc(bit[i]+48);
}
}using namespace chtholly;
using namespace std;
const int yuzu=1e5;
typedef ll fuko[yuzu<<2|10];
int n=read(),m=read();
struct segtree{
#define le rt<<1
#define ri le|1
#define ls le,l,mid
#define rs ri,mid+1,r
fuko val,lazy;
void build(int rt=1,int l=1,int r=n){
if (l==r) val[rt]=read();
else{
int mid=l+r>>1;
build(ls),build(rs);
val[rt]=val[le]+val[ri];
}
}
void push_down(int rt,int l,int r){
if (lazy[rt]){
int mid=l+r>>1;
lazy[le]+=lazy[rt],lazy[ri]+=lazy[rt];
val[le]+=(mid-l+1)*lazy[rt];
val[ri]+=(r-mid)*lazy[rt];
lazy[rt]=0;
}
}
void update(int ql,int qr,int v,int rt=1,int l=1,int r=n){
if (ql>r||qr<l) return;
if (ql<=l&&qr>=r){
val[rt]+=(r-l+1)*v;
lazy[rt]+=v;
}else{
int mid=l+r>>1;
push_down(rt,l,r);
update(ql,qr,v,ls),update(ql,qr,v,rs);
val[rt]=val[le]+val[ri];
}
}
ll query(int ql,int qr,int rt=1,int l=1,int r=n){
if (ql>r||qr<l) return 0;
if (ql<=l&&qr>=r) return val[rt];
int mid=l+r>>1;
push_down(rt,l,r);
return query(ql,qr,ls)+query(ql,qr,rs);
}
}my_;
int main(){
my_.build();
for (;m--;){
re0 t=read(),l=read(),r=read();
if (t==1){
my_.update(l,r,read());
}
else{
write(my_.query(l,r)),pl;
}
}
}
例题
我看看有没有线段树的例题.有几个.
我再引用一下别人的博客吧.
线段树经典例题