所谓倍增,就是成倍增长。以2的次幂的方式增长。
我们在进行递推时,如果状态空间很大,线性递推无法满足时间与空间复杂度的要求,我们可以通过“成倍增长”的方式,只递推在2的整数次幂位置上的值作为代表。
当需要其他位置上的值时,也可以用这些2的幂次上的值所拼成,因为 “任意整数都可以表示成若干个2的次幂项的和”。
例题1:区间和
题目描述:
给定长度为 n n n 的数列,进行若干次询问。
给出整数 T T T,给出左端点 p p p,求出最大的 k k k,使得从 l l l 开始的 k k k 个位置元素之和不超过 T T T。
思路:
预处理出前缀和 s [ i ] s[i] s[i]。
考虑暴力算法,依次往后枚举 k k k 的位置,时间复杂度 O ( N ) O(N) O(N)。
由于前缀和满足单调性,所以可以二分 k k k 的位置。
但是,对于每次询问,二分的时间复杂度都为 O ( N l o g N ) O(NlogN) O(NlogN)。如果当答案 k k k 很小的话,还不如直接枚举效率高。
那么是否找到一种方法,能够兼顾两者的优点呢?
倍增!
我们可以用2的幂次来判断 k k k 的位置。设立左端点 l = p l = p l=p,右端点 r = 1 r =1 r=1,倍增长度 l e n = 1 len =1 len=1。
- 如果
s[r+len] - s[l-1]≤ T
,说明当前长度可行,继续倍增,r+=len
,len*=2
; - 否则,说明倍增长度太长,就要缩减,
len/=2
。
重复上述操作,直到 l e n = 0 len=0 len=0了,那么当前 r r r 便是答案。
这样,如果答案 k k k 很小,这个算法的复杂度便也变小。
这个算法始终在答案大小的范围内实施“倍增”与“二进制划分”思想,通过若干长度为2的次幂的区间拼成最后的 k k k,时间复杂度级别为答案 k k k 的对数,能够应对 T T T 的各种大小的情况。
例题2、Genius ACM
题意:
给定一个整数 M M M,对于任意一个整数集合 S S S,定义“校验值”如下:
从集合 S S S 中取出 M M M 对数(即 2 ∗ M 2*M 2∗M个数,不能重复使用集合中的数,如果 S S S 中的整数不够 M M M 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值就称为集合 S S S 的“校验值”。
现在给定一个长度为 N N N 的数列 A A A 以及一个整数 T T T。我们要把 A A A 分成若干段,使得
每一段的“校验值”都不超过 T T T。求最少需要分成几段?
思路:
对于一个集合 S S S,为了使“没对数的差的平方”之和最大,只能最大值和最小值配对,次大值和次小值配对…
为了总的段数最小,需要让每一段的“检验值”不超过T的前提下尽量长。所以从从头开始对 A A A 分段,让每一段都尽量长,这样得到的就是最小分段数。
于是,需要解决的问题是,对于一个起点 l l l,最多往后延伸多少个位置,能够使得这一段区间的“检验值”不超过 T T T?
因为往后延伸的区间越长,其“检验值”越大,满足单调性,所以很容易想到二分右端点。
但是对于每一次二分,复杂度为O(logN),对于每一次check,需要排序O(NlogN),而最坏情况下,需要对每个位置二分右端点,所以整个复杂度为 O ( N 2 l o g 2 N ) O(N^2 log^2N) O(N2log2N)。(其实真实是O(N^2 logN),证明)复杂度很高。
而用倍增,复杂度可以降到 O ( N l o g 2 N ) O(N log^2N) O(Nlog2N)。
对于一个起点位置 l l l,定义右端点 r = l r=l r=l,倍增长度 l e n = 1 len=1 len=1。
- 如果区间 [ l , r + l e n ] [l, r+len] [l,r+len] 的“校验值”满足,那么说明当前倍增的长度是可以的,更新右端点
r+=len
,len*=2
; - 否则,说明倍增长度太长,
len/=2
。
重复上述操作,直到倍增长度 l e n = 0 len =0 len=0 ,此时的 r r r 便是最右端的位置。
考虑这种算法的复杂度:
上面的过程最多循环 O ( l o g N ) O(logN) O(logN) 次,每次循环求“检验值” O ( N l o g N ) O(N logN) O(NlogN),所以时间复杂度为 O ( N l o g 2 N ) O(N log^2N) O(Nlog2N)。
Code:
const int N = 500010, mod = 1e9+7;
ll T, n, m, a[N],b[N];
ll maxa;
bool pd(int l,int r){
if(r>n) return 0; //最右端不超过数组长度
int cnt=0;
for(int i=l;i<=r;i++) b[++cnt]=a[i];
sort(b+1,b+cnt+1);
ll sum=0;
for(int i=0;i<m;i++)
{
if(i+1>=cnt-i) break;
sum+=(b[cnt-i]-b[i+1])*(b[cnt-i]-b[i+1]);
}
if(sum<=maxa) return 1;
return 0;
}
signed main(){
Ios;
cin>>T;
while(T--)
{
int cnt=0;
cin>>n>>m>>maxa;
for(int i=1;i<=n;i++) cin>>a[i];
int st=1;
while(st<=n)
{
ll l=st,r=st,len=1; //设置左端点,右端点,倍增长度
while(len!=0) //当倍增长度为0的时候结束
{
if(pd(l,r+len)) r+=len,len*=2; //满足,倍增
else len/=2; //不满足,倍减
}
st=r+1;
cnt++;
}
cout<<cnt<<endl;
}
return 0;
}
对于每次求“检验值”,可以不用 s o r t sort sort 排序,而是采用归并排序,只对新增的长度排序,然后合并新旧两段,总体复杂度可以降到 O ( N l o g N ) O(N logN) O(NlogN)。
RMQ(区间最值问题)ST算法
RMQ问题:
给定一个长度为 n n n 的数列,每次给出一个区间,问这个区间中元素的最大值?
对于暴力,时间复杂度为 O ( N ∗ M ) O(N*M) O(N∗M), M M M 为询问次数。
而 S T ST ST 算法能在 O ( N l o g N ) O(N logN) O(NlogN) 时间的预处理之后,以 O ( 1 ) O(1) O(1) 的时间复杂度在线回答 R Q M RQM RQM 问题。
定义 f [ i , j ] f[i,j] f[i,j] 表示数列中下标在区间 [ i , i + 2 j − 1 ] [i, i+2^j-1] [i,i+2j−1] 里的数的最大值,也就是从位置 i i i 开始的 2 j 2^j 2j 个数的最大值。
递推求出 f [ i , j ] f[i,j] f[i,j]: O ( N l o g N ) O(N logN) O(NlogN)
递推边界: f [ i ] [ 0 ] = a [ i ] f[i][0] = a[i] f[i][0]=a[i],即数列a在子区间 [ i , i ] [i,i] [i,i] 里的最大值。
递推时,我们把子区间的长度成倍增长,长度为 2 j 2^j 2j 的子区间的最大值为左右两半长度为 2 j − 1 2^{j-1} 2j−1 的子区间的最大值中较大的一个,即:f[i,j] = max(f[i, j-1], f[i + (1<<(j-1)),j-1]
。
考虑 j j j 的最大值,为使得 2 j 2^j 2j 不超过 n 的最大的 j,那么 j = l o g 2 n j = log_2^n j=log2n。
我们可以调用 < c m a t h > <cmath> <cmath>中的 log()
函数, l o g 2 n = l o g ( n ) / l o g ( 2 ) log_2^n = log(n)/log(2) log2n=log(n)/log(2)。 ( l o g 2 n = l o g 10 n / l o g 10 2 ) (log_2^n = log_{10}^n / log_{10}^2) (log2n=log10n/log102)。
考虑 i i i 的最大值,从 i i i 往右延伸的区间长度最大为 2 j 2^j 2j ,所以 i i i 最大只需要到 n − 2 j + 1 n-2^j+1 n−2j+1。
递推时,当前状态需要用到 前面 j − 1 j-1 j−1 状态的 i + 2 j − 1 i+2^{j-1} i+2j−1 ,所以需要先循环 j j j,再循环 i i i。
void RMQ()
{
for(int i=1;i<=n;i++) f[i][0]=a[i];
int t=log(n)/log(2); //t为 不超过n的,2^t的最大值 = log_2^n。
for(int j=1;j<=t;j++) //先遍历j,再遍历i。
{
for(int i=1;i<=n-(1<<j)+1;i++) //i位置最大为 n-2^j+1。
{
f[i][j]=max(f[i][j-1],f[i + (1<<(j-1))][j-1]);//max(最区间最大值,右区间最大值)
}
}
}
对于询问一个区间 [ l , r ] [l,r] [l,r] 的最大值: O ( 1 ) O(1) O(1)
我们需要先算出不超过这个区间长度的 2 t 2^t 2t 的 t t t 的最大值: l o g 2 r − l + 1 log_2^{r-l+1} log2r−l+1。
那么这个区间的最大值就为 “从 l l l 开始的 2 t 2^t 2t 个数” 和 “以 r r r 结尾的 2 t 2^t 2t 个数” 这两段的最大值较大的一个。即 max(f[l][t], f[r-(1<<t)+1][t])
。
int query(int l,int r){
int t=log(r-l+1)/log(2); //这里是区间长度的对数,不是整个数组的对数
return max(f[l][t],f[r-(1<<t)+1][t]); //从后往前找的时候+1,从前往后不用加。
}
完整代码:
#include<iostream>
#include<cmath>
using namespace std;
const int N=100010;
int n,m,a[N];
int f[N][20];
void RMQ()
{
for(int i=1;i<=n;i++) f[i][0]=a[i];
int t=log(n)/log(2);
for(int j=1;j<=t;j++)
for(int i=1;i<=n-(1<<j)+1;i++)
f[i][j]=max(f[i][j-1],f[i + (1<<(j-1))][j-1]);
}
int query(int l,int r){
int t=log(r-l+1)/log(2);
return max(f[l][t],f[r-(1<<t)+1][t]);
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
RMQ();
while(m--){
int x,y;cin>>x>>y;
cout<<query(x,y)<<endl;
}
return 0;
}
同理,把 m a x max max 换成 m i n min min ,我们可以求出一个区间的最小值。
参考来源: 《 算 法 竞 赛 进 阶 指 南 》 — — 李 煜 东 《算法竞赛进阶指南》 ——李煜东 《算法竞赛进阶指南》 ——李煜东
哪里有问题或者不明白的话欢迎留言评论~