ST表(RMQ问题)
算法理解
RMQ问题就是对与区间最值查询一类问题的总称
对于RMQ问题的求解主要有以下两种方式:
- 线段树,建树O(n),查询O(logn),支持在线修改
- ST表,预处理O(logn),查询O(1),不支持在线修改
这个单元主要讲解的是ST表
倍增思想
考虑一个数必然能被拆分成二进制,所以我们先预处理出 \(2^k\) 的答案,对于一次查询,我们直接将查询分为二进制各个位,按位依次求解即可,通常搭配预处理使用
ST表
本质上是对区间进行一个拆分,拆成一块一块,然后对块进行查询
正常的分块一个块长是 \(\sqrt n\),因为不需要进行修改操作(个人理解),所以我们可以将区间拆分的更细致一些,以至于块与块之间可以重叠
结合倍增思想,我们选择将块长定为 \(2^k\),对于每一个端点都求出从这个端点出发,覆盖 \(2^k\) 个点的区间的最值,具体拆分方式见下图
预处理
因为静态询问所以我们先预处理出这个倍增数组,因为 \(2^k=2^{k-1}+2^{k-1}\) 所以 \(2^k\) 的答案可以通过 \(2^{k-1}\) 递推得来,因为递推本质上是递推,所以我们将这个数组设为 \(dp[i][j]\) 表示从i开始经过 \(2^j\) 个点的区间的最大值
查询
我们想要查询 \((l,r)\) 的区间最值,可以将其拆分成两个可以重叠的区间,区间长度就是 \(log2(r-l+1)\) 就是最大的二进制位,第一个区间的左端点就是l,第二个区间的右端点就是r
\(ans=max(dp[l][k],dp[r-(1<<k)+1][k]))\)
代码
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,m,l,r;
int a[N],dp[N][40];
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
dp[i][0]=a[i];
}
for(int j=1;j<=32;j++){
for(int i=1;i+(1ll<<j)-1<=n;i++){
dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
}
for(int i=1;i<=m;i++){
scanf("%lld%lld",&l,&r);
int k=log2(r-l+1);
printf("%lld\n",max(dp[l][k],dp[r-(1<<k)+1][k]));
}
}
T1:
注意只要用到long long 你的位运算就要用 \(1ll<<j\) ,否则会Re
T2:
直接把max改成gcd即可
考虑两个区间可以合并,当且仅当区间重复计算并不会对答案产生影响,因为查询时两个区间是重叠的
那我可不可以将ST表改成倍增我对区间(l,r),进行一个二进制拆分,然后使得区间查询时并不重复,就能实现O(logn)静态维护区间加区间乘呢?(问题暂且保留)
突然发现自己唐氏了,您是否再找前缀和?!!
T3:
ST表比较复杂的一道题
首先我们先不考虑区间查询,我们只找以第i个数结尾的最长不同区间的长度 \(f[i]\)
这个是很好求的只需要记录一下\(a[i]\) 上一次出现的位置 \(pri[a[i]]\) ,所以 \(f[i]=max(f[i-1]+1,i-pri[a[i]])\)
然后考虑一下对于一个询问 \((l,r)\) 的答案,为什么直接查询 \(max_{i=l}^r(f[i])\) 不行呢?
因为在 \((l,r)\) 之间存在位置 \(now\) 使得 \((l,now)\) 这部分的 \(f[i]\) 所对应的左端点是小于l的,显然在now之后的所有区间的 \(f[i]\) 的左端点一定是大于等于l的,所以对于now的位置的枚举是满足单调性的
我们只需要二分的找到now的位置,显然在now之前的区间对于(l,r)的贡献是 \(now-l+1\)
所以最后答案统计即为
\(ans=max(now-l+1,max(dp[now+1][k],dp[r-(1ll<<k)+1][k]))\)
注意几点:
1.数组下标从0开始
2.a[i]存在负数,要用map
T4:
经典二维题(我差亿点就做出来了)
预处理
先初始化 \(dp[i][0][j][0]=a[i][j]\)
然后再考虑线性做法,从 \(2^0\) 开始递推,然后推出整个线性的表
我们先推出当x的还没开始倍增时(即\(2^0\)时)y的表,相当于开n个线性st表,然后求完答案后在让x坐标倍增,因为y的答案已经求完了,所以直接递推即可
查询
我们将一个区间拆成4个可重区间
然后求得答案即可
代码
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=255,M=40;
int n,m,k,l1,l2,r1,r2;
int a[N][N],dp[N][M][N][M];
signed main(){
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%lld",&a[i][j]);
dp[i][0][j][0]=a[i][j];
}
}
for(int j1=0;j1<=31;j1++){
for(int j2=0;j2<=31;j2++){
if(!j1&&!j2) continue;
for(int i1=1;i1+(1ll<<j1)-1<=n;i1++){
for(int i2=1;i2+(1ll<<j2)-1<=m;i2++){
if(!j1) dp[i1][j1][i2][j2]=max(dp[i1][j1][i2][j2-1],dp[i1][j1][i2+(1ll<<(j2-1))][j2-1]);
else dp[i1][j1][i2][j2]=max(dp[i1][j1-1][i2][j2],dp[i1+(1ll<<(j1-1))][j1-1][i2][j2]);
}
}
}
}
for(int i=1;i<=k;i++){
scanf("%lld%lld%lld%lld",&l1,&r1,&l2,&r2);
int k1=log2(l2-l1+1),k2=log2(r2-r1+1);
int ans=max(max(dp[l1][k1][r1][k2],dp[l2-(1ll<<k1)+1][k1][r1][k2]),max(dp[l1][k1][r2-(1ll<<k2)+1][k2],dp[l2-(1ll<<k1)+1][k1][r2-(1ll<<k2)+1][k2]));
printf("%lld\n",ans);
}
}
T6:
哈哈,笑死,我看到这道题的查询区间和竟然想到对ST表的查询进行二进制拆分,怀着发现新大陆的心情去隔壁机房问唐老师
然后唐老师说:前缀和直接做就做完了(っ °Д °;)っ
呃然后我又码了一个二维st表板子上去,喜提60+MLE,然后打开题解,才发现有一条特殊性质
每个询问的方阵的较长边不超过较短边的两倍
考虑我们先想一下这个特殊的性质,也就是说这个区间的 \(k1,k2\) (x与y的区间长度的log)并不会相差超过1,所以我们只需要预处理出一个倍增边长的正方形st表即可
简单来说就是将 \(dp[i][k1][j][k2]\) 变为 \(dp[i][j][k]\) 对答案并不产生影响
预处理
把一个正方形拆成4个子正方形合并答案,因为子正方形的边长一定时正方形边长的一半,所以满足合并关系
查询
先将矩形沿长边拆成两个正方形,再对正方形进行拆成4个可重正方形,进行答案统计即可
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=805;
int n,m,q,l1,l2,r1,r2;
int a[N][N],num[N][N],sum[N][N],mx[N][N][16],mn[N][N][16];
char s[N];
int maxans(int l1,int r1,int l2,int r2){
int k=log2(min(l2-l1+1,r2-r1+1));
return max(max(mx[l1][r1][k],mx[l1][r2-(1<<k)+1][k]),max(mx[l2-(1<<k)+1][r1][k],mx[l2-(1<<k)+1][r2-(1<<k)+1][k]));
}
int minans(int l1,int r1,int l2,int r2){
int k=log2(min(l2-l1+1,r2-r1+1));
return min(min(mn[l1][r1][k],mn[l1][r2-(1<<k)+1][k]),min(mn[l2-(1<<k)+1][r1][k],mn[l2-(1<<k)+1][r2-(1<<k)+1][k]));
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%d",&a[i][j]);
num[i][j]=num[i][j-1]+a[i][j];
sum[i][j]=sum[i-1][j]+num[i][j];
mx[i][j][0]=a[i][j];
mn[i][j][0]=a[i][j];
}
}
for(int k=1;k<=15;k++){
for(int i=1;i+(1<<k)-1<=n;i++){
for(int j=1;j+(1<<k)-1<=m;j++){
mx[i][j][k]=max(max(mx[i][j][k-1],mx[i+(1<<(k-1))][j][k-1]),max(mx[i][j+(1<<(k-1))][k-1],mx[i+(1<<(k-1))][j+(1<<(k-1))][k-1]));
mn[i][j][k]=min(min(mn[i][j][k-1],mn[i+(1<<(k-1))][j][k-1]),min(mn[i][j+(1<<(k-1))][k-1],mn[i+(1<<(k-1))][j+(1<<(k-1))][k-1]));
}
}
}
scanf("%d",&q);
for(int i=1;i<=q;i++){
scanf("%s",s+1);
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
l1++,l2++,r1++,r2++;
if(s[1]=='S'){
printf("%d\n",sum[l2][r2]+sum[l1-1][r1-1]-sum[l1-1][r2]-sum[l2][r1-1]);
}
else if(s[2]=='A'){
int c1=l2-l1,c2=r2-r1;
if(c1>c2){
printf("%d\n",max(maxans(l1,r1,l1+c2,r2),maxans(l2-c2,r1,l2,r2)));
}
else{
printf("%d\n",max(maxans(l1,r1,l2,r1+c1),maxans(l1,r2-c1,l2,r2)));
}
}
else{
int c1=l2-l1,c2=r2-r1;
if(c1>c2){
printf("%d\n",min(minans(l1,r1,l1+c2,r2),minans(l2-c2,r1,l2,r2)));
}
else{
printf("%d\n",min(minans(l1,r1,l2,r1+c1),minans(l1,r2-c1,l2,r2)));
}
}
}
}
T7:
咋一眼看:板子题
2h后。。。
真tm屎
先看年份的值域 \(-1e9~1e9\) 首先先离散化一下,要不然不能做st表
然后对于一个查询 \((a,b)\) 我们二分找到它所对应的离散化后的下标 \((u,v)\),但是考虑会有a,b对应的下标不存在的情况,于是就开始了我们的分讨之旅