树状数组and线段树
感悟:
1.了解线段树
2.区间修改,查询 以及建树
来一道题目理解理解:洛谷题目链接
线段树视频讲解配有代码详解
题意:题意很简单,给一a数组,对a数组有两种操作1,和2,输出操作2的结果.
1 x y k:将区间 [x, y][x,y] 内每个数加上 k。
2 x y:输出区间 [x, y][x,y] 内每个数的和。
思路: 线段树的区间修改和区间查询。
简单介绍一下线段树:
用一tree数组存放每个a数组一个区间和的值。 下面拿一张图直观了解下线段树的大致模样。
tree数组(二叉树构造)和a数组(求区间和)
提交部分正确(70分)
分析:1.时间超限,需要在区间修改和查询修改(主要)
2.没开long long, 区间和超出int范围了.(次要)
代码如下:
#include<iostream>
using namespace std;
#define ls (now<<1)
#define rs (now<<1|1)
#define ll long long
const int maxn=1e5+10;
//开四倍,上面的我画的二叉数你也看见了,最大也到24了。
int tree[maxn<<2];
int read(){
int s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
//建立二叉树,并读入a数组.(巧在不用创建a数组)
void Built(int now,int l,int r){
if(l==r){
//这里有点东西,不断进栈 区间长度是1说明该输入吗,而且就是按a数组顺序输入.
tree[now]=read();return ;
}
int mid=(l+r)>>1;
//采用先序遍历
Built(ls,l,mid);
Built(rs,mid+1,r);
//退栈,没退前拿点东西再走吧,
//父节点的值 是两个儿子节点值之和。
tree[now]=tree[now<<1]+tree[now<<1|1];
return ;
}
//区间修改
void update(int now,int l,int r,int ql,int qr,int v){
//l.r变化区间 ql,qr指定区间
if(l==r){
tree[now]+=v;return ;
}
int mid=(l+r)>>1;
//指定区间定(思考逻辑) ql>mid和 qr<mid+1肯定是不在我们指定的区间
//这么理解 其实是把l,r区间的不断挤压到ql,qr区间 直至相同
if(ql<=mid) update(ls,l,mid,ql,qr,v);
if(mid<qr) update(rs,mid+1,r,ql,qr,v);
tree[now]=tree[ls]+tree[rs];
return ;
}
//区间查询
int query(int now,int l,int r,int ql,int qr){
//理解为,区间不断挤压的过程
//在什么情况就可以不用查了或者说递归结束呢???
// ql,qr区间覆盖了l,r就不用了递归了,直接出结果.
//2021129 ql,qr固定,mid区间左边l-mid不要,区间右边mid-r不要。
//mid在区间内:都要,慢慢拆分。
if(ql<=l&&qr>=r){
//只要查到了就行了 指定的区间包含l,r区间就可.
return tree[now];
}
int mid=(l+r)>>1;
int ans=0;
if(ql<=mid) ans+=query(ls,l,mid,ql,qr);
if(mid<qr) ans+=query(rs,mid+1,r,ql,qr);
return ans;
}
int main (){
int n;
int m;
cin>>n>>m;
Built(1,1,n);
int l,r,flag,v;
for(int i=1;i<=m;i++){
cin>>flag>>l>>r;
if(flag==1){
cin>>v;
update(1,1,n,l,r,v);
}else{
cout<<query(1,1,n,l,r)<<endl;
}
}
return 0;
}
优化:
tag数组标记二叉树节点
tag[i]:在树的i号节点对应的区间都加上tag[i]
tag在哪里做"文章"呢:不难看出没使用tag的线段树,每次区间修改的return返回之前时间复杂度到了o(nlogn) (凡是i节点的区间与指定修改的区间有交集就要改动,所以是o(nlogn))。用tag的话 可以缩短至log(n)
其想法是:只要指定修改的区间包含i节点的区间,用tag标记i节点。i的子节点就不用管了(啥时候管呢? 在下一次区间修改,查询时 需要用到该区间时,就标记下放,父节点标记归0。总结一句:当该区间要用到时tag就启动).
AC代码如下:
#include<iostream>
using namespace std;
#define ls (now<<1)
#define rs (now<<1|1)
#define ll long long
const int maxn=1e5+10;
//开四倍,上面的我画的二叉数你也看见了,最大也到24了。
//tree数组和标记数组
ll tree[maxn<<2],tag[maxn<<2];
ll read(){
ll s = 0, f = 1; char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
return s * f;
}
//建立二叉树,并读入a数组.(巧在不用创建a数组)
void Built(int now,int l,int r){
if(l==r){
//这里有点东西,不断进栈 区间长度是1说明该输入吗,而且就是按a数组顺序输入.
tree[now]=read();return ;
}
int mid=(l+r)>>1;
//采用先序遍历
Built(ls,l,mid);
Built(rs,mid+1,r);
//退栈,没退前拿点东西再走吧,
//父节点的值 是两个儿子节点值之和。
tree[now]=tree[now<<1]+tree[now<<1|1];
return ;
}
//标记下放
void pushdown(int now,int l,int r){
if(!tag[now]) return ;
int mid=(l+r)>>1;
//标记下放
tree[ls]+=tag[now]*(mid-l+1);
tree[rs]+=tag[now]*(r-mid);
//一定要记得标记儿子节点!!!
//你把我坑死了+= ps:- _ -;
tag[ls]+=tag[now];
tag[rs]+=tag[now];
//下放后清空父节点的标记
//父节点用了,就不要了,传给儿子了,父亲tag自然就没了蛮
tag[now]=0;
}
//区间修改
void update(int now,int l,int r,int ql,int qr,ll v){
if(l>=ql&&r<=qr){
tree[now]+=v*(r-l+1);
tag[now]+=v;
return ;
}
int mid=(l+r)>>1;
//每次修改和查询都要标记下放
pushdown(now,l,r);
if(ql<=mid) update(ls,l,mid,ql,qr,v);
if(mid<qr) update(rs,mid+1,r,ql,qr,v);
tree[now]=tree[ls]+tree[rs];
//return ;
}
//区间查询
ll query(int now,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr){
return tree[now];
}
int mid=(l+r)>>1;
ll ans=0;
//每次修改和查询都要标记下放
pushdown(now,l,r);
if(ql<=mid) ans+=query(ls,l,mid,ql,qr);
if(mid<qr) ans+=query(rs,mid+1,r,ql,qr);
return ans;
}
int main (){
ll n;
ll m;
cin>>n>>m;
Built(1,1,n);
ll l,r,flag,v;
for(int i=1;i<=m;i++){
cin>>flag>>l>>r;
if(flag==1){
cin>>v;
update(1,1,n,l,r,v);
}else{
cout<<query(1,1,n,l,r)<<endl;
}
}
return 0;
}