ST表,Sparse Table,是解决区间最值问题,及RMQ问题的工具,利用倍增思想,O(N*log2(N))预处理,O(1)查询。
设f[i][j]表示从i开始的2^j个数的最值,初始化f[i][0]=a[i],对于处理f数组,首先枚举区间长度j,第二层枚举左端点i,当i+(1<<j)-1>n时终止循环,对于最大值的转移,f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1])。
对于查询,先取log2(区间长度),之后分别查询左右端点,为了保证覆盖整个区间,查询左端点是f[l][log2(len)],可能区间右边有空白部分,于是查询右端点时,找一个左端点x满足x+len-1=r,即查询f[r-len+1][log2(len)],两者取max即可。
inline int query(int l,int r){
int k=log2(r-l+1);//区间长度的对数
return max(f[l][k],f[r-(1<<k)+1][k]);//分别查询左右端点,覆盖完整区间
}
for(int i=1;i<=n;i++)f[i][0]=a[i];//初始化
for(int j=1;j<=20;j++){//区间长度
for(int i=1;i+(1<<j)-1<=n;i++){//区间左端点
f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]);//进行转移,用两个2^(j-1)拼接得到答案2^j
}
}
ST表也可以用来获取关于位置的信息,可用于查询区间最值的位置。
inline int pushup(int x,int y){
return a[x]>a[y]?x:y;
}
inline int query(int l,int r){
int k=log2(r-l+1);
return pushup(f[l][k],f[r-(1<<k)+1][k]);
}
for(int i=1;i<=n;i++)f[i][0]=i;
for(int j=1;j<=20;j++)for(int i=1;i+(1<<j)-1<=n;i++)f[i][j]=pushup(f[i][j-1],f[i+(1<<j-1)][j-1]);
二维ST表,时间复杂度O(N^2*log2(N)),设f[i][j][k]表示以(i,j)为左上角的长度为k的正方形的最值,那么转移类似。
inline int query(int l,int r,int len,int op=0){//询问正方形内的最值,四个点都要类似的查一遍
int k=log2(len),a=f[op][l][r][k],b=f[op][l+len-(1<<k)][r][k],c=f[op][l][r+len-(1<<k)][k],d=f[op][l+len-(1<<k)][r+len-(1<<k)][k];
return op?max({a,b,c,d}):min({a,b,c,d});
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[0][i][j][0]=f[1][i][j][0]=a[i][j];//初始化
}
}
for(int k=1;k<=12;k++){
for(int i=1;i+(1<<k)-1<=n;i++){
for(int j=1;j+(1<<k)-1<=m;j++){
int a=f[0][i][j][k-1],b=f[0][i+(1<<k-1)][j][k-1],c=f[0][i][j+(1<<k-1)][k-1],d=f[0][i+(1<<k-1)][j+(1<<k-1)][k-1];
f[0][i][j][k]=min({a,b,c,d});
a=f[1][i][j][k-1],b=f[1][i+(1<<k-1)][j][k-1],c=f[1][i][j+(1<<k-1)][k-1],d=f[1][i+(1<<k-1)][j+(1<<k-1)][k-1];
f[1][i][j][k]=max({a,b,c,d});
}
}
}
一个n个点的环,定义f[i]为满足a[j]>=b[i]的最近点j与i在换上距离较短一边的长度,i!=j,若没有j,则f[i]=-1,求出每个点的f[i].按照a值构建ST表最大值,对于每个b[i]二分求解。
inline int divide(int x,int p){
int l=1,r=n;
while(l<r){
int mid=l+r>>1;
if(max(query(p-mid,p-1),query(p+1,p+mid))>=x)r=mid;
else l=mid+1;
}
return l==n?-1:l;
}
for(int i=1;i<=n;i++)cin>>a[i],f[i][0]=f[i+n][0]=f[i+n+n][0]=a[i];
for(int j=1;j<=20;j++)for(int i=1;i+(1<<j)-1<=n+n+n;i++)f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]);
for(int i=1,b;i<=n;i++)cin>>b,cout<<divide(b,i+n)<<' ';
喷泉由上到下编号1到n,每个盘子有直径d和容量c,若水超过若容量会向下流到半径大于这个盘子的盘子里,每次询问向x个盘子中倒入v的水,问水最后会到哪个盘子,若无法满足则输出0.从n到1倒序构建一个单调栈,找出每个盘子溢出后流到的第一个位置,倍增处理到哪个盘子和路径上会经过的容量和。
for(int i=n;i;i--){
while(s[0]&&d[i]>=d[s[s[0]]])s[0]--;//找到第一个超过d[i]的盘子
fa[i][0]=s[s[0]];
sum[i][0]=c[i];
s[++s[0]]=i;
}
for(int j=1;j<=20;j++)for(int i=1;i<=n;i++)fa[i][j]=fa[fa[i][j-1]][j-1],sum[i][j]=sum[i][j-1]+sum[fa[i][j-1]][j-1];
while(q--){
int x,v;
cin>>x>>v;
for(int i=20;~i;i--)if(v>sum[x][i])v-=sum[x][i],x=fa[x][i];//只有严格大于sum时才跳fa
cout<<x<<'\n';
}
一个01序列,每次询问[l,r]的最长上升子序列或最长不下降子序列。最长上升子序列只有1或2的情况,可以预处理区间内01串的个数。对于最长不下降子序列,一定是一段0接着一段1,对0出现的次数前缀和,1出现的次数后缀和,枚举一个端点1,答案就是max{pre[i]+suc[i]}-pre[l-1]-suc[r+1],ST表处理max值。
for(int i=1;i<=n;i++){
read(a[i]);
pre[i]=pre[i-1]+(a[i]==0);
if(i>1)sum[i]=sum[i-1]+(a[i]==1&&a[i-1]==0);
}
for(int i=n;i;i--)suc[i]=suc[i+1]+(a[i]==1);
for(int i=1;i<=n;i++)f[i][0]=pre[i]+suc[i];
for(int j=1;j<=20;j++)for(int i=1;i+(1<<j)-1<=n;i++)f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]);
while(q--){
int op,l,r;
read(op),read(l),read(r);
if(op==1)put(query(l,r)-pre[l-1]-suc[r+1]),putchar('\n');
else put(sum[l]==sum[r]?1:2),putchar('\n');
}
m个原数,若x经过若干次/d并向下取整的操作可以变成一个原数,那么x可以被这个原数消灭,原数不会被消耗,询问区间[l,r]消灭最多个数最少需要多少个原数。若x可以被一堆原数消灭,那么只需要记录最小的那个原数作为这一组的代表元素,这样肯定不会漏掉大的原数,二进制状压区间内的最大消除最小原数集合,ST表合并两个区间,查询时查询区间答案的1的个数即可。
sort(b+1,b+1+m);
m=unique(b+1,b+1+m)-b-1;
for(int i=1;i<=m;i++)id[b[i]]=i;
for(int i=1;i<=n;i++){
if(!b[1])f[i][0]=1;
else for(;a[i];a[i]/=d)if(id.find(a[i])!=id.end())f[i][0]=1ll<<id[a[i]]-1;
}
for(int j=1;j<=20;j++)for(int i=1;i+(1<<j)-1<=n;i++)f[i][j]=f[i][j-1]|f[i+(1<<j-1)][j-1];
while(q--){
int l,r,ans=0;
cin>>l>>r;
int k=log2(r-l+1),t=f[l][k]|f[r-(1<<k)+1][k];
while(t)ans++,t-=(t&-t);
cout<<ans<<'\n';
}
将一个序列恰好划分成连续非空k段,每段贡献为区间最大值,答案是每段贡献的gcd,最大化答案。答案肯定是全局最大值的约数,于是枚举约数g,设f(l,r)表示在答案为g时最多划分成多少个区间,[l,r]最大值位置为mid,若a[mid]%g==0,那么可以贪心将mid单独作为一段,否则若l>1,l-1这个位置一定是某个祖先的mid,[l,r]的最大值可以属于l-1所在区间,若r<n,[l,r]的最大值可以属于r+1所在区间,所以f(l,r)=max{[r<n]f(l,mid-1),[l>1]f(mid+1,r},查询最大值位置可以用ST表.
inline int pushup(int x,int y){
return a[x]>a[y]?x:y;
}
inline int query(int l,int r){
int k=log2(r-l+1);
return pushup(f[l][k],f[r-(1<<k)+1][k]);
}
int solve(int l,int r,int g){
if(l>r)return 0;
int mid=query(l,r);
if(a[mid]%g==0)return solve(l,mid-1,g)+1+solve(mid+1,r,g);
int re=0;
if(l>1)re=max(re,solve(mid+1,r,g));
if(r<n)re=max(re,solve(l,mid-1,g));
return re;
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>a[i],ma=max(ma,a[i]),f[i][0]=i;
for(int j=1;j<=20;j++)for(int i=1;i+(1<<j)-1<=n;i++)f[i][j]=pushup(f[i][j-1],f[i+(1<<j-1)][j-1]);
int i=1;
for(;i*i<=ma;i++)if(ma%i==0&&solve(1,n,ma/i)>=k)return cout<<ma/i,0;
for(--i;i;i--)if(ma%i==0&&solve(1,n,i)>=k)return cout<<i,0;
return 0;
}
从给定序列中选取k段连续区间,每段区间数字个数属于[l,r],问所选取区间的和的最大值。首先处理处前缀和,定义MAX(p,l,r)=max{sum[t]-sum[p-1]|l<=t<=r},即以p为左端点,右端点区间在[l,r]内的最大子段,sum[t]通过RMQ求解,ST表按照位置构建,每次选取最优子段,连续选k次即可,每次选过之后,(p,l,r)不能再选,但是最优解还可能在t的左右两部分,于是将(p,l,t-1)和(p,t+1,r)再扔进堆中,注意l=t和r=t的特判。
struct node{
int p,l,r,t;
inline node(int p,int l,int r):p(p),l(l),r(r),t(query(l,r)){}
inline friend bool operator<(node a,node b){return sum[a.t]-sum[a.p-1]<sum[b.t]-sum[b.p-1];}
};
for(int i=1;i<=n-l+1;i++)q.push({i,i+l-1,min(i+r-1,n)});
int ans=0;
while(k--){
int p=q.top().p,l=q.top().l,r=q.top().r,t=q.top().t;
q.pop();
ans+=sum[t]-sum[p-1];
if(l!=t)q.push({p,l,t-1});
if(r!=t)q.push({p,t+1,r});
}
cout<<ans;
求一个长n的数,满足m个限制条件(l1,r1,l2,r2),使得[l1,r1]的数和[l2,r2]的数都相等,问满足条件的数的个数。对应区间相等可以转化为对应点相等,但是肯定不能N2并查集连边,于是考虑倍增,f[i][j]表示左端点为i的长2j区间的集合根,合并时将每层对应合并,查询时需要最底层的集合数量,只能是长1的区间,于是我们拆分并查集,从最长的区间开始逐个枚举,每次找当前点的父亲,它和它的父亲都分成两半,前一半和前一半连边,后一半和后一半连边,这样相当于将较长区间并查集差分成了两个一半的并查集。由于最高位不能选0,答案就是9*10^(num-1).
int f[N][20];
inline void init(int n){for(int i=1;i<=n;i++)for(int j=0;j<=20;j++)f[i][j]=i;}
int find(int x,int k){return x==f[x][k]?x:f[x][k]=find(f[x][k],k);}
inline void merge(int x,int y,int k){x=find(x,k),y=find(y,k);if(x!=y)f[x][k]=y;}
inline int getans(int ans=0){for(int i=1;i<=n;i++)if(f[i][0]==i)ans=ans?ans*10ll%mod:9;return ans;}
for(int i=1;i<=m;i++){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
for(int j=20;~j;j--)if(l1+(1<<j)-1<=r1)dsu.merge(l1,l2,j),l1+=1<<j,l2+=1<<j;
}
for(int j=20;j;j--)for(int i=1;i+(1<<j)-1<=n;i++){
int p=dsu.find(i,j);
dsu.merge(i,p,j-1);
dsu.merge(i+(1<<j-1),p+(1<<j-1),j-1);
}
给定一棵单向带权树,求有多少点对满足(x,y)满足y在x的子树中且路径上的or和>=k.or和满足结合律和单调性,枚举每个y,当某个祖先x满足or和>=k时,x的所有祖先也都满足,但是此题卡空间,需要手动模拟dfs。
inline int find(int x){
int re=a[x];
if(re>=k)return x;
for(int i=20;~i;i--)if((re|sum[x][i])<k)re|=sum[x][i],x=fa[x][i];
return fa[x][0];
}
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
add(x,y);
sum[y][0]=a[fa[y][0]=x];
}
for(int i=1;i<=n;i++){
if(fa[i][0])continue;
dep[i]=1;
dfs(i);
break;
}
for(int j=1;j<=20;j++)for(int i=1;i<=n;i++)fa[i][j]=fa[fa[i][j-1]][j-1],sum[i][j]=sum[i][j-1]|sum[fa[i][j-1]][j-1];
for(int i=1;i<=n;i++)ans+=dep[find(i)];
void calc(int x){
s[++s[0]]=x;
f[s[0]][0]=a[x];
for(int i=1;(1<<i)<=s[0];i++)f[s[0]][i]=f[s[0]][i-1]|f[s[0]-(1<<i-1)][i-1];
int p=s[0]+1,v=0;
for(int i=20;~i;i--)if((1<<i)<p&&(f[p-1][i]|v)<k)v|=f[p-1][i],p-=1<<i;
ans+=p-1;
}
void solve(int x){
calc(x);
while(s[0]){
x=s[s[0]];
if(h[x])calc(e[h[x]].to),h[x]=e[h[x]].next;
else s[0]--;
}
}
for(int i=1;i<=n;i++){
if(f[i][0])continue;
solve(i);
break;
}
给出一些年份的降雨量,判断一个询问(y,x)表示x年是自y年以来降雨量最多的的真假或不定。y>=x则无解,x和y均未知则maybe,包括x和y的整个区间均已知则判断true或false,x和y均已知但中间未知则判断maybe或false,有一个未知,判断另一个与区间最大值的关系,maybe或false.注意每个询问的含义是x年的降雨量不超过y年的降雨量,对于所有y<z<x,z年的降雨量严格小于x年,ST表查询区间最大值即可。
for(int i=1;i<=n;i++){
int x;
cin>>x>>f[i][0];
mp[x]=i;
}
while(m--){
int x,y,flag,tx,ty,re;
cin>>y>>x;
auto itx=mp.lower_bound(x),ity=mp.lower_bound(y);
if(itx->first!=x&&ity->first!=y){
cout<<"maybe\n";
continue;
}
tx=itx==mp.end()?n+1:itx->second,ty=ity->second;
if(ity->first!=y)ty--;
re=ty+1<=tx-1?query(ty+1,tx-1):0;
if(ity->first!=y)flag=re<f[tx][0];
else if(itx->first!=x)flag=re<f[ty][0];
else flag=re<f[tx][0]&&f[tx][0]<=f[ty][0];
if(itx->first==x&&ity->first==y&&tx-ty==x-y)cout<<(flag?"true\n":"false\n");
else cout<<(flag?"maybe\n":"false\n");
}
丹钓战,初始空栈,向其中中加入元素(a,b)时,先不断弹出栈顶直到栈空或栈顶(aj,bj)满足ai!=aj且bi<bj,之后将其加入栈中,若一个二元组入栈后只有这一个元素,则称该二元组时成功的,每次询问[l,r]表示将若编号在[l,r]的二元组按编号从小到大依次入栈,有多少个二元组是成功的。怀念我第一次做NOI相关的题,这是T1。每次都是固定顺序插入某段元素,那么对于每个元素来说,最后都是固定的元素把它弹出,单调栈记录这个元素。对于询问,首先插入一个,ans++,弹出栈底后加入f[i],此时只有一个f[i],ans++,接下来还是找到下一个能把栈底弹出的数f[f[i]],直到该编号超过了r。于是问题转化成从l开始沿着f数组往后跳,问跳几次会超过r,倍增处理,最开始单独一个l肯定是成功的。
for(int i=1;i<=n;i++){
while(s[0]&&(a[i]==a[s[s[0]]]||b[i]>=b[s[s[0]]]))f[s[s[0]--]][0]=i;
s[++s[0]]=i;
}
for(int j=1;j<=20;j++)for(int i=1;i<=n;i++)f[i][j]=f[f[i][j-1]][j-1];
while(q--){
int l,r;
cin>>l>>r;
int re=1,p=l;
for(int i=20;~i;i--)if(f[p][i]&&f[p][i]<=r)p=f[p][i],re+=1<<i;
cout<<re<<'\n';
}
标签:RMQ,return,int,mid,ST,--,区间,inline,倍增
From: https://www.cnblogs.com/safeng/p/16889878.html