线段树详解(原理,实现,应用)入门篇
原理:
请见这篇blog。
https://www.cnblogs.com/AC-King/p/7789013.html
实现:
建树:
堆式建树
const int N=1e5+10;
int a[N],n,m;
struct SegmentTree{
int l,r;
long long maxn,add;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define Max(x) tree[x].maxn
}tree[N<<2];// N<<2=N*4, 注意一定要开四倍空间
void push_up(int p){// 从下向上更新信息
Max(p)=max(Max(p*2),Max(p*2+1));
}
void build(int p,int l,int r){
l(p)=l, r(p)=r;// 节点p代表区间[l,r]
if(l==r){
Max(p)=a[l];
return;
}
int mid=(l+r)/2;
build(p*2,l,mid);// 左子节点[l,mid], 编号p*2
build(p*2+1,mid+1,r);// 右子节点[mid+1,r], 编号p*2+1
push_up(p);
}
build(1,1,n);// 使用
单点修改
// 将a[x]修改为v,时间复杂度为O(logN)
void change_max(int p,int x,int v){
if(l(p)==r(p)){ Max(p)=v; return;}// 找到叶子节点
int mid=(l(p)+r(p))/2;
if(x<=mid) change_max(p*2,x,v);// x属于左区间
if(x>mid) change_max(p*2+1,x,v);// x属于右区间
push_up(p);
}
change_max(1,x,v);// 使用
区间查询
long long ask_max(int p,int l,int r){
if(l<=l(p)&&r(p)<=r) return Max(p);//完全包含
int mid=(l(p)+r(p))/2;
long long res=0;
if(l<=mid) res=max(res,ask_max(p*2,l,r));// 与左子节点有重叠
if(r>mid) res=max(res,ask_max(p*2+1,l,r));// 与右子节点有重叠
return res;
}
cout<<ask_max(1,l,r)<<'\n';// 使用
单点修改+区间查询最大值Code
#include<bits/stdc++.h>
#define ri register int
#define rll register long long
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=1e5+10;
int n,m;
int a[N];
template<typename T>inline void read(T &x){// 快读
char ch=getchar();int fx=0;x=0;
for(;!isdigit(ch);ch=getchar()) fx|=(ch=='-');
for(;isdigit(ch);ch=getchar()) x=((x<<3)+(x<<1)+(ch^48));
x=(fx?-x:x);
}
template<typename T>inline void write(T x){// 快写
if(x<0) putchar('-'), x=-x;
if(x>9) write(x/10);
putchar(x%10^'0');
}
struct SG{
int l,r;
long long maxn;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define Max(x) tree[x].maxn
}tree[N<<2];
void push_up(int p){// 从下向上更新信息
Max(p)=max(Max(p*2),Max(p*2+1));
}
void build(int p,int l,int r){
l(p)=l, r(p)=r;// 节点p代表区间[l,r]
if(l==r){
Max(p)=a[l];
return;
}
int mid=(l+r)/2;
build(p*2,l,mid);// 左子节点[l,mid], 编号p*2
build(p*2+1,mid+1,r);// 右子节点[mid+1,r], 编号p*2+1
push_up(p);
}
// 将a[x]修改为v,时间复杂度为O(logN)
void change_max(int p,int x,int v){
if(l(p)==r(p)){ Max(p)=v; return;}// 找到叶子节点
int mid=(l(p)+r(p))/2;
if(x<=mid) change_max(p*2,x,v);// x属于左区间
if(x>mid) change_max(p*2+1,x,v);// x属于右区间
push_up(p);
}
long long ask_max(int p,int l,int r){
if(l<=l(p)&&r(p)<=r) return Max(p);//完全包含
int mid=(l(p)+r(p))/2;
long long res=0;
if(l<=mid) res=max(res,ask_max(p*2,l,r));// 与左子节点有重叠
if(r>mid) res=max(res,ask_max(p*2+1,l,r));// 与右子节点有重叠
return res;
}
int main(){
read(n), read(m);
for(ri i=1;i<=n;i++) read(a[i]);
build(1,1,n);
while(m--){
int op,l,r,k;
read(op);
switch(op){
case 1: int x,v; read(x), read(v); change_max(1,x,v); break;// 单点修改
case 2: read(l), read(r); write(ask_max(1,l,r)), cout<<'\n'; break;// 区间查询最大值
}
}
return 0;
}
区间修改–延迟标记
如果暴力处理区间修改,那么时间复杂度就变成了O(N), 再加上有m次操作,时间复杂度就来到了O(N*M),自然是不优秀的,会被大数据卡掉。这时,引入延迟标记,就使时间复杂度减少到了O( l o g N {log_N} logN) 。
来一个区间加的模板题。
题目:P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
区间加线段树Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=1e5+10;
int n,m;
int a[N];
template<typename T>inline void read(T &x){// 快读
char ch=getchar();int fx=0;x=0;
for(;!isdigit(ch);ch=getchar()) fx|=(ch=='-');
for(;isdigit(ch);ch=getchar()) x=((x<<3)+(x<<1)+(ch^48));
x=(fx?-x:x);
}
template<typename T>inline void write(T x){// 快写
if(x<0) putchar('-'), x=-x;
if(x>9) write(x/10);
putchar(x%10^'0');
}
struct SegmentTree{
int l,r;
long long sum,add;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define sum(x) tree[x].sum
#define add(x) tree[x].add
}tree[N<<2];// 线段树
void push_up(int p){// 从下向上更新信息
sum(p)=sum(p*2)+sum(p*2+1);
}
void push_down(int p){// 从上向下更新信息
if(add(p)){// 节点p有标记
sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1);// 更新左子节点信息
sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1);// 更新右子节点信息
add(p*2)+=add(p);// 左子节点打延迟标记
add(p*2+1)+=add(p);// 右子节点打延迟标记
add(p)=0;
}
}
void build(int p,int l,int r){// 建树
l(p)=l, r(p)=r;
if(l==r){ sum(p)=a[l]; return;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
push_up(p);
}
void change(int p,int l,int r,int d){// 将[l,r]加d,时间复杂度为O(logN)
if(l<=l(p)&&r>=r(p)){// 完全覆盖
sum(p)+=(long long)d*(r(p)-l(p)+1);
add(p)+=d;// 给节点打延迟标记
return;
}
push_down(p);// 下传延迟标记
int mid=(l(p)+r(p))/2;
if(l<=mid) change(p*2,l,r,d);
if(r>mid) change(p*2+1,l,r,d);
push_up(p);
}
long long ask(int p,int l,int r){// 询问[l,r]的和
if(l<=l(p)&&r>=r(p)) return sum(p);// 完全覆盖
push_down(p);// 下传延迟标记
int mid=(l(p)+r(p))/2;
long long val=0;
if(l<=mid) val+=ask(p*2,l,r);
if(r>mid) val+=ask(p*2+1,l,r);
return val;
}
int main(){
read(n), read(m);
for(int i=1;i<=n;i++) read(a[i]);
build(1,1,n);
while(m--){
int num,l,r,k;
read(num), read(l), read(r);
if(num==1) read(k), change(1,l,r,k);
if(num==2) cout<<ask(1,l,r)<<'\n';
}
return 0;
}
再来一个较难的区间加和区间乘的线段树。
题目:P3373 【模板】线段树 2 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这个题就需要两个lazy标记——add,mul, 记住一定要在mul标记时之前的add标记要全部下传,不然就会出问题。这里有一篇blog讲解了为什么,不懂的朋友可以看一看。
[题解 P3373 【【模板】线段树 2】 - lqhsr 的博客 - 洛谷博客 (luogu.com.cn)]
加乘线段树Code
提示: n < < 1 = n × 2 , n < < 1 ∣ 1 = n × 2 + 1 n<<1=n {\times}2, n<<1|1=n{\times}2+1 n<<1=n×2,n<<1∣1=n×2+1
#include<bits/stdc++.h>
#define ri register int
#define rll register long long
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=1e5+10;
int n,T,mod;
int a[N];
struct SG{
int l,r;
ll sum,add,mul;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define sum(x) tree[x].sum
#define add(x) tree[x].add
#define mul(x) tree[x].mul
}tree[N<<2];
template<typename T>inline T read(T &x){
char ch=getchar(); int fx=0; x=0;
for( ;!isdigit(ch);ch=getchar()) fx|=(ch=='-');
for( ;isdigit(ch);ch=getchar()) x=x*10+(ch-'0');
return (fx? -x: x);
}
template<typename T>inline void write(T x){
if(x<0) putchar('-'), x=-x;
if(x>9) write(x/10);
putchar(x%10^48);
}
void push_up(int p){
sum(p)=(sum(p<<1)+sum(p<<1|1))%mod;
}
void push_down(int p){
sum(p<<1)=(sum(p<<1)*mul(p)+add(p)*(r(p<<1)-l(p<<1)+1)%mod)%mod;
sum(p<<1|1)=(sum(p<<1|1)*mul(p)+add(p)*(r(p<<1|1)-l(p<<1|1)+1)%mod)%mod;// add已经乘过mul了
mul(p<<1)=mul(p<<1)*mul(p)%mod;
mul(p<<1|1)=mul(p<<1|1)*mul(p)%mod;
add(p<<1)=(add(p<<1)*mul(p)+add(p))%mod;
add(p<<1|1)=(add(p<<1|1)*mul(p)+add(p))%mod;
add(p)=0, mul(p)=1;
}
void build(int p, int l, int r){
l(p)=l, r(p)=r, mul(p)=1;
if(l==r){ sum(p)=a[l]%mod; return;}
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
push_up(p);
}
void change_add(int p, int l, int r,int d){// 区间加
if(l<=l(p)&&r(p)<=r){
sum(p)=(sum(p)+1ll*d*(r(p)-l(p)+1))%mod;
add(p)=(add(p)+d)%mod;
return;
}
push_down(p);
int mid=(l(p)+r(p))>>1;
if(l<=mid) change_add(p<<1,l,r,d);
if(r>mid) change_add(p<<1|1,l,r,d);
push_up(p);
}
void change_mul(int p, int l, int r,int d){// 区间乘
if(l<=l(p)&&r(p)<=r){
sum(p)=sum(p)*d%mod;
add(p)=(add(p)*d)%mod;
// 注意:add要在这里乘上d,因为后面可能要加其他的数,而那些数其实是不用乘d的
mul(p)=mul(p)*d%mod;
return;
}
push_down(p);
int mid=(l(p)+r(p))>>1;
if(l<=mid) change_mul(p<<1,l,r,d);
if(r>mid) change_mul(p<<1|1,l,r,d);
push_up(p);
}
ll ask_sum(int p, int l, int r){// 区间求和
if(l<=l(p)&&r(p)<=r) return sum(p);
push_down(p);
int mid=(l(p)+r(p))>>1;
ll res=0;
if(l<=mid) res=(res+ask_sum(p<<1,l,r))%mod;
if(r>mid) res=(res+ask_sum(p<<1|1,l,r))%mod;
return res;
}
int main(){
read(n),read(T), read(mod);
for(ri i=1;i<=n;i++) read(a[i]);
build(1,1,n);
while(T--){
int op,l,r,k;
read(op);
switch(op){
case 1: {read(l), read(r), read(k), change_mul(1,l,r,k); break;}
case 2: {read(l), read(r), read(k), change_add(1,l,r,k); break;}
case 3: {read(l), read(r), write(ask_sum(1,l,r)), cout<<'\n'; break;}
}
}
return 0;
}
这里有一篇比较ok的blog,链接如下。
题解 P3373 【模板】线段树 2 - 小朋友的博客 - 洛谷博客 (luogu.com.cn)
这篇线段树入门篇中的基本操作你学会了吗?若还不太熟练,那我建议多去洛谷上刷一些有关线段树的题。麻烦点赞,收藏qwq!!!