RMQ问题:区间最大值或者最小值问题,类似的还要区间和问题
操作:
(1)求最值 :区间内
(2)修改元素 :点修改
线段树:用于区间处理的数据结构,用二叉树构造
复杂度:O(nlogn):二叉折半查找
查找点或者区间的时候:顺着往下查找
修改元素:直接修改叶子节点,然后自底向上更新
!!!存储空间:4n
更新和查询:更新经过的每个节点,就把这个节点剩下的数量(长度)减1
复杂度:线段是把n个数按照二叉树进行分组,每次更新有关节点的时候,这个节点下面的所有子节点都隐含被更新了,从而减少了操作次数
例题:
还是last cows
第一种做法:用结构体实现线段树
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //线段树做法 /* 从后往前遍历输入的序列,遇到的每个值a表示此牛在剩余牛中排在第a+1个,删除此编号,循环此过程,最终得到的序列即为牛在此队列中的编号序列。 借助线段树查找未删除的数中排在第a+1个位置(编号排序位置)的牛的位置(读取顺序) */ struct node{ int l,r,len; }cow[100000]; int s[100000],ans[100000]; void build(int v,int l,int r){ cow[v].l=l; cow[v].r=r; cow[v].len=r-l+1; if(l==r) return; int mid=(l+r)/2; build(v*2,l,mid); build(v*2+1,mid+1,r); } int que(int v,int k){ --cow[v].len; if(cow[v].l==cow[v].r) return cow[v].r; //找到叶子节点, 注意此处不可用cow[v].len == 0代替,否则单支情况将直接返回,导致未达到最末端 else if(cow[v*2].len>=k){ return que(v*2,k); } else return que(v*2+1,k-cow[v*2].len);////!!!! } int main(){ int n; while(~scanf("%d",&n)){ for(int i=2;i<=n;i++) scanf("%d",&s[i]); s[1]=0; build(1,1,n); for(int i=n;i>=1;i--){ ans[i]=que(1,s[i]+1); } for(int i=1;i<=n;i++) printf("%d ",ans[i]); } return 0; }
第二种做法:完全二叉树(数组)
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=11010; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //数组实现线段树 int n; int pre[maxn],tree[maxn*4]={0},ans[maxn]={0}; void build(int n,int last_left){ for(int i=last_left;i<last_left+n;i++) tree[i]=1; //最后一行赋值 //从二叉树的最后一行倒推到根节点,根节点的值是牛的总数 while(last_left!=1){ for(int i=last_left/2;i<last_left;i++) tree[i]=tree[i*2]+tree[i*2+1]; last_left/=2; } } int que(int u,int num,int last_left){ //查询+维护,求出当前区间中坐起第num个元素 tree[u]--; if(tree[u]==0&&u>=last_left) return u; if(tree[u<<1]<num) //左子区间数量不够,查到右子区间 return que((u<<1)+1,num-tree[u<<1],last_left); if(tree[u<<1]>=num) //左子区间数量够了 return que(u<<1,num,last_left); } int main(){ int las; scanf("%d",&n); pre[1]=0; for(int i=2;i<=n;i++) scanf("%d",&pre[i]); las=1<<(int(log(n)/log(2))+1); //cout<<las<<endl; build(n,las); //从后往前退出每次最后一个数字 for(int i=n;i>=1;i--) ans[i]=que(1,pre[i]+1,las)-las+1; for(int i=1;i<=n;i++) printf("%d ",ans[i]); return 0; }
当数据太大:也可以考虑离散化,把原有的大二叉树压缩为小二叉树,但是压缩前后子区间的关系不变
区间修改
操作:(1)加 (2)查询和
lazy_tag方法:当修改一个整块区间时,只对这个线段区间进行整体上的修改,其内部每个元素内容先不修改,只有当这部分线段的一致性被破坏时才把变化之传给子区间(查询时也一样)
tag[]数组:记录节点i是否用到lazy原理,其值是op a b c中的c,如果做了多次lazy,那么add[]可以累加,如果在某次操作中被深入, 破坏了lazy,那么add[]归0
POJ 3468 A Simple Problem with Integers
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; //有错 const int maxn=1e5+5; const int INF=0x3fffffff; typedef long long LL; LL summ[maxn<<2],add[maxn<<2]; //4倍空间 void pusup(int r){ //向上更新,把值给i递归到父节点 什么时候使用这个函数呢?当现在的节点发生变化,需要顺便改变父节点 summ[r]=summ[r<<1]+summ[r<<1|1]; } void push_down(int rt,int m){ //更新r的子节点,m为长度 if(add[rt]){ add[rt<<1]+=add[rt]; add[rt<<1|1]+=add[rt]; summ[rt<<1]+=(m-(m>>1))*add[rt]; summ[rt<<1|1]+=(m>>1)*add[rt]; add[rt]=0; //取消本层的标记 } } void build(int l,int r,int rt){ //用满二叉树建树 add[rt]=0; if(l==r) { scanf("%lld",&summ[rt]); //叶子节点 赋值 return; } int mid=(l+r)/2; build(l,mid-1,rt<<1); build(mid+1,r,rt<<1|1); pusup(rt); //这是个递归结构 向上更新区间和 } void update(int a,int b,LL c,int l,int r,int rt){ //区间更新, if(a<=l&&b>=r){ //lazy方法,包含在区间里面,就整体更新 summ[rt]+=(r-l+1)*c; add[rt]+=c; return; } push_down(rt,r-l+1); //向下更新 int mid=(l+r)/2; //这里就是在处理有分叉的情况 if(a<=mid) update(a,b,c,l,mid-1,rt<<1); //分成两半,继续深入 if(b>mid) update(a,b,c,mid+1,r,rt<<1|1); pusup(rt); //向上更新 } LL que(int a,int b,int l,int r,int rt){ //区间求和 if(a<=l&&b>=r) return summ[rt]; //满足lazy直接返回 push_down(rt,r-l+1); //向下更新 int mid=(l+r)/2; LL ans=0; if(a<=mid) ans+=que(a,b,l,mid-1,rt<<1); if(b>mid) ans+=que(a,b,mid+1,r,rt<<1|1); return ans; } int main(){ int n,m; scanf("%d %d",&n,&m); build(1,n,1); //先建树 while(m--){ string str; int a,b; LL c; cin>>str; if(str[0]=='C'){ scanf("%d %d %lld",&a,&b,&c); update(a,b,c,1,n,1); } else{ scanf("%d %d",&a,&b); printf("%lld ",que(a,b,1,n,1)); } } return 0; }
1547:【 例 1】区间和
点修改、区间求和
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e5+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //模板题:点修改、区间查询 int n,m; LL summ[maxn*4]; /* void build(int l,int r,int root){ summ[root]=0; if(l==r) return; int mid=(l+r)/2; build(1,mid,root*2); build(mid+1,r,root*2+1); summ[root]=summ[root*2]+summ[root*2+1]; } */ LL que(int root,int l,int r,int x,int y){ //调用的时候: upda(1,1,n,a,b) if(r<x||y<l) return 0; //如果要求的区间与找到的区间交集为空,返回 if(l>=x&&y>=r) return summ[root];//如果找到的区间包含于要求的区间,返回这个区间的值 int mid=(l+r)/2; return que(root*2,l,mid,x,y)+que(root*2+1,mid+1,r,x,y); } void upda(int root,int l,int r,int a,int b){ //调用的时候: upda(1,1,n,a,b) if(a<l||a>r) return; if(l==r&&l==a){ //点修改 summ[root]+=b; return ; } int mid=(l+r)/2; upda(root*2,l,mid,a,b); upda(root*2+1,mid+1,r,a,b); summ[root]=summ[root*2]+summ[root*2+1]; //在这里回溯的时候修改 } int main(){ scanf("%d %d",&n,&m); int k,a,b; ///build(1,n,1); //在这里调用建树 for(int i=0;i<m;i++){ scanf("%d %d %d",&k,&a,&b); if(k==0) upda(1,1,n,a,b); //点修改,在a上加b else printf("%lld ",que(1,1,n,a,b)); //区间查询 } return 0; }
1548:【例 2】A Simple Problem with Integers
区间修改(加上x),区间求和
可以用线段树、也可以用树状数组
感觉线段树简单一点,但是不好推
用树状数组讲解:维护两个前缀和
https://blog.csdn.net/gzcszzx/article/details/100539427
维护两个前缀和,
S1[i]=d[i],S2[i]=d[i]*i
查询:位置Pos的前缀和就是(Pos+1)*S1中1到Pos的和 减去 S2中1到Pos的和,[L,R]=SS[R]-SS[L-1]
修改:[L,R]
S1:S1[L]+Tag,S1[R+1]-Tag
S2:S2[L]+Tag*L ,S2[R+1]-Tag*(R+1)
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e6+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //这道题是模板题:区间求和、区间修改 //可以用线段树、也可以用树状数组 //感觉线段树简单一点,但是不好推 //用树状数组讲解:维护两个前缀和 //https://blog.csdn.net/gzcszzx/article/details/100539427 /* 维护两个前缀和, S1[i]=d[i],S2[i]=d[i]*i 查询:位置Pos的前缀和就是(Pos+1)*S1中1到Pos的和 减去 S2中1到Pos的和,[L,R]=SS[R]-SS[L-1] 修改:[L,R] S1:S1[L]+Tag,S1[R+1]-Tag S2:S2[L]+Tag*L ,S2[R+1]-Tag*(R+1) */ LL n,m; LL a[maxn],d[maxn]; //a[i]为原数组 d[i]为差分数组 LL c1[maxn],c2[maxn]; //两个前缀和 #define lowbit(x) ((x)&(-x)) void add(LL x,LL v){ LL p=x; while(x<=n){ c1[x]+=v; c2[x]+=p*v; x+=lowbit(x); } } LL getans(LL x){ LL ans=0,p=x; while(x){ ans+=(p+1)*c1[x]-c2[x]; x-=lowbit(x); } return ans; } int main(){ scanf("%lld %lld",&n,&m); for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); d[i]=a[i]-a[i-1]; add(i,d[i]); } while(m--){ int p; scanf("%d",&p); if(p==1){ LL l,r,c; scanf("%lld %lld %lld",&l,&r,&c); add(l,c); add(r+1,-c); } if(p==2){ LL x,y; scanf("%lld %lld",&x,&y); printf("%lld ",getans(y)-getans(x-1)); } } return 0; }
1549:最大数
修改:在序列最后添加数
查询:最后L个数种最大数
单点更新,区间查询
这道题也有两种做法
//但是有一种是单调队列,另一种是线段树
//开始时创建一个大序列,全部设为 2147483647。每插入一个数,就将大序列中空闲部分的第一个数改为被插入的数,然后递归更新上层。复杂度O(nlog2n)。
原文链接:https://blog.csdn.net/sinat_34943123/article/details/53861325
单调队列的做法:
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=200001; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //单调队列的做法 /* 由于先入队的较小数,在有后入队的大数的情况下不可能为答案,所以,可以维护一个单调队列。由于单调队列中入队先后,与数的大小皆是有序的, 故可以用二分查找找到单调队列中,在后l个数里,最靠近队首(最大)的数,即为答案。 ps:(1)线段树常数大故此做法要快得多 (2)c++中可用函数lower_bound实现二分查找功能。 原文链接:https://blog.csdn.net/sinat_34943123/article/details/53861325 */ int a[maxn]; //q是队列 int q[maxn]; //一个存下标,一个存值 int m,p,num,t; int main(){ scanf("%d %d",&m,&p); t=0; int tmp,tail=0,l=0; char op; int xx; for(int i=0;i<m;i++){ scanf(" %c %d",&op,&xx); //cout<<l<<endl; if(op=='A'){ scanf("%d",&xx); int shuji=(t+xx)%p; while(q[tail]<=shuji&&tail) tail--; q[++tail]=shuji; a[tail]=++l; } if(op=='Q'){ int pos=lower_bound(a+1,a+1+tail,l-xx+1)-a; t=q[pos]; printf("%d ",t); } } return 0; }
线段树做法:
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=2e5+19; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //单点更新,区间查询? //这道题也有两种做法 //但是有一种是单调队列,另一种是线段树 //开始时创建一个大序列,全部设为 2147483647。每插入一个数,就将大序列中空闲部分的第一个数改为被插入的数,然后递归更新上层。复杂度O(n?log2n)。 int m,p; int a[maxn*4]; void build(int root,int l,int r){ //初始化 if(l>r) return; a[root]=-INF; int mid=(l+r)/2; if(l<r){ //记得要加这个条件呀。。。。 build(root*2,l,mid); build(root*2+1,mid+1,r); } } void upda(int root,int l,int r,int pos,int val){ //在pos位置上增加val值,也就是最后一个位置 if(l>r) return; if(l==r) a[root]=val; //找到了根节点,更新 else{ int mid=(l+r)/2; if(pos<=mid) upda(root*2,l,mid,pos,val); else upda(root*2+1,mid+1,r,pos,val); a[root]=max(a[root*2],a[root*2+1]); //在这里!!!每个节点存储的是最大的孩子节点值 } } int que(int root,int l,int r,int x,int y){ //l,r是会变化的 if(l>r||l>y||r<x) return -INF; if(l>=x&&r<=y) return a[root]; int mid=(l+r)/2; return max(que(root*2,l,mid,x,y),que(root*2+1,mid+1,r,x,y)); } int main(){ scanf("%d %d",&m,&p); build(1,1,m); //最多也只有m个数 int num=0;//添加的数的个数 int t=0; //存储上一次的查找结果 //一开始就初始化创建树,共m个节点,因为最多就m个节点 char op; int xx; for(int i=0;i<m;i++){ //cout<<i<<endl; scanf(" %c %d",&op,&xx); //cout<<op<<" "<<xx<<"jj"<<endl; if(op=='A'){ //表示添加一个数在后面 upda(1,1,m,++num,(xx+t)%p); } if(op=='Q') { //询问序列最后L个数中最大的数 int tmp=que(1,1,m,num-xx+1,num); //查询后面xx个数字 t=tmp; printf("%d ",tmp); } getchar(); } return 0; }
1550:花神游历各国
//区间修改、区间查询
//并且变化很神奇,l--r中每个国家的喜欢度变为sqrt()
注意要处理节点的值不断sqrt()后的变化,要特判是不是1或者0 mx[root]==1||mx[root]==0
需要数组:mx[maxn*4],summ[maxn*4],num[maxn],分别存储左右孩子最大值、总和、这个节点的值
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=2e5+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //区间修改、区间查询 //并且变化很神奇,l--r中每个国家的喜欢度变为sqrt() int n,m; LL summ[maxn*4],num[maxn]; LL mx[maxn*4]; void build(int l,int r,int root){ if(l==r) { summ[root]=mx[root]=num[l]; //根节点赋值 return; } int mid=(l+r)/2; build(l,mid,root*2); build(mid+1,r,root*2+1); summ[root]=summ[root<<1]+summ[(root<<1)+1]; //两个子树的和 mx[root]=max(mx[root<<1],mx[(root<<1)+1]); //两个子树的最大值 } void upda(int root,int l,int r,int x,int y){ //看这里为什么需要mx数组!!! if(mx[root]==1||mx[root]==0) return; //不需要改变值了 if(l==r){ summ[root]=mx[root]=int(sqrt(summ[root])); return; } int mid=(l+r)/2; if(x<=mid) upda(root*2,l,mid,x,y); if(y>mid) upda(root*2+1,mid+1,r,x,y); summ[root]=summ[root*2]+summ[root*2+1]; mx[root]=max(mx[root*2],mx[root*2+1]); } LL getans(int root,int l,int r,int x,int y){ if(x<=l&&r<=y) return summ[root]; int mid=(l+r)/2; LL ans=0; if(x<=mid) ans+=getans(root*2,l,mid,x,y); if(y>mid) ans+=getans(root*2+1,mid+1,r,x,y); return ans; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%lld",&num[i]); } build(1,n,1); //别忘列写这个TAT LL xx,ll,rr; scanf("%d",&m); while(m--){ scanf("%lld %lld %lld",&xx,&ll,&rr); if(xx==1){ printf("%lld ",getans(1,1,n,ll,rr)); } else{ upda(1,1,n,ll,rr); } } return 0; }
1551:维护序列
是区间修改,区间求和
//但是修改有两种方式:1、全部乘一个值 2、全部加一个值
//https://www.cnblogs.com/lher/p/6556238.html
//https://blog.csdn.net/weixin_43323172/article/details/99689300
经典线段树题目,同时有两个标记,一个加法标记,一个乘法标记,每个标记维护的意义为:下面的子树中,要先把每一项都乘以乘法标记,再加上加法标记。
设序列A = {a1,a2,a3,…,an},如果每一项先乘以p1,则序列变为{p1*a1,p1*a2,p1*a3,...,p1*an},再加上p2,则序列变为{p1*a1+p2,p1*a2+p2,p1*a3+p2,...,p1*an+p2},
再乘以p3,则序列变为{p1*p3*a1+p2*p3,p1*p3*a2+p2*p3,p1*p3*a3+p2*p3,...,p1*p3*an+p2*p3}。
由此可见,在添加标记或者下放标记合并时,
若新加乘法标记,则原有的乘法标记,加法标记和区间和都乘以新加的乘法标记,
若新加加法标记,则与前面的乘法标记无关,直接加在加法标记上,区间和加上区间长度*加法标记。
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e5+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //也是区间修改,区间求和 //但是修改有两种方式:1、全部乘一个值 2、全部加一个值 //https://www.cnblogs.com/lher/p/6556238.html //https://blog.csdn.net/weixin_43323172/article/details/99689300 LL n,p,m; LL summ[maxn*4]; //要加上Lazy操作,不然会超时 LL lazy_add[maxn*4],lazy_mul[maxn*4]; LL num[maxn]; void add(int v,int l,int r,int root){ //区间整体加 lazy_add[root]=(lazy_add[root]+v%p)%p; summ[root]=(summ[root]+(LL)v*(r-l+1)%p)%p; } /* 经典线段树题目,同时有两个标记,一个加法标记,一个乘法标记,每个标记维护的意义为:下面的子树中,要先把每一项都乘以乘法标记,再加上加法标记。 设序列A = {a1,a2,a3,…,an},如果每一项先乘以p1,则序列变为{p1*a1,p1*a2,p1*a3,...,p1*an},再加上p2,则序列变为{p1*a1+p2,p1*a2+p2,p1*a3+p2,...,p1*an+p2}, 再乘以p3,则序列变为{p1*p3*a1+p2*p3,p1*p3*a2+p2*p3,p1*p3*a3+p2*p3,...,p1*p3*an+p2*p3}。 由此可见,在添加标记或者下放标记合并时, 若新加乘法标记,则原有的乘法标记,加法标记和区间和都乘以新加的乘法标记, 若新加加法标记,则与前面的乘法标记无关,直接加在加法标记上,区间和加上区间长度*加法标记。 */ void mul(int v,int l,int r,int root){ lazy_mul[root]=(lazy_mul[root]*v)%p; lazy_add[root]=(lazy_add[root]*v)%p; //新加乘法标记,则原有的乘法标记,加法标记和区间和都乘以新加的乘法标记, summ[root]=(summ[root]*v)%p; } void push_down(int mm,int l,int r,int root){ if(lazy_mul[root]!=1){ // int mid=(l+r)/2; mul(lazy_mul[root],l,mm,root*2); mul(lazy_mul[root],mm+1,r,root*2+1); lazy_mul[root]=1; } if(lazy_add[root]!=0){ // int mid=(l+r)/2; add(lazy_add[root],l,mm,root*2); add(lazy_add[root],mm+1,r,root*2+1); lazy_add[root]=0; } } void build(LL l,LL r,LL root){ summ[root]=0; lazy_add[root]=0; lazy_mul[root]=1; if(l==r) { summ[root]=num[l]; return; } int mid=(l+r)/2; build(l,mid,root*2); build(mid+1,r,root*2+1); summ[root]=(summ[root*2]+summ[root*2+1])%p; } void upda(int root,int l,int r,int x,int y,int flag,int c){ if(x<=l&&r<=y) { if(flag==1) return mul(c,l,r,root); if(flag==2) return add(c,l,r,root) ; //return; } int mid=(l+r)/2; push_down(mid,l,r,root); //int mm,int l,int r,int root if(x<=mid) upda(root*2,l,mid,x,y,flag,c); if(y>mid) upda(root*2+1,mid+1,r,x,y,flag,c); summ[root]=(summ[root*2]+summ[root*2+1])%p; } LL getans(int root,int l,int r,int x,int y){ if(x<=l&&r<=y) return summ[root]; int mid=(l+r)/2; push_down(mid,l,r,root); LL ans=0; if(x<=mid) ans=(ans+getans(root*2,l,mid,x,y))%p; if(y>mid) ans=(ans+getans(root*2+1,mid+1,r,x,y))%p; return ans%p; } int main(){ scanf("%lld %lld",&n,&p); for(int i=1;i<=n;i++) scanf("%lld",&num[i]); build(1,n,1); int op,g,c,t; scanf("%d",&m); while(m--){ scanf("%d",&op); if(op==1||op==2){ scanf("%d %d %d",&t,&g,&c); upda(1,1,n,t,g,op,c); } else if(op==3){ scanf("%d %d",&t,&g); printf("%lld ",getans(1,1,n,t,g)); } } return 0; }