线段树(递归实现,以求和为例):
定义部分
#define maxn 100007 //元素总个数
#define ls l,m,rt<<1
#define rs m+1,r,rt<<1|1
int Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add为懒惰标记
int A[maxn],n;//存原数组数据下标[1,n]
建树:
//PushUp函数更新节点信息 ,这里是求和
void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}
//Build函数建树
void Build(int l,int r,int rt){ //l,r表示当前节点区间,rt表示当前节点编号
if(l==r) {//若到达叶节点
Sum[rt]=A[l];//储存数组值
return;
}
int m=(l+r)>>1;
//左右递归
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新信息
PushUp(rt);
}
点修改(假设A[L]+=C)
void Update(int L,int C,int l,int r,int rt){//l,r表示当前节点区间,rt表示当前节点编号
if(l==r){//到叶节点,修改
Sum[rt]+=C;
return;
}
int m=(l+r)>>1;
//根据条件判断往左子树调用还是往右
if(L <= m) Update(L,C,l,m,rt<<1);
else Update(L,C,m+1,r,rt<<1|1);
PushUp(rt);//子节点更新了,所以本节点也需要更新信息
}
区间修改(假设A[L,R]+=C)
void Update(int L,int R,int C,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R){//如果本区间完全在操作区间[L,R]以内
Sum[rt]+=C*(r-l+1);//更新数字和,向上保持正确
Add[rt]+=C;//增加Add标记,表示本区间的Sum正确,子区间的Sum仍需要根据Add的值来调整
return ;
}
int m=(l+r)>>1;
PushDown(rt,m-l+1,r-m);//下推标记
//这里判断左右子树跟[L,R]有无交集,有交集才递归
if(L <= m) Update(L,R,C,l,m,rt<<1);
if(R > m) Update(L,R,C,m+1,r,rt<<1|1);
PushUp(rt);//更新本节点信息
}
为了简便,上面函数中,PushUp函数没有考虑Add标记。所以无论是相对标记还是绝对标记,在更新信息的时候,
到达的每个节点都必须调用PushDown函数来下推标记,另外,代码中,点修改函数中没有PushDown函数,因为这里假设只有点修改一种操作,
如果题目中是点修改和区间修改混合的话,那么点修改中也需要PushDown。
区间查询:首先是标记下推的函数
void PushDown(int rt,int ln,int rn){
//ln,rn为左子树,右子树的数字数量。
if(Add[rt]){
//下推标记
Add[rt<<1]+=Add[rt];
Add[rt<<1|1]+=Add[rt];
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]+=Add[rt]*ln;
Sum[rt<<1|1]+=Add[rt]*rn;
//清除本节点标记
Add[rt]=0;
}
}
求和函数
int Query(int L,int R,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R){
//在区间内,直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//下推标记,否则Sum可能不正确
PushDown(rt,m-l+1,r-m);
//累计答案
int ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}
函数调用:
//建树
Build(1,n,1);
//点修改
Update(L,C,1,n,1);
//区间修改
Update(L,R,C,1,n,1);
//区间查询
int ANS=Query(L,R,1,n,1);
线段树(非递归实现,以区间求和为例)
在进行点修改和区间查询时代码简单而且速度快,建树简单
原理:
这个线段树存在两段坐标映射:
原数组下标+1=线段树下标
线段树下标+N-1=存储下标
联立方程得到:原数组下标+N=存储下标
于是从原数组下标到存储下标的转换及其简单。
N的含义之一是,这棵树可以存N个元素,也就是说N必须大于等于n+2
于是,N的定义,N是大于等于n+2的,某个2的次方。
定义
#define maxn 100007
int A[maxn],n,N;//原数组,n为原数组元素个数 ,N为扩充元素个数
int Sum[maxn<<2];//区间和
int Add[maxn<<2];//懒惰标记
建树
void Build(int n){
//计算N的值
N=1;while(N < n+2) N <<= 1;
//更新叶节点
for(int i=1;i<=n;++i) Sum[N+i]=A[i];//原数组下标+N=存储下标
//更新非叶节点
for(int i=N-1;i>0;--i){
//更新所有非叶节点的统计信息
Sum[i]=Sum[i<<1]+Sum[i<<1|1];
//清空所有非叶节点的Add标记
Add[i]=0;
}
}
点修改(A[L]+=C)
void Update(int L,int C){
for(int s=N+L;s;s>>=1){
Sum[s]+=C;
}
}
点修改之下的查询:
求A[L..R]的和(点修改没有使用Add所以不需要考虑)
s^t^1 在s和t的父亲相同时值为0,终止循环。
两个if是判断s和t分别是左子节点还是右子节点,根据需要来计算Sum
int Query(int L,int R){
int ANS=0;
for(int s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1){
if(~s&1) ANS+=Sum[s^1];
if( t&1) ANS+=Sum[t^1];
}
return ANS;
}
区间修改:
void Update(int L,int R,int C){
int s,t,Ln=0,Rn=0,x=1;
//Ln: s一路走来已经包含了几个数
//Rn: t一路走来已经包含了几个数
//x: 本层每个节点包含几个数
for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){
//更新Sum
Sum[s]+=C*Ln;
Sum[t]+=C*Rn;
//处理Add
if(~s&1) Add[s^1]+=C,Sum[s^1]+=C*x,Ln+=x;
if( t&1) Add[t^1]+=C,Sum[t^1]+=C*x,Rn+=x;
}
//更新上层Sum
for(;s;s>>=1,t>>=1){
Sum[s]+=C*Ln;
Sum[t]+=C*Rn;
}
}
区间修改下的区间查询:
int Query(int L,int R){
int s,t,Ln=0,Rn=0,x=1;
int ANS=0;
for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){
//根据标记更新
if(Add[s]) ANS+=Add[s]*Ln;
if(Add[t]) ANS+=Add[t]*Rn;
//常规求和
if(~s&1) ANS+=Sum[s^1],Ln+=x;
if( t&1) ANS+=Sum[t^1],Rn+=x;
}
//处理上层标记
for(;s;s>>=1,t>>=1){
ANS+=Add[s]*Ln;
ANS+=Add[t]*Rn;
}
return ANS;
}
树状数组:
树状数组就是简单的利用二进制的性质构造一个树:将tree[]数组的结点序号转化为二进制
int lowbit(int t)
{
return t&(-t);
}
void add(int x,int y)
{
for(int i=x;i<=n;i+=lowbit(i))
tree[i]+=y;
}
int getsum(int x)
{
int ans=0;
for(int i=x;i>0;i-=lowbit(i))
ans+=tree[i];
return ans;
}
树状数组的优点:
- 代码短小,实现简单;
- 容易扩展到高纬度的数据;
缺点:
- 只能用于求和,不能求最大/小值;
- 不能动态插入;
- 数据多时,空间压力大;