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;
}
posted on
浙公网安备 33010602011771号