各种常见线段树(模板)
最近遇到了好多线段树的题目,包括CCPC铜牌线也是线段树,每次都是束手无策,于是打算整理整理几种常见的线段树模板,就是针对不同要求修改点和区间,不过感觉线段树的题目最难还是难在变化上,lazy数组如何下推需要好好思考,也希望认真总结一会,写点自己的理解。
尤其是最后一种,题目变化还是挺灵活的,就是对跟结点修改,再向上pushup,这边以hdu2795为例题目链接
先介绍几种常见的基础模板
维护区间最大值:
当然如果仅仅是查询区间最大值,并不进行修改的话,可以直接使用ST板来O(1)查询,但如果对单点或区间修改,就要采用线段树来维护。
//最小值同理
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 2e5+5;
typedef long long ll;
int Max[maxn<<2];
int a[maxn];
void pushup(int rt){
Max[rt]=max(Max[rt<<1],Max[rt<<1|1]);
}
void Build(int l,int r,int rt){
if(l==r){
Max[rt]=a[l];
return;
}
int m=(l+r)>>1;
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
pushup(rt);
}
void update(int L,int c,int l,int r,int rt){ //单点修改
if(l==r){
Max[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);
}
int Query(int L,int R,int l,int r,int rt){
if(L<=l&&r<=R){
return Max[rt];
}
int m=(l+r)>>1;
int ans=0;
if(L<=m) ans = max(ans,Query(L,R,l,m,rt<<1));
if(R>m) ans = max(ans,Query(L,R,m+1,r,rt<<1|1));
return ans;
}
维护区间和
觉得维护区间和最难还是在区间修改,lazy数组如何下推是个难点。
int Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add为懒惰标记
int A[maxn],n;//存原数组数据下标[1,n]
//(1)建树:
//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);
}
//(2)点修改:
//假设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);//子节点更新了,所以本节点也需要更新信息
}
//首先是下推标记的函数:
void PushDown(int rt,int ln,int rn){ //要深刻理解
//ln,rn为左子树,右子树的数字数量。
if(Add[rt]){
//下推标记
Add[rt<<1]+=Add[rt]; //这时候是将当前结点的Add下推到左右两个孩子的add数组
Add[rt<<1|1]+=Add[rt]; //所以要+=Add[rt],add[rt]清零
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]+=Add[rt]*ln;//由于add[rt]清空了,Sum[rt]的左孩子总共要加上add[rt]*左子树数量
Sum[rt<<1|1]+=Add[rt]*rn;
//清除本节点标记
Add[rt]=0;
}
}
//(3)区间修改
//假设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);//更新本节点信息
}
//(4)区间查询:
//询问A[L,R]的和
//然后是区间查询的函数:
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;
}
将区间修改为同一数值
对于修改A[L,R]=C, 这边要于上述区间修改推记add数组区别,因为将区间修改为同一数值,update如果本区间完全在操作区间[L,R]以内,这时候将Sum[rt]直接修改为C就行,但是add数组推记也需要做出相应变化。以后对于这种要求的题目都可以套用这个方法,主要还是对add数组推记的理解,希望以后自己也能多看看。
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);
}
//首先是下推标记的函数:
void PushDown(int rt,int ln,int rn){
//ln,rn为左子树,右子树的数字数量。
if(Add[rt]){
//下推标记
Add[rt<<1]=Add[rt];//add数组表示这段区间单个的值,这时候更新就不需要+=,而是=add[rt],这是一个区别
Add[rt<<1|1]=Add[rt];
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]=Add[rt]*ln;//add数组表示这段区间单个的值,所以这时候Sum[rt<<1]要修改为C*左子树的结点个数
Sum[rt<<1|1]=Add[rt]*rn;
//清除本节点标记
Add[rt]=0;
}
}
//(3)区间修改
//假设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);//更新数字和,向上保持正确,因为该区间值都变成C了,所以Sum[rt]就要修改为C*区间长度
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);//更新本节点信息
}
//(4)区间查询:
//询问A[L,R]的和
//然后是区间查询的函数:
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;
}
修改根节点,向上pushup更新
题意:给你一个h*w的广告板,需要粘贴n条长度为x的纸条,每条纸条不能在板上有重叠,也是一种单点修改,问能粘贴的最小行。
这题可以说是比较灵活的线段树,有点难想到,但是一定要搞懂,这种在叶子结点修改,再向上维护的题目,这边也可以当做个模板。
做法是:维护一个最大权值线段树,维护区间的最大值,询问时要在原来板子上进行一些修改,询问原来找到包含当前区间的一段就可以返回,但这题要返回的是可以粘贴的最小行,所以递归结束的条件是l=r,表示找到了这个叶子节点,如果你当前找到的结点的值大于你要粘贴的纸条x,那就查看左子树的最大值是不是大于x,如果是,就向左子树递归,直到找到叶子结点 ,总的来说就是一条链状的查找路径
详细可以看下代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 2e5+5;
typedef long long ll;
ll Max[maxn<<2];
ll a[maxn],n;
void pushup(int rt){
Max[rt]=max(Max[rt<<1],Max[rt<<1|1]);
}
void Build(int l,int r,int rt){
if(l==r){
Max[rt]=a[l];
return;
}
int m=(l+r)>>1;
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
pushup(rt);
}
void update(int L,int c,int l,int r,int rt){
if(l==r){
Max[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);
}
int Query(int L,int R,int l,int r,int rt,int val){
if(l==r){ //找到叶节点返回
return l;
}
int m=(l+r)>>1;
int ans=0;
if(Max[rt<<1]>=val) ans=Query(L,R,l,m,rt<<1,val); //如果左子树的最大值大于val,向左子树递归,找到最小的行
else if(Max[rt<<1|1]>=val) ans=Query(L,R,m+1,r,rt<<1|1,val);
return ans;
}
int main()
{
ll h,w;
while(cin>>h>>w>>n){
if(h>n) h=n;
for(int i=1;i<=h;i++) a[i]=w;
Build(1,h,1);
int t=n;
while(t--){
int x;
scanf("%d",&x);
if(Max[1]<x){
printf("-1\n");
}
else {
int idex=Query(1,h,1,h,1,x);
cout<<idex<<'\n';
update(idex,x,1,h,1);
}
}
}
return 0;
}
线段树之后还是要持续更新的,大多数还都不会写,觉得这么一篇写下来,清晰了不少,如果有错误望指出。