7.1 倍增
倍增呢,我认为应该算是一种思想,算不上一种算法
一般来讲,我们常将倍增和二进制思想结合,不仅可以很好的降低时间复杂度,还可以很好的降低空间复杂度
其实,我们在很久很久以前就已经接触了倍增和二进制思想,比如快速幂和多重背包的优化
倍增本身没有什么好说的,如果只考倍增,要么巨水,要么根本想不到要用倍增;如果不是只考倍增,那么一般是以一个对原算法的优化的面孔出现
所以,接下来我们将了解与倍增有关的问题——区间最值问题( RMQ 问题)
7.2 RMQ 问题
一句话概括:
现有一个长度为 N N N 的一个序列和 M M M 次询问,对于每一次询问,求出区间 [ l , r ] [\ l,r\ ] [ l,r ] 的最大(小)值
先考虑暴力做法
7.2.1 RMQ 问题暴力求解
暴力应该是显而易见的,对于每一次询问,我们再来一重询问来求出该区间内的最大值并输出即可
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,x,y;
int a[100005];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
} //输入序列
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y); //输入询问
int ans=-2147483647;
for(int j=x;j<=y;j++){
ans=max(ans,a[j]); //遍历区间,求出最大值
}
printf("%d\n",ans); //输出
}
return 0;
}
但是,显然,这样的算法是 O ( n m ) O(nm) O(nm) ,而数据范围相当大: 1 ≤ n ≤ 1 0 5 , 1 ≤ m ≤ 1 0 6 1\le n\le10^5,1\le m\le10^6 1≤n≤105,1≤m≤106
自然,超时不可避免的 QAQ
所以,我们需要更高级的做法
7.2.2 RMQ 问题倍增+DP
我们先考虑 DP 状态
定义状态:
d p [ i ] [ j ] dp[\ i\ ][\ j\ ] dp[ i ][ j ] 表示区间 [ i , i + 2 j − 1 ] [\ i,i+2^j-1\ ] [ i,i+2j−1 ] 的最大值
那么显然,初始化 d p [ i ] [ 0 ] = a [ i ] dp[\ i\ ][\ 0\ ]=a[\ i\ ] dp[ i ][ 0 ]=a[ i ]
结合下面的一张图,考虑状态转移方程式
我们考虑 1 1 1 号区间(即 d p [ 1 ] [ 1 ] dp[\ 1\ ][\ 1\ ] dp[ 1 ][ 1 ]),我们要求 d p [ 1 ] [ 1 ] dp[\ 1\ ][\ 1\ ] dp[ 1 ][ 1 ] 里的值,此时,因为我们已知了 d p [ 1 ] [ 0 ] dp[\ 1\ ][\ 0\ ] dp[ 1 ][ 0 ] 和 d p [ 2 ] [ 0 ] dp[\ 2\ ][\ 0\ ] dp[ 2 ][ 0 ] 的值,而二者区间范围凑到一起刚好就是 d p [ 1 ] [ 1 ] dp[\ 1\ ][\ 1\ ] dp[ 1 ][ 1 ] 的区间范围,所以,我们比一个二者的最大值,即可得到 d p [ 1 ] [ 1 ] dp[\ 1\ ][\ 1\ ] dp[ 1 ][ 1 ] 的值
我们考虑 2 2 2 号区间(即 d p [ 5 ] [ 2 ] dp[\ 5\ ][\ 2\ ] dp[ 5 ][ 2 ]),我们要求 d p [ 5 ] [ 2 ] dp[\ 5\ ][\ 2\ ] dp[ 5 ][ 2 ] 里的值,此时,因为我们已知了 d p [ 5 ] [ 1 ] dp[\ 5\ ][\ 1\ ] dp[ 5 ][ 1 ] 和 d p [ 7 ] [ 1 ] dp[\ 7\ ][\ 1\ ] dp[ 7 ][ 1 ] 的值,而二者区间范围凑到一起刚好就是 d p [ 5 ] [ 2 ] dp[\ 5\ ][\ 2\ ] dp[ 5 ][ 2 ] 的区间范围,所以,我们比一个二者的最大值,即可得到 d p [ 5 ] [ 2 ] dp[\ 5\ ][\ 2\ ] dp[ 5 ][ 2 ] 的值
此时,我们发现,对于区间 [ i , i + 2 j − 1 ] [\ i,i+2^j-1\ ] [ i,i+2j−1 ] ,我们可以将其拆分为两部分, [ i , i + 2 j − 1 − 1 ] [\ i,i+2^{j-1}-1\ ] [ i,i+2j−1−1 ] 以及 [ i + 2 j − 1 , i + 2 j − 1 ] [\ i+2^{j-1},i+2^j-1\ ] [ i+2j−1,i+2j−1 ] ,将两个小区间比上一个最大值,就是大区间的值
换言之,我们得到了状态转移方程式:
d p [ i ] [ j ] = max ( d p [ i ] [ j − 1 ] , d p [ i + 2 j − 1 ] [ j − 1 ] ) dp[\ i\ ][\ j\ ]=\max(dp[\ i\ ][\ j-1\ ],dp[\ i+2^{j-1}\ ][\ j-1\ ]) dp[ i ][ j ]=max(dp[ i ][ j−1 ],dp[ i+2j−1 ][ j−1 ])
这样一来,我们就用 O ( n log n ) O(n\log n) O(nlogn) 的时间复杂度进行了预处理,接下来,处理每一次询问
假设现要求区间 [ l , r ] [\ l,r\ ] [ l,r ] 的最大值,我们另设 k = log 2 ( r − l + 1 ) k=\log_2(r-l+1) k=log2(r−l+1) ,然后,求出 max ( d p [ l ] [ k ] , d p [ r − 2 k + 1 ] [ k ] ) \max(dp[\ l\ ][\ k\ ],dp[\ r-2^k+1][\ k\ ]) max(dp[ l ][ k ],dp[ r−2k+1][ k ]) 即可得到答案
我们简单伪证一下
对于一个区间,我们只需要选择两个子区间,使其能够完全覆盖整个区间,再比较两个子区间的最大值即可
换言之,我们只需要得到 r − 2 k + 1 ≤ l + 2 k − 1 r-2^k+1\le l+2^k-1 r−2k+1≤l+2k−1 即可
先假设 r − 2 k + 1 > l + 2 k − 1 r-2^k+1>l+2^k-1 r−2k+1>l+2k−1
r − 2 k + 1 > l + 2 k − 1 r + 2 > l + 2 × 2 k r + 2 > l + 2 × ( r − l + 1 ) r + 2 > l + 2 × r − 2 × l + 2 r > 2 × r − l l > r \begin{aligned}r-2^k+1&>l+2^k-1\\r+2&>l+2\times2^k\\r+2&>l+2\times (r-l+1)\\r+2&>l+2\times r-2\times l+2\\r&>2\times r-l\\l&>r\end{aligned} r−2k+1r+2r+2r+2rl>l+2k−1>l+2×2k>l+2×(r−l+1)>l+2×r−2×l+2>2×r−l>r
Tips: 2 k = n ( k = log ( n ) ) 2^k=n(k=\log(n)) 2k=n(k=log(n))
显然,矛盾,假设不成立,所以 r − 2 k + 1 ≤ l + 2 k − 1 r-2^k+1\le l+2^k-1 r−2k+1≤l+2k−1
那么,现在,我们就可以得到完整代码了
#include<cmath> //为了使用 log2 函数
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,x,y;
int dp[100005][105];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&dp[i][0]); //初始化
}
for(int j=1;(1<<j)<=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]);
}
}
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y); //询问
int k=log2(y-x+1); //求解
printf("%d\n",max(dp[x][k],dp[y-(1<<k)+1][k]));
}
return 0;
}
我的博客,完成啦哈哈哈