倍增和ST表
倍增与ST表
1.什么是倍增?
顾名思义,成倍增长就是倍增。
任何一个数字都可以表示为一个二进制数。
\(N = a_02^0+a_12^1+a_22^2+...\)
一个数字n如果用二进制表示那么就是\(\log_2n\).
显然这是一个优化思路。
快速幂就是这个思路。
2.ST表
ST表可以处理静态的RMQ问题。
RMQ问题:区间最值问题
一个长度为n的序列,给出m个查询。
每一次查询[l,r]的最值。
显然我们可以暴力的使用\(O(nm)\)的时间复杂度来处理这个问题。
但是使用ST算法来处理这个问题就会达到\(O(nlogn)\)的时间复杂度。
预处理\(O(nlogn)\),每次查询时间复杂度为\(O(1)\)。总计\(O(nlogn)\)。
a.原理
以最大值为例:
首先我们要理解下面这个简单的事实:
定义dp[l][r] 是区间的最大值
一定有 \(dp[l][r] = max(dp[l][t],dp[s][r])\)其中t>=s,t<=r,s>=l。
这里表明一个大区间的的最值一定是两个完全覆盖的小区间的最值的一个。
如下所示(不想做图来着)
l------------------r
l--------t*********
******s-----------r
在此基础上我们结合倍增与DP思想建立ST表(从某种角度来说DP不就是建一个记忆化的表格吗)
我们从小区间往大区间建表。
第1轮是长度为1的小区间,有n个小区间,每个小区间有1个元素;
他是初始状态。
第2轮是长度为2的小区间,有n个小区间,每个小区间有2个元素;
它可以从第一轮的状态转移
第3轮是长度为4的小区间,有n个小区间,每个小区间有4个元素;
它可以从第二轮的状态转移。
.......
一共有\(\log_2n\)轮。
b.建表
我们现在稍微优化一下dp数组
我们定义:
dp[s][k]表示以s为起点的,长度为\(2^k\)的区间的最值。
转移方程为:
\(dp[s][k] = max(dp[s][k-1],dp[s+1<<(k-1)][k-1]);\)
注意这里的k是小于等于logn的。
时间花费为: O(nlogn)。
c.查询
之前已经讲到过,每一个大区间的最值是由两个小区间的最值确定的。
那么我们如何来确定这个小区间的长度呢?
其实很明显了。假设大区间\([l,r]\),设\(k = \log_2(r-l+1)\)
那么小区间为\([l,l+k]与[r-(1<<k)+1,r]\)
这两个区间是一定有对应的dp数组的。(因为我们预处理了所有的长度为\(2^k\)的区间)
3.实例代码
[https://www.luogu.com.cn/problem/P2880]洛谷P2880
//头文件太长了影响美观就删了
int n,q;//n个结点,q次查询
int h[N];//每个结点的权值
int dp_max[N][22],dp_min[N][22];
//dp[s][k]表示以s为起点,区间长度为2的k次方的区间最值
int Log2[N];//预处理方式,加速方法
void init(){
Log2[0] = -1;//这里是-1注意
for(int i = 1;i <= N;i++) Log2[i] = Log2[i>>1]+1;
for(int i = 1;i <= n;i++) dp_max[i][0] = dp_min[i][0] = h[i];
int p = Log2[n];//轮次
for(int k = 1;k <= p;k++){
for(int s = 1;s + (1<<k) <= n+1;s++){
dp_max[s][k] = max(dp_max[s][k-1],dp_max[s+(1<<(k-1))][k-1]);
dp_min[s][k] = min(dp_min[s][k-1],dp_min[s+(1<<(k-1))][k-1]);
}
}
}
int solve(int l,int r){
int k = Log2[r-l+1];
int x = max(dp_max[l][k],dp_max[r-(1<<k)+1][k]);
int y = min(dp_min[l][k],dp_min[r-(1<<k)+1][k]);
//cout<<k<<' '<<x<<' '<<y<<endl;
return x-y;
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>q;
for(int i = 1;i <= n;i++) cin>>h[i];
init();
while(q--){
int a,b;
cin>>a>>b;
cout<<solve(a,b)<<endl;
}
return 0;
}