ST表(跳表)
倍增
- 倍增是一种思想,也是一个算法
- 倍增比二分的常数要大,但比二分更好写
int x=-1; for(int i=31;~i;--i) if(pan(x+(1<<i)) x+=(1<<i); // ~i的意思是i!=-1
- 倍增在树上和链上更常见
ST表
跳表用于只需查询的问题中【把需要的信息预处理】
ST表又叫跳表
ST表有两种,一种是根号跳表,另一种是倍增跳表
跳表是为了处理静态查询问题,线段树既可以处理静态查询,也可以处理动态查询
区间长度为n【1e5的话】,查询m次
跳表相对于线段树的优势在于,其单次查询是O(1)的,线段树的查询是O(lgn)的【线段树查询可能跑不满 lg】
而相对而言,其预处理是O(nlgn)【lg跑满了】,线段树建树也是O(nlgn)【lg跑不满】
所以跳表在查询次数非常大时,才能发挥优势,否则直接上线段树吧
所以m<=n时,跳表不具优势,但 m=10N 时,倍增跳表时间优,m=100N 时,根号跳表时间更优【根号跳表比倍增跳表需要的空间要大】
st表的本质:st[i][j]表示以i为起点,长度为 j 的区间信息,其本质就是预处理区间信息(不需要后期维护)
根号跳表
根号跳表原理: n可以分解为sqrt(n)*sqrt(n) 任何一个数x都可以写成 a*sqrt(n)+b //a小于sqrt(n),b也小于sqrt(n) st_small[N][sqrt(N)]; //小表,st_sm[i][j]表示以i为起点,长度为j的区间最值 // j<=sqrt(n) st_big[N][sqrt(N)]; //大表,st_big[i][j]表示以i为起点,长度为j*sqrt(n)的区间最值 //j<=sqrt(n) int z=sqrt(n); st_sm[i][1]=a[i]; st_sm[i][j]=max(st_sm[i][j-1],a[i+j-1]); st_big[i][1]=st_sm[i][z]; st_big[i][j]=max(st_big[i][j-1],st_sm[i+(j-1)*z][z]); int ask(int l,int r){ int len=r-l+1; return max(st_big[l][len/z],st_sm[l+len/z*z][len-(len/z*z)]); //也可以写成%,但为了好理解,写成减比较好 }
#include<bits/stdc++.h> using namespace std; int const N=1e5+7; int const L=3e2+7; int n,m,bl; int a[N]; int sts[N][L],stb[N][L]; int ask(int l,int r){ int len=r-l+1; return max(stb[l][len/bl],sts[l+len/bl*bl][len-len/bl*bl]); // } int main(){ cin >> n >> m; for(int i=1;i<=n;++i) cin >> a[i]; bl=ceil(sqrt(n)); for(int i=1;i<=n;++i){ for(int j=1;j<=bl&&i+j-1<=n;++j){ //&&i+j-1<=n 为实际意义的限制,既可以防止数组越界,又可以优化时间常数 //以后这种情况都这么写(for有好几个限制时,把条件全部写出来),这么写不会错,不写还要考虑是否越界,还会增大时间常数 if(j==1) sts[i][j]=a[i]; else sts[i][j]=max(sts[i][j-1],a[i+j-1]); } } for(int i=1;i<=n;++i){ for(int j=1;j<=bl&&i+j*bl-1<=n;++j){ //&&i+j*bl-1<=n //j<=bl可以省略 if(j==1) stb[i][j]=sts[i][bl]; else stb[i][j]=max(stb[i][j-1],sts[i+(j-1)*bl][bl]); // } } int l,r; while(m--){ cin >> l >> r; cout << ask(l,r) << "\n"; } return 0; }
倍增跳表
用于查询可重复合并的区间信息,比如 RMQ,GCD等等,也就是多次合并不影响结果
倍增跳表原理: st[N][lg(N)]; //st[i][j]表示以i为起点,长度为2^j的区间最值 st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]); int ask(int l,int r){ int k=log2(r-l+1); //向下取整 return max(st[l][k],st[r-(1<<k)][k]); } log2不要调用系统函数 众所周知,在处理整数问题上,pow,log,log2,sqrt等函数都比较有问题 第一是精度问题【它们是先转换成小数运算,再转换回整数】,第二是时间常数 预处理log2 如果范围不大,可以暴力预处理 LG2[1]=0; for(int i=2;i<n;++i) LG2[i]=LG2[i/2]+1; //因为对数【logx(y)】的实际意义就是y能除以x几次,与幂的定义【运算】相反,幂【x^y】是x乘y次的结果 扩展: LGX[1]=0; for(int i=2;i<n;++i) LGX[i]=LGX[y/x]+1;
#include<bits/stdc++.h> using namespace std; #define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define ll long long int const N=1e5+7; int n,m; int a[N]; int st[N][20]; void build(){ for(int i=1;i<=n;++i) st[i][0]=a[i]; for(int j=1;(1<<j)<=n;++j){ for(int i=1;i<=n&&i+(1<<j)-1<=n;++i){ //i<=n可以省略 st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]); } } } int LG2[N]; void init(){ LG2[1]=0; for(int i=2;i<=n;++i) LG2[i]=LG2[i/2]+1; //这里的LG2是lg2向下取整 } int ask(int l,int r){ int len=r-l+1; int k=LG2[len]; return max(st[l][k],st[r-(1<<k)+1][k]); } int main(){ fio; cin >> n >> m; for(int i=1;i<=n;++i) cin >> a[i]; build(); init(); int l,r; while(m--){ cin >> l >> r; cout << ask(l,r) << "\n"; } return 0; }