【模板】ST 表
传送门:
P3865 【模板】ST 表 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路
学习ST表之前最重要的还是了解倍增的思路
与其说是倍增这种高深的词汇,还不如就说十进制转二进制,二进制转十进制
十进制和二进制的互转,无非其重点就是任意十进制都可以转换成任意二进制,并且可以利用二进制每一位只为0 or 1的特性来使得题目的 时间复杂度 和 空间复杂度 大幅度降低
我们以求区间最大值来引入ST表
例题:
给定一个长度为 N 的数列,和 M 次询问,求出每一次询问的区间内数字的最大值。
遇到这种题目是不是想直接暴力
for(int i=l;i<=r;i++){//1次最大值
MAX=max(MAX,a[i]);
}
但是n和m很大呢?r比l大很多呢?重复询问很多呢? 是不是肯定超时
有人说打表,打表虽然虽然可以解决重复询问和每次计算的过程,但是如果n,m的值很大,预处理的时候就已经超时了
好了这时候我们可以引入ST表了
我们可以求每段长度的一半的最大值进行比较然后最后求出最大值
(画的比较丑勿喷)
以这种方式求区间最大值,第二层的第一个结点就是由他的两个子节点比较得出,第二层的第二个结点也是由他的两个子节点比较得出;然后往上往上一直到得出区间最大值为止。(实际上是个递归的过程,就是从最长的那个区间往下二分二分,分到每个结点只有一个数然后回溯,回溯;但是为了方便理解用dp来求出st表,所以用递推的方式描述)
这里大家可能有很多问题,,,,
-
为什么这是一直二分就可以求出最大值,那原来的数的个数不是2的次幂呢?
其实是没有关系的,如果我的二叉树中比如说,第二层第一个结点实际上有5个数,那是不是按照二分思路,他的两个子节点各包含3个结点(这边因为宁愿多数也不愿少数,多数可以处理,少数的话还要加一条分支就不能是二叉树了,实现细节这里不要纠结),那么事实上现在这两个子节点一共由6个数,有一个数是重复的,那这个重复的数是肯定不会影响第二层第一个结点的最大值的,思考一下,如果重复的是一个小的数那是不是不影响最大值,如果重复的是一最大值,那最大值还是最大值。
-
为什么跟其他st表的题解有点不一样?
这里直接写了优化后的,如果是非优化前,需要以树的思路来思考,码量和时间复杂度都要提高。这边我们直接讲二叉树和优化后的思路,如果有兴趣,可以熟练了之后回去看看。
如果还有问题欢迎在评论区私信!!!
开始步入正途!!!
构建ST表
//在这之前我们要知道dp[i][j]表示什么?(前面铺垫的二叉树和进制互转就是为了理解这个,当然那只是为了理解折半线段后重复数不影响最大值的求解,当然那里不理解我们接着往下看也能理解这道题为什么这样做)
//表示以第i个数到第i+2^j-1个数的最大值
void InitSt(){
rep(i,1,n) dp[i][0]=a[i];//初始化第i到第i+0个数(也就是只有自己的时候)的最大值就是自己
for(int j=1;j<=log2(n);j++){//上图中已描述二叉树的思路,这边实则就是二叉树的高度-1
//为什么是要-1呢?
//因为j表示第第j步推到第j+1的步骤,
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]);
//那么第i个数到第i+2^j-1个数的最大值就是第i个数到第i+2^(j-1)个数和第i+2^(j-1)到i+(2^j-1)个数的较大值,那是不是左边的一半和右边的一半(这里讲的一半是有可能有重复数的,因为我们只有了二分而不是将每一部分的j作比较)比较的较大值就是i到i+2^j-1的最大值
}
}
}
构建完了ST表是不是还有问题?
-
上面括号里讲的这个(这里讲的一半是有可能有重复数的,因为我们只有了二分而不是将每一部分的j作比较)是什么意思因为j表示的是2j,所以每一部分的j就表示的是 2a+2b+2c(这里abc表示不同时刻的j)也就是此时是没有重复部分的,正好上面二进制之和是原十进制数的个数(进制转换!!!思考思考)。
-
为什么要将这个for(int j=1;j<=log2(n);j++)放在外面,for(int i=1;i+(1<<j)-1<=n;i++)放在里面?
理解广搜的思路,如果换了顺序就是深搜的思路,因为该二叉树的上层的最大值是与子节点的最大值相关的,所以不适合用深搜。
这题的关键还是对于这个状态转移方程的理解,可以自己配合画二叉树来理解
查询
int query(int l,int r){
int x=(int)log2(r-l+1);//2^x就是表达包含的数的个数那么我们r-l+1==2^x,x就等于这个了
return max(dp[l][x],dp[r-(1<<x)+1][x]);//这一步不讲了吧,与上面状态转移方程的思路是一样的
}
总结
构建ST表实则就是构建二叉树的每一个结点,
而查询实则就是找两个匹配的结点比较
这里思路有问题可以评论或私信我,大家一起学习进步!
AC code
// Problem:
// P3865 【模板】ST 表
//
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3865
// Memory Limit: 125 MB
// Time Limit: 800 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<iostream>
#include<algorithm>
//#include<cstdio>
#include<cmath>
#define ll long long
#define endl '\n'
#define rep(i,a,b) for(int i=(a);i<=(b);i++)//表示遍历[a,b]
#define per(i,a,b) for(int i=(a);i>(b);i--)
#define N 100010 //1e6+100
#define M 20
using namespace std;
int n,a[N],m;
int dp[N][M];//表示当前第i个数到第i+2^j-1个数的最大值
void InitSt(){
rep(i,1,n) dp[i][0]=a[i];//初始化第i到第i+0个数的最大值就是自己
for(int j=1;j<=log2(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 query(int l,int r){
int x=(int)log2(r-l+1);//两端最大,其实跟建立st表的那个状态转移方程是一样的
return max(dp[l][x],dp[r-(1<<x)+1][x]);
}
int main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);//输入数据量较大如果用cin cout输入输出是需要关闭输出流的
cin>>n>>m;
rep(i,1,n) cin>>a[i];
int l,r;
InitSt();
rep(i,1,m){
cin>>l>>r;
cout<<query(l,r)<<endl;
}
return 0;
}
有问题请评论或私信我,谢谢!
有共同学习需求的可以加入洛谷团队:https://www.luogu.com.cn/team/66731