目录
线段树,顾名思义就是由一个一个线段组成的一颗树,每个结点都是一个线段(叶子结点是单元结点),那么每个结点应该包括:
区间左右端点。
区间要维护的信息(如最大值,最小值,区间和,区间异或、最大公约数)。
即每个结点是一个结构体。区间上可以动态的进行区间查询,修改,求和等操作。线段树还是一颗二叉搜索树。其主要用于高效解决连续区间的动态查询问题。
判断一个问题能否用线段树求解,首先要判断维护的值是否能由两个子区间得到
本质上线段树中的线段都是具有独特的属性,线段树就是一种维护线段的属性进而得出区间属性的数据结构。
线段树的构建:
线段树是一个完全二叉树,为了存储线段的属性,我们可以用一个结构体来表示线段集合。
注:下面的示例除了pushdown以外都是以维护区间和以及区间最大公约数来展示的,最终目的是为了求区间最大公约数。
{
问题模型:维护一个线段树,有两种操作, C ,l ,r ,d 表示 把区间【l,r】 区间内的数都加上d ; Q ,l, r 输出【l,r】 区间所有元素的最大公约数。
有最大公约数 gcd 函数满足交换律和结合律,的 gcd (a1 ,a2 ,a3, a4,.......an)=gcd(a1,a2-a1,a3-a2,........an-a(n-1));
}
struct node{
int l,r;//区间的左右子节点
int sum,d;//区间所具有的属性如区间和sum 区间最大公约数等
}tr[N*4];//空间正常开四倍
线段树的四种主要操作:
1.pushup 操作。
线段树最核心的操作,pushup(u)就是用 左子线段和右子线段 来更新 当前叶子线段属性的操作
void pushup(node &u,node &l,node &r){
u.sum=l.sum+r.sum;//更新区间和
u.d=gcd(l.d,r.d);// 更新区间最大公约数
}
2.pushdown 操作。
进行区间修改用父节点的标记更新子节点的属性的操作:
以区间修改,区间求和为例。
void pushdown(int u){
node &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];//引用简化代码。
if(root.add){
left.add+=root.add,left.sum+=(left.r-left.l+1)*root.d;//用父节点更新left子节点
right.add+=root.add,right.sum+=(right.r-right.l+1)*root.d;//用父节点更新right子节点
//lazy 标记的继承。
root.add=0;//把父节点lazy 标记清除。
}
}
那么为什么 lazy 标记需要pushdown 操作呢?
先考虑修改为什么需要pushdown
想象一下,我们给一段修改后的区间加上了lazy 标记,在我们对一个区间【l,r】进行修改操作时我们其实是通过修改他的两个子区间【l,mid】,【mid+1,r】来实现对整个区间的修改的,那么我们就需要进行pushdown 操作从而将父节点的标记传入到子节点来这样我们的两个子区间区间拥有了lazy 标记,同时利用父节点的标记,两个子区间的属性也被我们修改完毕,这样递归下去所有节点的信息就都被我们修改好了。
再考虑查询操作为什么需要pushdown 操作:
当我们进行查询操作的时候,我们其实只关注查询的值,但其实,当我们进行查询操作时,所要查询的区间不一定恰好位于某个子区间内,也就是说,有可能查询区间的一部分标记 是lazy1,而另一部分标记是lazy2,标记的内容不同,这时再用父节点的 lazy 标记去计算就不行了用pushdown
操作把区间一分为二,在递归求解左右儿子,就能完成查询操作。
再深入理解一下 sum属性和 lazy 标记(以区间加上一个数 add 为例)
每个结点的sum 表示的是如果只考虑当前节点及其子结点上所有标记的和,add 表示的不考虑父节点(当前节点),给当前区间的所有子区间加上一个add。
3.build 操作 :
线段树的简单递归构造操作:
void build(int u,int l,int r){
if(l==r) {//如果区间仅包含一个元素了 属性值与这一个元素 elemen[l]有关
int t=w[l]-w[l-1];
tr[u]={l,r,t,t};
}else {
tr[u]={l,r};
int mid=(l+r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);//递归建树,不要忘记pushup一下
}
}
4.modify()
擅长单点修改,其中区间修改需要 lazy 标记.
1>单点修改:
不断递归,最后一定能递归到目标单点直接修改即可:
//单点修改:
void modify(int u,int x,int v){
if(tr[u].l==x&&tr[u].r==x)
{
int t=tr[u].sum+v;
tr[u]={x,x,t,t};//单点直接修改
}else {
int mid=(tr[u].l+tr[u].r)>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
2>区间修改:
需要 lazy 标记
void modify(int u,int l,int r,int d){
if(tr[u].l>=l&&tr[u].r<=r){
tr[u].sum+= (tr[u].r-tr[u].l+1)*d;
tr[u].add+=d;
}else {
pushdown(u);
int mid=(tr[u].l+tr[u].r)>>1;
if(l<=mid) modify(u<<1,l,r,d);
if(r>mid) modify(u<<1|1,l,r,d);
pushup(u);
}
}
5. query()查询操作
int query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum%p;
else {
pushdown(u);//区间修改时才会有,分裂区间
int mid=(tr[u].l+tr[u].r)>>1;
int sum=0;
if(l<=mid) sum=query(u<<1,l,r);//求【l,r】在左区间的部分的和
if(r>mid) sum=(sum+query(u<<1|1,l,r))%p;//同理求右区间
return sum%p;
}
}
//注:这里p是是模数,与题目情景有关。
例题部分:
1.最大数:
给定一个正整数数列 a1,a2,…,an,每一个数都在 0~p-1 之间。
可以对这列数进行两种操作:
1.添加操作:向序列后添加一个数,序列长度变成 n+1;
2.询问操作:询问这个序列中最后 L 个数中最大的数是多少。
程序运行的最开始,整数序列为空。
一共要对整数序列进行 m 次操作。
输入格式
第一行有两个正整数 m,p,意义如题目描述;
接下来 m 行,每一行表示一个操作。
如果该行的内容是 Q L,则表示这个操作是询问序列中最后 L 个数的最大数是多少;
如果是 A t,则表示向序列后面加一个数,加入的数是 (t+a) mod p。其中,t 是输入的参数,a 是在这个添加操作之前最后一个询问操作的答案(如果之前没有询问操作,则 a=0)。
第一个操作一定是添加操作。对于询问操作,L>0 且不超过当前序列的长度。
输出格式
对于每一个询问操作,输出一行。该行只有一个数,即序列中最后 L 个数的最大数。
数据范围
1≤m≤2×105,
1≤p≤2×109,
0≤t<p
输入样例:
10 100
A 97
Q 1
Q 1
A 17
Q 2
A 63
Q 1
Q 1
Q 3
A 99
输出样例:
97
97
97
60
60
97
思路:用线段树维护一个区间最大值即可。
#include<bits/stdc++.h>
using namespace std;
int n,m,p,last;//n表示的是当前集合中一共有多少个元素从而确定修改与查询的区间。
const int N=2e5+10;
#define int long long
struct node{
int l,r,v;
}tr[N*4];
void build(int u,int l,int r){
tr[u]={l,r};
if(l==r) return;//这里直接退出是因为此题初始时是没有元素的,构建的是一颗空树。
int mid=(tr[u].l+tr[u].r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
}
void pushup(int u){
tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}
int query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
int mid=(tr[u].l+tr[u].r)>>1;
int v=0;
if(l<=mid) v=query(u<<1,l,r);
if(r>mid) v=max(v,query(u<<1|1,l,r));
return v;
}
void modify(int u,int x,int v){
if(tr[u].l==x&&tr[u].r==x) tr[u].v=v;
else {
int mid=(tr[u].l+tr[u].r)>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
signed main(){
cin>>m>>p;
build(1,1,m);
while(m--){
char op;
int x;
cin>>op>>x;
if(op=='A'){
modify(1,++n,(last+x)%p);
}else {
last=query(1,n-x+1,n);//询问后x 个元素组成的区间。
cout<<last<<endl;
}
}
return 0;
}
2、区间最大公约数:
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 d。
Q l r,表示询问 A[l],A[l+1],…,A[r] 的最大公约数(GCD)。
对于每个询问,输出一个整数表示答案。
输入格式
1.第一行两个整数 N,M。
2.第二行 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
N≤500000,M≤100000,
1≤A[i]≤1e18,
|d|≤1e18
输入样例:
5 5
1 3 5 7 9
Q 1 5
C 1 5 1
Q 1 5
C 3 3 6
Q 2 4
输出样例:
1
2
4
思路:
由于最大公约数 gcd 函数满足交换律和结合律,的 gcd (a1 ,a2 ,a3, a4,.......an)=gcd(a1,a2-a1,a3-a2,........an-a(n-1));
=> gcd(al......ar)=gcd(al,a[l+1]-a[l],......a[r]-a[r-1])
仔细观察等式的右边,al,就是数组a 的差分数组求前缀和,而右边一大堆就是数组a的差分数组的最大公约数,我们可以用线段树来维护数组a 的差分数组的区间和sum和最大公约数d,
这样我们在对数组a 进行区间修改时,等价于在其差分数组的左右两边各进行一次单点修改,简化了操作。
#include<bits/stdc++.h>
using namespace std;
int n,m;
#define int long long
const int N=5e5+10;
int w[N];
int gcd(int a,int b){
return b?gcd(b,a%b):a;
}
struct node{
int l,r;
int sum,d;
}tr[N*4];
void pushup(node &u,node &l,node&r){
u.sum=l.sum+r.sum;
u.d=gcd(l.d,r.d);
}
void pushup(int u){
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
if(l==r){
int d=w[l]-w[l-1];
tr[u]={l,r,d,d};
}else {
tr[u]={l,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){
if(l>r) return {0};//还是注意端点不能越界
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
else {
int mid=(tr[u].l+tr[u].r)>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
else {
node left =query(u<<1,l,r);
node right=query(u<<1|1,l,r);
node ans;
pushup(ans,left,right);//这里的pushup更像是线段的合并,把所有在[l,r]节点合并一起
return ans;
}
}
}
void modify(int u,int x,int v){
if(tr[u].l==x&&tr[u].r==x){
int sum=tr[u].sum+v;
tr[u]={x,x,sum,sum};
}else {
int mid=(tr[u].l+tr[u].r)>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
while(m--){
char op;
int l,r,d;
cin>>op>>l>>r;
if(l>r) swap(l,r);//数据有点坑
if(op=='Q'){
auto left=query(1,1,l).sum;
auto right=query(1,l+1,r).d;
cout<<abs(gcd(left,right))<<endl;
}else {
int d;
cin>>d;
modify(1,l,d);
if(r+1<=n) modify(1,r+1,-d);//注意端点不能越界
}
}
return 0;
}
3、你能回答这些问题吗
给定长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
1 x y,查询区间 [x,y] 中的最大连续子段和,即 maxx≤l≤r≤y{∑i=lrA[i]}。
2 x y,把 A[x] 改成 y。
对于每个查询指令,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M 行每行 3 个整数 k,x,y,k=1 表示查询(此时如果 x>y,请交换 x,y),k=2 表示修改。
输出格式
对于每个查询指令输出一个整数表示答案。
每个答案占一行。
数据范围
N≤500000,M≤100000,
-1000≤A[i]≤1000
输入样例:
5 3
1 2 -3 4 5
1 2 3
2 2 -1
1 3 2
输出样例:
2
-1
思路:容易看出这是一个线段树的题目,为了得到答案,我们需要考虑要用线段树维护什么东西。
1.我们肯定需要维护最大连续字段和记作 tmax,以及区间和sum.
2.1如果我们要查询的区减恰好就在某一个子区间内(左区间或者右区间),答案就是tmax
2.2如果我们要查询的区间横跨两个子区间,那么这时答案就不会是简单的代数和了,为了得到答案我们还需要再额外维护两个属性分别是 区间最大连续前缀和lmax,区间最大连续后缀和rmax,
在更新时就有
root.lmax=max(lson.lmax,lson.sum+rson.lmax);
root.rmax=max(rson.rmax,rson.sum+lson.rmax);
root.tmax=max(max(root.tmax,root.tmax),lson.rmax+rson.lmax);
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=5e5+10;
struct node{
int l,r;
int sum,lmax,rmax,tmax;
}tr[N*4];
int w[N];
void pushup(node &u,node &l,node &r){
u.sum=l.sum+r.sum;
u.lmax=max(l.lmax,l.sum+r.lmax);
u.rmax=max(r.rmax,r.sum+l.rmax);
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u){
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
if(l==r) tr[u]={l,r,w[l],w[l],w[l],w[l]};
else {
tr[u]={l,r};
int mid=(tr[u].l+tr[u].r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
node query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
else {
int mid=(tr[u].l+tr[u].r)>>1;
if(r<=mid) return query(u<<1,l,r);
else if(l>mid) return query(u<<1|1,l,r);
else {
node ls=query(u<<1,l,r);
node rs=query(u<<1|1,l,r);
node ans;
pushup(ans,ls,rs);
return ans;
}
}
}
void modify(int u,int x,int v){
if(tr[u].l==x&&tr[u].r==x) tr[u]={x,x,v,v,v,v};
else {
int mid=(tr[u].l+tr[u].r)>>1;
if(x<=mid) modify(u<<1,x,v);
else modify(u<<1|1,x,v);
pushup(u);
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
while(m--){
int op,l,r;
cin>>op>>l>>r;
if(op==1){
if(l>r) swap(l,r);//数据的坑
cout<<query(1,l,r).tmax<<endl;
}else{
modify(1,l,r);
}
}
return 0;
}
4、一个简单的整数问题2
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
1 .C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 d。
2.Q l r,表示询问数列中第 l~r 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤105,
|d|≤10000,
|A[i]|≤109
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
输出样例:
4
55
9
15
思路:lazy标记的引入。
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
const int N=2e5+10;
int w[N];
struct node{
int l,r;
int sum,add;
}tr[N*4];
void pushup(int u){
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void pushdown(int u){
auto &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
if(root.add){
left.add+=root.add,left.sum+=(left.r-left.l+1)*root.add;
right.add+=root.add,right.sum+=(right.r-right.l+1)*root.add;
root.add=0;
}
}
void build(int u,int l,int r){
if(l==r){
tr[u]={l,r,w[l],0};
}else {
tr[u]={l,r};
int mid=(tr[u].l+tr[u].r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int l,int r,int d){
if(tr[u].l>=l&&tr[u].r<=r) {
tr[u].sum+=(tr[u].r-tr[u].l+1)*d;
tr[u].add+=d;
}else {
pushdown(u);
int mid=(tr[u].l+tr[u].r)>>1;
if(l<=mid) modify(u<<1,l,r,d);
if(r>mid) modify(u<<1|1,l,r,d);
pushup(u);
}
}
int query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
else {
pushdown(u);
int mid=(tr[u].l+tr[u].r)>>1;
int sum=0;
if(l<=mid) sum+=query(u<<1,l,r);
if(r>mid) sum+=query(u<<1|1,l,r);
return sum;
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
while(m--){
char op;
int l,r,d;
cin>>op>>l>>r;
if(op=='C'){
cin>>d;
modify(1,l,r,d);
}else cout<<query(1,l,r)<<endl;
}
return 0;
}
5、维护区域和
老师交给小可可一个维护数列的任务,现在小可可希望你来帮他完成。
有长为 N 的数列,不妨设为 a1,a2,…,aN。
有如下三种操作形式:
把数列中的一段数全部乘一个值;
把数列中的一段数全部加一个值;
询问数列中的一段数的和,由于答案可能很大,你只需输出这个数模 P 的值。
输入格式
第一行两个整数 N 和 P;
第二行含有 N 个非负整数,从左到右依次为 a1,a2,…,aN;
第三行有一个整数 M,表示操作总数;
从第四行开始每行描述一个操作,输入的操作有以下三种形式:
操作 1:1 t g c,表示把所有满足 t≤i≤g 的 ai 改为 ai×c;
操作 2:2 t g c,表示把所有满足 t≤i≤g 的 ai 改为 ai+c;
操作 3:3 t g,询问所有满足 t≤i≤g 的 ai 的和模 P 的值。
同一行相邻两数之间用一个空格隔开,每行开头和末尾没有多余空格。
输出格式
对每个操作 3,按照它在输入中出现的顺序,依次输出一行一个整数表示询问结果。
数据范围
1≤N,M≤105,
1≤t≤g≤N,
0≤c,ai≤109,
1≤P≤109
输入样例:
7 43
1 2 3 4 5 6 7
5
1 2 5 5
3 2 4
2 3 7 9
3 1 3
3 4 7
输出样例:
2
35
8
样例解释
初始时数列为 {1,2,3,4,5,6,7};
经过第 1 次操作后,数列为 {1,10,15,20,25,6,7};
对第 2 次操作,和为 10+15+20=45,模 43 的结果是 2;
经过第 3 次操作后,数列为 {1,10,24,29,34,15,16};
对第 4 次操作,和为 1+10+24=35,模 43 的结果是 35;
对第 5 次操作,和为 29+34+15+16=94,模 43 的结果是 8。
思路:两个lazytag,但是要考虑好运算顺序以便于更新lazytag和得出答案,先乘后加更有利于计算,所以我们 用 tag(c) 表示 区间乘,tag(d)表示区间加;
#include<bits/stdc++.h>
using namespace std;
int n,m,p;
#define int long long
const int N=2e5+10;
int w[N];
struct node{
int l,r;
int sum,c,d;
}tr[N*4];
void pushup(int u){
tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%p;
}
//since (x*c0+d0)*c+d=(x*c0*c)+(b0*c+d)/ c0->c0*c / d0->d0*c+d
void eval(node &root ,int c,int d){//因为需要多次用到节点先乘后加的操作,所以封装一个函数。
root.sum=(root.sum*c+(root.r-root.l+1)*d)%p;
root.c=(root.c*c)%p;//update tagc
root.d=(root.d*c+d)%p;
}
void pushdown(int u){
eval(tr[u<<1],tr[u].c,tr[u].d);
eval(tr[u<<1|1],tr[u].c,tr[u].d);
tr[u].c=1,tr[u].d=0;//initialize fathernode tagc and tagd;
}
void build(int u,int l,int r){
if(l==r) tr[u]={l,r,w[r],1,0};
else {
tr[u]={l,r,0,1,0};
int mid=(tr[u].l+tr[u].r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int l,int r,int c,int d){
if(tr[u].l>=l&&tr[u].r<=r) {
eval(tr[u],c,d);
}else {
pushdown(u);
int mid=(tr[u].l+tr[u].r)>>1;
if(l<=mid) modify(u<<1,l,r,c,d);
if(r>mid) modify(u<<1|1,l,r,c,d);
pushup(u);
}
}
int query(int u,int l,int r){
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum%p;
else {
int sum=0;
pushdown(u);
int mid=(tr[u].l+tr[u].r)>>1;
if(l<=mid) sum+=query(u<<1,l,r)%p;
if(r>mid) sum=(sum+query(u<<1|1,l,r))%p;
return sum%p;
}
}
signed main(){
cin>>n>>p;
for(int i=1;i<=n;i++) cin>>w[i];
build(1,1,n);
cin>>m;
while(m--){
int op,l,r,c,d;
cin>>op>>l>>r;
if(op==1){
cin>>c;
modify(1,l,r,c,0);
}else if(op==2){
cin>>d;
modify(1,l,r,1,d);
}else cout<<query(1,l,r)%p<<endl;
}
return 0;
}