RMQ问题:区间最大值或者最小值问题,类似的还要区间和问题
操作:
(1)求最值 、求和:区间内
(2)修改元素 :点修改、区间修改
线段树:用于区间处理的数据结构,用二叉树构造
二叉折半查找,查找点或者区间的时候:顺着往下查找 。存储空间:4n
修改点:直接修改叶子节点,然后自底向上更新
修改区间:使用lazy标记,加上pushdown函数,更新区间的lazy标记
复杂度:O(nlogn),线段是把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\n",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\n",ans[i]); return 0; }
当数据太大:也可以考虑离散化,把原有的大二叉树压缩为小二叉树,但是压缩前后子区间的关系不变
区间修改
操作:(1)加 (2)查询和
lazy_tag方法:当修改一个整块区间时,只对这个线段区间进行整体上的修改,其内部每个元素内容先不修改,只有当这部分线段的一致性被破坏时才把变化之传给子区间(查询时也一样)
tag[]数组:记录节点i是否用到lazy原理,其值是op a b c中的c,如果做了多次lazy,那么add[]可以累加,如果在某次操作中被深入, 破坏了lazy,那么add[]归0
1548:【例 2】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=1e6+10; int add[maxn*4],a[maxn]; long long summ[maxn*4]; int n,m; inline int getin(){ //读入优化 char c; int sgn=1; while((c=getchar())<'0'||c>'9') if(c=='-') sgn=-1; int res=c-'0'; while((c=getchar())>='0'&&c<='9') res=res*10+c-'0'; return res*=sgn; } void build(int l,int r,int rt){ if(l==r){ summ[rt]=a[l];return; } int mid=l+r>>1; build(l,mid,rt<<1); build(mid+1,r,rt<<1|1); summ[rt]=summ[rt<<1]+summ[rt<<1|1]; //位运算优化常数 } void adde(int rt,int l,int r,int v){ add[rt]+=v; summ[rt]+=(long long)v*(r-l+1); } void pushdown(int rt,int l,int r,int mid){ //标记下方 if(add[rt]==0) return; adde(rt<<1,l,mid,add[rt]); adde(rt<<1|1,mid+1,r,add[rt]); add[rt]=0; } long long que(int rt,int l,int r,int x,int y){ if(l>=x&&r<=y) return summ[rt]; int mid=l+r>>1; long long res=0; pushdown(rt,l,r,mid); if(x<=mid) res+=que(rt<<1,l,mid,x,y); if(mid<y) res+=que(rt<<1|1,mid+1,r,x,y); return res; } void chan(int rt,int l,int r,int x,int y,int v){ if(l>=x&&r<=y) { return adde(rt,l,r,v); } int mid=l+r>>1; pushdown(rt,l,r,mid); if(x<=mid) chan(rt<<1,l,mid,x,y,v); if(mid<y) chan(rt<<1|1,mid+1,r,x,y,v); summ[rt]=summ[rt<<1]+summ[rt<<1|1]; } int main(){ scanf("%d %d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); build(1,n,1); while(m--){ int d,l,r,x; scanf("%d ",&d); if(d==1){ scanf("%d %d %d",&l,&r,&x); chan(1,1,n,l,r,x); } else{ scanf("%d %d",&l,&r); printf("%lld\n",que(1,1,n,l,r)); } } 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\n",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\n",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\n",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\n",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\n",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\n",getans(1,1,n,t,g)); } } return 0; }