线段树与树状数组模版

线段树(递归实现,以求和为例):

定义部分

#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;
}

树状数组的优点:

  1. 代码短小,实现简单;
  2. 容易扩展到高纬度的数据;

缺点:

  1. 只能用于求和,不能求最大/小值;
  2. 不能动态插入;
  3. 数据多时,空间压力大;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值