各种线段树模板&&Billboard HDU2795

本文详细介绍了线段树的基础模板,包括维护区间最大值、区间和以及区间修改为同一数值的方法,并通过实例解析了如何处理lazy数组的下推。此外,还探讨了一种特殊场景——修改根节点并向上pushup更新的应用。通过这些模板,读者可以更好地理解和解决线段树相关问题。
摘要由CSDN通过智能技术生成

各种常见线段树(模板)

最近遇到了好多线段树的题目,包括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;
}

线段树之后还是要持续更新的,大多数还都不会写,觉得这么一篇写下来,清晰了不少,如果有错误望指出。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值