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


引入倍增:

所谓倍增,就是成倍增长。以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 分段,让每一段都尽量长,这样得到的就是最小分段数。

于是,需要解决的问题是,对于一个起点 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)


应用:

ST算法 求解 RMQ(区间最值问题)

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,从前往后不用加。 
}
模板Code:
#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、最敏捷的机器人


ST算法 维护区间最大公约数

和区间最值有相似性质的还有 最大公约数 g c d gcd gcd
类似于更新区间最值的方法,对于一整个区间的 gcd 等于其两个子区间的 gcd 的 gcd。要求两个子区间覆盖住整个区间,允许有重合。
所以,和维护最值一样:

  • 在更新的时候用两个半长区间更新;
  • 查询的时候用两个2的次幂数长度的区间取 gcd。
void st(){
	int t = log(n)/log(2);
	
	for(int j=1;j<=t;j++)
		for(int i=1;i<=n-(1<<j)+1;i++)
		{
			gcd[i][j] = __gcd(gcd[i][j-1], gcd[i+(1<<(j-1))][j-1]);
		}
}

int query(int l, int r)
{
	int t = log(r-l+1)/log(2);
	
	return __gcd(gcd[l][t], gcd[r-(1<<t)+1][t]);
}
例题:Pair of Numbers

题意
给定长度为 n 的数列,求出长度最长的满足下列条件的区间:

  • 区间中存在一个数 x,能够被其他所有数除尽。

1   ≤   n   ≤   3 ∗ 1 0 5 1 ≤ n ≤ 3*10^5 1n3105

分析
如果长度为 5 的区间满足上面条件的话,那么其长度为 3 的子区间也一定满足。所以区间长度满足单调性,可以二分最长长度。

对于每种长度,遍历起点位置。
如果说,区间所有数中的最小值等于这些数的最大公约数的话,那么这个最小值就能够被其他所有数除尽,就满足条件。

用ST表维护区间 gcd 和区间最小值,O(1) 查询。

Code:

#include<bits/stdc++.h>
using namespace std;

#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
map<int,int> mp;

/**/

const int N = 300010, mod = 1e9+7;
int T, n, m;
int a[N];
int mina[N][30], gcd[N][30];
vector<int> ans[N];
 
void st(){
	int t = log(n)/log(2);
	
	for(int j=1;j<=t;j++)
	{
		for(int i=1;i<=n-(1<<j)+1;i++)
		{
			mina[i][j] = min(mina[i][j-1], mina[i+(1<<(j-1))][j-1]);
			gcd[i][j] = __gcd(gcd[i][j-1], gcd[i+(1<<(j-1))][j-1]);
		}
	}
}

bool query(int l, int r)
{
	int t = log(r-l+1)/log(2);
	
	int minn = min(mina[l][t], mina[r-(1<<t)+1][t]);
	int gcdd = __gcd(gcd[l][t], gcd[r-(1<<t)+1][t]);
	
	return minn == gcdd;
}

bool check(int mid)
{
	int flag = 0;
	
	for(int i=1;i<=n-mid+1;i++)
	{
		if(query(i, i+mid-1)){
			flag = 1;
			ans[mid].pb(i);
		}
	}
	if(flag) return 1;
	return 0;
}

signed main(){
	cin >> n;
	for(int i=1;i<=n;i++) cin>>a[i], mina[i][0] = gcd[i][0] = a[i];
	
	st();
	
	int l = 0, r = n;
	while(l<r)
	{
		int mid = l+r+1>>1;
		if(check(mid)) l = mid;
		else r = mid-1;
	}
	
	cout << ans[l].size() << " " <<l-1<<endl;
	for(auto x:ans[l]) cout<<x<<" ";
	
	return 0;
}

那么既然这样的话,或操作(|) 和 与操作(&) 也可以区间维护了。


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

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

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值