倍增
- 倍增是一种思想,也是一个算法
- 倍增比二分的常数要大,但比二分更好写
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;
}

京公网安备 11010502036488号