如题,已知一个数列,你需要进行下面两种操作:
第一行包含两个整数 n, m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [x, y] 内每个数加上 k。2 x y
:输出区间 [x, y] 内每个数的和。输出包含若干行整数,即为所有操作 2 的结果。
输入 #1
5 5 1 5 4 2 3 2 2 4 1 2 3 2 2 3 4 1 1 5 1 2 1 4
输出 #1
11 8 20
前缀和,差分,树状数组1,树状数组2,都无法解决这道题,最多70分,所以我们要学习线段树
什么是线段树
线段树是一种二叉搜索树,故线段树满足所有二叉树的规律。一般来说,我们对二叉树从上往下,做左到右的方式进行编号,故线段树的左儿子的编号总为2*i,右儿子的编号总为2*i+1。所以我们得到儿子就非常的方便。
因为这种关系在数组上非常容易实现,所以我们把这种关系的数组用树的结构来维护。
取儿子函数
inline int lc(int i){//取左儿子
return i<<1;
}
inline int rc(int i){//取右儿子
return i<<1|1;
}
注意,取出来的是下标,因为是下标满足规律
1.
什么是inline
有inline前置的函数,都会在使用函数的地方具体展开
例如调用了 lc(1) 会自动变成 return 1<<1
当代码体量很小的时候,可以减少栈的开销,节约时间和空间。
2.
i<<1是位运算,把i的二进制往左移动1位,缺少的用0补全,即乘2操作
故i<<1|1,的|1就可以实现+1操作,因为是用0补全的,最后一位必定是0
儿子你都会取了,怎么建树呢,赶快端上来罢!,没有学过二叉树的,自行百度二叉树的递归建树。
递归建树
递归之前首先我们要有存放和线段树相关信息的数组。
const int MAXN=1e+5;
int st[MAXN<<2];//segmentTree 直译 线段树
MAXN<<2就是上面提到过的位运算,不过是移动两位,所以相当于乘以4,
MAXN的大小取决于给定序列的长度。
为什么要给线段树开四倍的空间?
我们将原序列的MAXN个数据全部放在二叉树的叶子节点中。
故最底层的叶子节点数量有MAXN个
易得二叉树得高度为log2MAXN,然后计算一下这棵树的所有节点数量,近似取得节点数量为4MAXN。
递归建树
inline void update(int i){
//区间和
st[i]=st[lc(i)]+st[rc(i)];
//如果要最小值可以写成
//st[i]=min(st[lc(i)],st[rc(i)])
}
void build(int l,int r,int i){
if(l==r){//两个儿子都没了,即叶子节点
st[i]=a[l];//给叶子节点赋值,l和r用哪个都一样,反正相等
return;
}
int mid=(l+r)>>1;
build(l,mid, lc(i));
build(mid+1,r,rc(i));
update(i);//更新父节点
}
update是什么东西?你不会自己看函数吗,这一步用来让叶子节点的父亲们都拥有自己的值,为什么update放在这个地方,百度什么是二叉树的后序遍历,不然赋出来的就不是我们想要的结果了。
完成了建树之后,接下来我们就根据树的特性来进行区间修改和区间查询,你说为什么没有单点修改和单点查询?区间修改和区间查询的长度为1不就是单点了吗?,在此之前我们还要知道什么是懒标记。
懒标记:如果对a[1]修改,那么他的父类节点控制的区间都需要都需要加上对a[1]修改的值,所以我们对公共祖先节点打上修改的值,代表他的儿子们都需要被修改,当修改的时候从上往下传递懒标记,让儿子们加上这个值,因为都加过了,所以使用之后清零。
因此我们在对区间进行修改或者查询的时候,都要先下放懒标记。
void push_down(int l,int r,int i){
if(tag[i]){//有tag的话
int mid=(l+r)>>1;
//儿子们的tag加上父的tag
tag[lc(i)]+=tag[i];
tag[rc(i)]+=tag[i];
//儿子们加上父tag传递下来的值
st[lc(i)]+=tag[i]*(mid-l+1);
st[rc(i)]+=tag[i]*(r-mid);//注意此处不用加1,因为右孩子掌管的内容不包含mid,具体看build函数
//父亲的tag归零
tag[i]=0;
}
}
区间修改
void add(ll l,ll r,ll i,ll x,ll y,ll k){
if(x<=l&&y>=r){//如果遍历区间在[x,y]内
tag[i]+=k;//儿子们的懒标记
st[i]+=k*(r-l+1);//节点增加值,修改值*区间长度
return ;//懒标记存在则不需要接下去遍历
}
push_down(l,r,i);//下放懒标记
//遍历所有区间
ll mid=(l+r)>>1;
if(x<=mid)add(l,mid, lc(i),x,y,k);
if(y>mid)add(mid+1,r, rc(i),x,y,k);
update(i);//更新父节点
}
区间修改和区间查询差不多,因此实际运用的时候完全可以复制粘贴,去掉赋值部分,删掉更新部分,增加返回值即可
区间查询
ll query(ll l,ll r,ll i,ll x,ll y){
ll ans=0;//这个ans放在哪里都可以,只要在递归之前
if(x<=l&&y>=r){
return st[i];
}
//遍历所有区间
ll mid=(l+r)>>1;
push_down(l,r,i);//下放懒标记
if(x<=mid)ans+=query(l,mid, lc(i),x,y);
if(y>mid)ans+= query(mid+1,r, rc(i),x,y);
//最终返回值
return ans;
}
AC代码
#include
using namespace std;
using ll=long long;
const ll MAXN=1000001;
ll n,m,a[MAXN],st[MAXN<<2],tag[MAXN<<2];
inline ll lc(ll i){
return i<<1;
}
inline ll rc(ll i){
return i<<1|1;
}
void update(ll i){
st[i]=st[lc(i)]+st[rc(i)];
}
void build(ll l,ll r,ll i){
if(l==r){
st[i]=a[l];
return;
}
ll mid=(l+r)>>1;
build(l,mid, lc(i));
build(mid+1,r, rc(i));
update(i);
}
void push_down(ll l,ll r,ll i){
tag[lc(i)]+=tag[i];
tag[rc(i)]+=tag[i];
ll mid=(l+r)>>1;
st[lc(i)]+=tag[i]*(mid-l+1);
st[rc(i)]+=tag[i]*(r-mid);
tag[i]=0;
}
void add(ll l,ll r,ll i,ll x,ll y,ll k){
if(x<=l&&y>=r){
tag[i]+=k;
st[i]+=k*(r-l+1);
return ;
}
push_down(l,r,i);
ll mid=(l+r)>>1;
if(x<=mid)add(l,mid, lc(i),x,y,k);
if(y>mid)add(mid+1,r, rc(i),x,y,k);
update(i);
}
ll query(ll l,ll r,ll i,ll x,ll y){
ll ans=0;
if(x<=l&&y>=r){
return st[i];
}
ll mid=(l+r)>>1;
push_down(l,r,i);
if(x<=mid)ans+=query(l,mid, lc(i),x,y);
if(y>mid)ans+= query(mid+1,r, rc(i),x,y);
return ans;
}
int main(){
cin>>n>>m;
for(ll i=1;i<=n;i++)
scanf("%lld",&a[i]);
build(1,n,1);
ll op,x,y,k;
for(ll i=0;i
全部代码都有75行了,树状数组也才20多行,只要不涉及双区间修改的,为了更快做出题目,选谁显而易见。我才不用线段树!
当然线段树是可以不使用懒标记的,使用懒标记是因为,如果一个控制区间2-4已经被选定了,那就不需要再去修改他的儿子了(大大降低复杂度),只要每次累计标记的值就可以让该用上的用上,不需要的不修改。
如果不加上懒标记,那么修改值时,需要遍历所有修改区间的叶子节点。
无懒标记的修改
void add(ll l,ll r,ll i,ll x,ll y,ll k){
if(l==r){
st[i]+=k*(r-l+1);
return ;
}
ll mid=(l+r)>>1;
if(x<=mid)add(l,mid, lc(i),x,y,k);
if(y>mid)add(mid+1,r, rc(i),x,y,k);
update(i);
}