Living-Dream 系列笔记 第66期
RMQ 问题 / ST 表:静态区间求最值。
实现(以最大值为例):
-
倍增 dp,预处理 \(st_{i,j}\) 表示区间 \([i,i+2^j-1]\) 内的最大值,我们有转移方程:
\[st_{i,j}=\max(st_{i,j-1},st_{i+2^{j-1},j-1}) \]相当于是把 \([i,i+2^{j-1}-1]\) 与 \([i+2^{j-1},i+2^j-1]\) 这两段区间的最大值拼了起来。
(倍增中常把 \(2^j\) 拆分为 \(2^{j-1}+2^{j-1}\))
-
对于每组询问 \([l,r]\),找到大于等于区间长度一半的二次幂(实际上易证这就是 \(\log_2 \ r-l+1\)),从 \(l,r\) 分别找,从而覆盖 \([l,r]\)。
\[\begin{cases} j=\log_2 r-l+1\\ ans=\max(dp_{l,j},dp_{r-2^j+1,r}) \end{cases} \]
P3865
板子。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5,M=31; int n,m,a[N]; int dp[N][M]; void STinit(){ for(int i=1;i<=n;i++) dp[i][0]=a[i]; for(int j=1;(1<<j)<=n;j++) for(int i=1;i+(1<<j)-1<=n;i++) dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]); } int STquery(int l,int r){ int t=log(r-l+1)/log(2); return max(dp[l][t],dp[r-(1<<t)+1][t]); } signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) cin>>a[i]; STinit(); while(m--){ int l,r; cin>>l>>r; cout<<STquery(l,r)<<'\n'; } return 0; }
CF689D
首先发掘性质,\(\max^{r}_{i=l} b_i\) 显然单调不减, \(\min^{r}_{i=l} a_i\) 显然单调不增,这说明可以只枚举起点,二分终点,从而确定区间。
接着我们发现两者相等的必定为一段连续区间,于是我们就二分查找满足 \(\max^{r}_{i=l} b_i \le \min^{r}_{i=l} a_i\) 的最大的 \(l1\),以及满足 \(\max^{r}_{i=l} b_i \ge \min^{r}_{i=l} a_i\) 的最大的 \(r2\),这个 \(l\) 对答案的贡献即为 \(l1-r2+1\)。
注意最后要在判断一次 \(\max^{r}_{i=l} b_i = \min^{r}_{i=l} a_i\),因为有可能找不到 \(l1,r2\)。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+5,M=31; int n,m,ans; int a[N],b[N]; int dpa[N][M],dpb[N][M]; void STinitA(){ for(int i=1;i<=n;i++) dpa[i][0]=a[i]; for(int j=1;(1<<j)<=n;j++) for(int i=1;i+(1<<j)-1<=n;i++) dpa[i][j]=max(dpa[i][j-1],dpa[i+(1<<(j-1))][j-1]); } void STinitB(){ for(int i=1;i<=n;i++) dpb[i][0]=b[i]; for(int j=1;(1<<j)<=n;j++) for(int i=1;i+(1<<j)-1<=n;i++) dpb[i][j]=min(dpb[i][j-1],dpb[i+(1<<(j-1))][j-1]); } int STqueryA(int l,int r){ int t=log(r-l+1)/log(2); return max(dpa[l][t],dpa[r-(1<<t)+1][t]); } int STqueryB(int l,int r){ int t=log(r-l+1)/log(2); return min(dpb[l][t],dpb[r-(1<<t)+1][t]); } signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=n;i++) cin>>b[i]; STinitA(),STinitB(); for(int i=1;i<=n;i++){ int l1=i-1,r1=n+1; int l2=i-1,r2=n+1; while(l1+1<r1){ int mid=(l1+r1)>>1; if(STqueryA(i,mid)<=STqueryB(i,mid)) l1=mid; else r1=mid; } while(l2+1<r2){ int mid=(l2+r2)>>1; if(STqueryA(i,mid)>=STqueryB(i,mid)) r2=mid; else l2=mid; } if(STqueryA(i,l1)==STqueryB(i,l1)&&STqueryA(i,r2)==STqueryB(i,r2)) ans+=l1-r2+1; } cout<<ans; return 0; }
LOJ #10121
首先,容易发现一条性质:若有一个确定的最长完美序列 \([i,j]\),则一定不存在完美序列 \([k,j]\),其中 \(k\) 满足 \(k<i\)。这说明完美序列存在单调性。
然后对于一组询问 \([l,r]\),考虑进行分割,哪部分可以更好算出贡献,就以它进行分割。
在这里,我们发现可能存在一个点 \(p\),使得以区间 \([l,p)\) 内的点为右端点的完美序列的左端点 \(L_1\) 满足 \(L_1 < l\),而以区间 \([p,r]\) 内的点为右端点的完美序列的左端点 \(L_2\) 满足 \(l \le L_2 \le r\)。
因为单调性的存在,所以 \(p\) 可以二分查找得到。
计算 \([l,p)\) 内的贡献是简单的,即为 \((p-1)-L_1+1=p-L_1\)。
对于每个 \(i \in [p,r]\),它贡献为 \(i-L_2+1\),用 ST 表维护这个式子的 \(\max\) 即可。
于是 \([l,r]\) 中的最长完美序列的长度即为 \(\max(p-L_1,\operatorname{query}(p,r))\)(\(\operatorname{query}\) 为 ST 表中的查询)。
接下来就是 \(L_1,L_2\) 的维护。
令 \(last_{a_i}\) 表示 \(a_i\) 上一次出现的位置,\(st_i\) 表示以 \(i\) 为右端点的最长完美序列的左端点。
显然有转移:
(可以继承 \(st_{i-1}\) 是因为有单调性)
最后注意下标从 \(0\) 开始,以及 \(a_i\) 可能为负,\(last_{a_i}\) 要用 \(\operatorname{map}\) 或者平移也行。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+5,M=31; int n,m,a[N]; int dp[N][M]; int st[N]; int l,r; map<int,int> last; void STinit(){ for(int i=1;i<=n;i++) dp[i][0]=i-st[i]+1; for(int j=1;(1<<j)<=n;j++) for(int i=1;i+(1<<j)-1<=n;i++) dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]); } int STquery(int l,int r){ int t=log(r-l+1)/log(2); return max(dp[l][t],dp[r-(1<<t)+1][t]); } int bin(int l,int r){ int lt=l-1,rt=r+1; while(lt+1<rt){ int mid=(lt+rt)>>1; if(st[mid]<l) lt=mid; else rt=mid; } return rt; } signed main(){ ios::sync_with_stdio(0); cin.tie(0),cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=n;i++){ st[i]=max(st[i-1],last[a[i]]+1); last[a[i]]=i; } STinit(); while(m--){ //int l,r; cin>>l>>r; l++,r++; int p=bin(l,r); if(p>r) cout<<r-l+1<<'\n'; else cout<<max(p-l,STquery(p,r))<<'\n'; } //cout<<ans; return 0; }
CF1142B
对于 \([l,r]\) 中的每个数 \(i\),我们都尝试将其作为 \(p\) 的循环移位的终点,
从 \(i\) 不断往前跳 \(n-1\) 次,记录它在 \(a_i\) 能跳到的点 \(b_i\),
把 \(b_i\) 扔进 ST 表中,对于每组询问 \([l,r]\) 取 \(\max\),
若这个值 \(\ge l\),则说明存在,否则不存在。
具体实现细节见 code。
code
#include<bits/stdc++.h> using namespace std; const int N=2e5+5,M=31; int n,m,q; int p[N],a[N]; int pos[N],last[N],b[N],st[N][M]; void STinit1(){ //memset(st,0,sizeof st); for(int j=1;j<=25;j++) for(int i=1;i<=m;i++) st[i][j]=st[st[i][j-1]][j-1]; } void STinit2(){ memset(st,0,sizeof st); for(int i=1;i<=m;i++) st[i][0]=b[i]; for(int j=1;(1<<j)<=m;j++) for(int i=1;i+(1<<j)-1<=m;i++) st[i][j]=max(st[i][j-1],st[i+(1<<j-1)][j-1]); } int STjump(int x){ int now=0; for(int i=25;i>=0;i--){ if(now+(1<<i)<n) x=st[x][i], now+=(1<<i); } return x; } int STquery(int l,int r){ int t=log2(r-l+1); return max(st[l][t],st[r-(1<<t)+1][t]); } int main(){ //last[x]表示x在a中的下标 //pos[x]表示x在p中的下标 //st[i][j]表示i跳回2^j步所到的点 cin>>n>>m>>q; for(int i=1;i<=n;i++) cin>>p[i],pos[p[i]]=i; for(int i=1;i<=m;i++){ cin>>a[i]; if(pos[a[i]]==1) st[i][0]=last[p[n]]; else st[i][0]=last[p[pos[a[i]]-1]]; last[a[i]]=i; } STinit1(); for(int i=1;i<=m;i++) b[i]=STjump(i); STinit2(); while(q--){ int l,r; cin>>l>>r; cout<<(STquery(l,r)>=l?1:0); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现