介绍:
概念
线段树是一种二叉树数据结构,它用于高效处理区间查询(如求区间和、区间最大值等)和单点修改、区间修改操作。线段树的每个节点代表一个区间,根节点代表整个待处理的区间,每个内部节点将其代表的区间分成两部分,分别由其左右子节点表示。
思想
线段树的核心思想是分治。它将一个大区间不断地分解成较小的区间,直到每个区间只包含一个元素。通过预先计算和存储这些小区间的信息,我们可以在查询时快速地合并这些信息,从而高效地处理大区间的查询请求。同时,对于修改操作,我们只需要更新涉及到的节点信息,而不需要遍历整个区间。
功能
线段树主要用于解决以下两类问题:
单点修改,区间查询:
在某个位置修改元素的值,并查询某个区间内元素的信息(如和、最大值、最小值等)。
typedef struct node {
int l, r, M;
} node;
node tr[N * 4];
int pre = 0;
// 向上更新父节点信息
void pushup(int u) {
tr[u].M = max(tr[u << 1].M, tr[u << 1 | 1].M);
}
// 构建线段树
void build(int u, int l, int r) {
if (l == r) {
tr[u] = {l, r, 0};
} else {
int mid = (l + r) >> 1;
tr[u] = {l, r, 0};
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
// 区间查询
int query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].M;
} else {
int mid = (tr[u].l + tr[u].r) >> 1;
int res = 0;
if (mid >= l) {
res = query(u << 1, l, r);
}
if (mid < r) {
res = max(res, query(u << 1 | 1, l, r));
}
return res;
}
return 0;
}
// 单点修改
void modify(int u, int x, int v) {
if (x == tr[u].l && tr[u].r == x) {
tr[u].M = v;
} else {
int mid = (tr[u].l + tr[u].r) >> 1;
if (mid >= x) modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u);
}
}
不光可以求这个max,还可以求sum等等
pushup 函数:用于更新父节点的信息,这里是取左右子节点的最大值。
build 函数:递归构建线段树,将整个区间不断地二分,直到每个区间只包含一个元素。
query 函数:递归查询指定区间的信息,如果当前节点的区间完全包含在查询区间内,则直接返回该节点的信息;否则,分别查询左右子节点,并合并结果。
modify 函数:递归修改指定位置的元素值,并更新涉及到的父节点信息。
区间修改,区间查询:
对某个区间内的所有元素进行统一修改,并查询某个区间内元素的信息。
对于区间修改,我们需要引入懒标记(Lazy Tag)来优化时间复杂度。懒标记的作用是在区间修改时,不立即更新所有受影响的节点,而是先将修改信息记录在父节点上,等到需要查询这些节点时再将标记下放到子节点。
typedef struct node {
int l, r, M;
int lazy; // 懒标记
} node;
node tr[N * 4];
int pre = 0;
// 向上更新父节点信息
void pushup(int u) {
tr[u].M = max(tr[u << 1].M, tr[u << 1 | 1].M);
}
// 懒标记下放
void pushdown(int u) {
if (tr[u].lazy) {
tr[u << 1].M += tr[u].lazy;
tr[u << 1].lazy += tr[u].lazy;
tr[u << 1 | 1].M += tr[u].lazy;
tr[u << 1 | 1].lazy += tr[u].lazy;
tr[u].lazy = 0;
}
}
// 构建线段树
void build(int u, int l, int r) {
tr[u] = {l, r, 0, 0};
if (l == r) {
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
// 区间查询
int query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].M;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
int res = 0;
if (mid >= l) {
res = query(u << 1, l, r);
}
if (mid < r) {
res = max(res, query(u << 1 | 1, l, r));
}
return res;
}
// 区间修改
void modify(int u, int l, int r, int v) {
if (l <= tr[u].l && tr[u].r <= r) {
tr[u].M += v;
tr[u].lazy += v;
return;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (mid >= l) {
modify(u << 1, l, r, v);
}
if (mid < r) {
modify(u << 1 | 1, l, r, v);
}
pushup(u);
}
这里的懒标记可以不止一个,可以通过后面的题看出来
最大数:
题目描述
现在请求你维护一个数列,要求提供以下两种操作:
查询操作。
语法:Q L
功能:查询当前数列中末尾 L 个数中的最大的数,并输出这个数的值。
限制:L 不超过当前数列的长度。(L>0)
插入操作。
语法:A n
功能:将 n 加上 t,其中 t 是最近一次查询操作的答案(如果还未执行过查询操作,则 t=0),并将所得结果对一个固定的常数 D 取模,将所得答案插入到数列的末尾。
限制:n 是整数(可能为负数)并且在长整范围内。
注意:初始时数列是空的,没有一个数。
输入格式
第一行两个整数,M 和 D,其中 M 表示操作的个数,D 如上文中所述。
接下来的 M 行,每行一个字符串,描述一个具体的操作。语法如上文所述。
输出格式
对于每一个查询操作,你应该按照顺序依次输出结果,每个结果占一行。
分析:
一个涉及区间查询和单点修改的题目,可以尝试用树状数组or线段树来解决,下面是线段树的解法:
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=200010;
int n,p,m;
typedef struct node{
int l,r,M;
}node;
node tr[N*4];
int pre=0;
void pushup(int u){
tr[u].M=max(tr[u<<1].M,tr[u<<1|1].M);
}
void build(int u,int l,int r){
if(l==r){
tr[u]={l,r,0};
}else {
int mid=(l+r)>>1;
tr[u]={l,r,0};
build(u<<1,l,mid);
build(u<<1 | 1,mid+1,r);
pushup(u);
}
}
int query(int u,int l,int r){
if(l<=tr[u].l&&tr[u].r<=r){
return tr[u].M;
}else {
int mid =(tr[u].l+tr[u].r)>>1;
int res=0;
if(mid>=l){
res=query(u<<1,l,r);
}
if(mid<r){
res=max(res,query(u<<1|1,l,r));
}
return res;
}
return 0;
}
void modify(int u,int x,int v){
if(x==tr[u].l&&tr[u].r==x){
tr[u].M=v;
}else{
int mid =(tr[u].l+tr[u].r)>>1;
if(mid>=x)modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
int main(){
scanf("%d%d",&m,&p);
n=0;
pre=0;
build(1,1,m);
while(m--){
char op[2];
int val;
scanf("%s%d",op,&val);
if(op[0]=='Q'){
pre=query(1,n-val+1,n);
cout<<pre<<endl;
}else {
int tmp=((val+pre)%p+p)%p;
modify(1,n+1,tmp);
n++;
}
}
}
你能回答这些问题吗
给定长度为 NN 的数列 AA,以及 MM 条指令,每条指令可能是以下两种之一:
1 x y,查询区间 [x,y][x,y] 中的最大连续子段和,即 maxx≤l≤r≤ymaxx≤l≤r≤y{∑i=lrA[i]∑i=lrA[i]}。
2 x y,把 A[x]A[x] 改成 yy。
对于每个查询指令,输出一个整数表示答案。
输入格式
第一行两个整数 N,MN,M。
第二行 NN 个整数 A[i]A[i]。
接下来 MM 行每行 33 个整数 k,x,yk,x,y,k=1k=1 表示查询(此时如果 x>yx>y,请交换 x,yx,y),k=2k=2 表示修改。
输出格式
对于每个查询指令输出一个整数表示答案。
每个答案占一行。
分析:
对于一个node来说,在l~r区间的一个最大的连续字段和可以是:在左儿子l中最大的连续字段和,在右儿子r中的最大的连续字段和,横跨左儿子和右儿子的连续字段和,针对这个设计出的node
node{
int l,r;
int maxc,lc,rc;
}
但是我们现在就要多的去维护lc和rc,考虑lc是这么得到的:左儿子的lc,左儿子的sum和右儿子的lc,这样就可以求出来lc了,那么rc同理
sum很好维护,这个线段树的模版
代码:
#include<bits/stdc++.h>
using namespace std;
int inf=-0x3f3f3f3f;
const int N=500010;
int n,m;
struct node{
int l,r,maxc,lc,rc,sum;
}tr[N*4];
void pushup(int u){
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
tr[u].lc=max(tr[u<<1].lc,tr[u<<1].sum+tr[u<<1|1].lc);
tr[u].rc=max(tr[u<<1|1].rc,tr[u<<1|1].sum+tr[u<<1].rc);
tr[u].maxc=max(tr[u<<1].maxc,max(tr[u<<1|1].maxc,tr[u<<1].rc+tr[u<<1|1].lc));
}
void build(int u,int l,int r){
if(l==r){
tr[u]={l,r,0,0,0,0};
}else{
tr[u].l=l,tr[u].r=r;
int mid=(l+r)>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
}
node query(int u,int l,int r){
int ll=tr[u].l,rr=tr[u].r;
if(ll>=l&&rr<=r){
return tr[u];
}
node res;
int mid=(ll+rr)>>1;
if(r<=mid){
res=query(u<<1,l,r);
}else if(l>mid){
res=query(u<<1|1,l,r);
}else {//一定要讨论清楚,
node left=query(u<<1,l,r);
node right=query(u<<1|1,l,r);
res.sum=tr[u<<1].sum+tr[u<<1|1].sum;
res.lc=max(left.lc,left.sum+right.lc);
res.rc=max(right.rc,right.sum+left.rc);
res.maxc=max(left.maxc,max(right.maxc,left.rc+right.lc));
}
return res;
}
void modify(int u,int x,int v){
int ll=tr[u].l,rr=tr[u].r;
if(ll==x&&rr==x){
tr[u]={x,x,v,v,v,v};
}else {
int mid=(ll+rr)>>1;
if(mid>=x)modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
int main(){
scanf("%d%d",&n,&m);
build(1,1,n+2);
for(int i=1;i<=n;i++){
int tmp;
scanf("%d",&tmp);
modify(1,i,tmp);
}
for(int i=0;i<m;i++){
int k,x,y;
scanf("%d%d%d",&k,&x,&y);
if(k==1){
if(x>y)swap(x,y);
printf("%d\n",query(1,x,y).maxc);
}else {
modify(1,x,y);
}
}
}