倍增,RMQ(区间最值问题)ST算法

所谓倍增,就是成倍增长。以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 2M个数,不能重复使用集合中的数,如果 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 分段,让每一段都尽量长,这样得到的就是最小分段数。

扫描二维码关注公众号,回复: 13523690 查看本文章

于是,需要解决的问题是,对于一个起点 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+=lenlen*=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(NM) 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+2j1] 里的数的最大值,也就是从位置 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} 2j1 的子区间的最大值中较大的一个,即: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 n2j+1

递推时,当前状态需要用到 前面 j − 1 j-1 j1 状态的 i + 2 j − 1 i+2^{j-1} i+2j1 ,所以需要先循环 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} log2rl+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 ,我们可以求出一个区间的最小值。

例题:
1、数列区间最大值
2、最敏捷的机器人


参考来源: 《 算 法 竞 赛 进 阶 指 南 》   — — 李 煜 东 《算法竞赛进阶指南》 ——李煜东  

哪里有问题或者不明白的话欢迎留言评论~

猜你喜欢

转载自blog.csdn.net/Mr_dimple/article/details/120774947