ST表&倍增&RMQ问题
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'; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】