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对应的下标不存在的情况,于是就开始了我们的分讨之旅

T8:

posted @ 2024-11-07 10:05  daydreamer_zcxnb  阅读(13)  评论(0编辑  收藏  举报