蓝桥杯精选赛题系列——区间最大值——倍增法

今天我们来讲第三个算法,已收录此专栏,话不多说,进入正题。
倍增法和二分法是“相反”的算法。二分是每次缩小一倍,从而以 O(logn) 的步骤极快地缩小定位到解;倍增是每次扩大一倍,从而以 O(2n) 的速度极快地扩展到极大7的空间。所以倍增和二分的效率都很高。
二分法与倍增的应用场景
二分法是缩小区间,最后定位到一个极小的区间,小到这个区间的左右端点重合(或者几乎重合),从而得到解,解就是最后这个极小区间的值。所以二分法的适用场合,是在一个有序的序列,或者一个有序的曲线上,通过二分缩小查询区间,其目的是找到一个特定的数值。
倍增相反,是从小区间扩大到大区间。在区间问题中,它是在大区间上求解和区间查询有关的问题,例如区间最大值或最小值。这种应用例如本讲将会提到的RMQ问题,是用基于倍增的ST算法解决。
除了在区间上的应用,倍增也能用于数值的精确计算。如果空间内的元素满足倍增关系,或者能利用倍增关系来计算,那么也能用倍增法达到求解这些元素的精确值的目的。
接下来我们来讲一个倍增的经典应用 — ST 算法吧。

ST(Sparse Table)算法

ST 算法是求解 RMQ 问题的优秀算法,它适用于静态空间的 RMQ 查询。

静态空间的 RMQ 问题(Range Minimum/Maximum Query,区间最值问题):给定长度为 n 的静态数列,做 m次询问,每次给定 L,R≤n,查询区间[L, R]内的最值。下面以区间最小值问题为例。

用暴力搜区间 [L, R]的最小值,即逐一比较区间内的每个数,复杂度是 O(n)的;m次查询,复杂度 O(mn)。暴力法的效率很低。那ST算法呢?

ST算法源于这样一个原理:一个大区间若能被两个小区间覆盖,那么大区间的最值等于两个小区间的最值。例如下图中,大区间{4, 7, 9, 6, 3, 6, 4, 8, 7, 5}被两个小区间{4, 7, 9, 6, 3, 6, 4, 8}、{4, 8, 7, 5}覆盖,大区间的最小值 3,等于两个小区间的最小值,min(3,4)=3。这个例子中两个小区间有部分重合,重合不影响结果。
图1大区间被两个小区间覆盖
图1大区间被两个小区间覆盖

从以上原理得到 ST 算法的基本思路,包括两个步骤:

  1. 把整个数列分为很多小区间,并提前计算出每个小区间的最值;
  2. 对任意一个区间最值查询,找到覆盖它的两个小区间,用两个小区间的最值算出答案。

那么如何设计出这 2 个步骤的高效算法呢?
1. 把数列按倍增分成小区间
对数列的每个元素,把从它开始的数列分成长度为1、2、4、8、…的小区间。下图给出了一个分区的例子,它按小区间的长度分成了很多组。

第 1 组是长度为1 的小区间,有 n 个小区间,每个小区间有 1 个元素;
第 2 组是长度为2 的小区间,有 n 个小区间,每个小区间有 2 个元素;
第 3 组是长度为4 的小区间,有 n 个小区间,每个小区间有 4 个元素;
…… 共有 logn组。
请添加图片描述
图2 按倍增分为小区间
我们可以发现,每组的小区间的最值,可以从前一组递推而来。例如第 3 组 {4, 7, 9, 6} 的最值,可以从第 2组 {4,7}、{9,6} 的最值递推得到。
所以我们定义 dp[s][k],表示左端点是 s,区间长度为 2k的区间最值。注意:1<<(k-1) 等于 2^{k-1} 例如:2<<3 = 2 * 23
递推关系是:(记住)
dp[s][k]=mindp[s][k−1],dp[s+1<<(k−1)][k−1]
2. 查询任意区间的最值
查询一个任意区间 [L, R] 的最小值,就是找到以 L 为起点的区间(区间终点小于 R),和以 R 为终点的区间(起点大于 L),这些区间的交集,就是[L,R]。而上面的分区方法,有以下结论:以任意元素为起点,有长度为 1、2、4、…的小区;以任意元素为终点,它前面也有长度为 1、2、4、…的小区。再根据这个结论,可以把需要查询的区间 [L,R] 分为 2 个小区间,且这两个小区间属于同一个组:以 L 为起点的小区间、以 R 为终点的小区间,让这两个小区间首尾相接覆盖 [L, R],区间最值从两个小区间的最值求得。一次查询的计算复杂度是 O(1)。
那我要怎么定这两个小区间的长度是多少呢?
首先区间[L,R]的长度是 len = R - L+1。设两个小区间的长度都是x,其中x是比len小的2的最大倍数,有x≤len且2×x≥len,这样才保证能覆盖。另外需要计算 dp[][],根据 dp[s][k] 的定义,有 2k =x。例如 len=19,x=16,2k= 16,k=4。
那已知 len 如何求k 呢?计算公式是 k=log2(len)=log(len)/log(2),向下取整。以下两种代码都行:
以 10 为底的库函数 log():int k=(int)(log(double(R-L+1)) / log(2.0));
以 2 为底的库函数 log2():int k=log2(R-L+1);
最后给出区间 [L, R]最小值的计算公式,等于覆盖它的两个小区间的最小值:min(dp[L][k],dp[R−(1<<k)+1][k]);

好下面我们来做个题看下倍增法的具体应用,最好记住倍增法的代码模板。

题目描述

给定一个长度为 N 的数组 a,其值分别为 a1,a2,…,aN 。

现有 Q 个询问,每个询问包含一个区间,请回答该区间的最大值为多少。

输入描述

输入第 1 行包含两个正整数 N,Q,分别表示数组 a 的长度和询问的个数。

第 2 行包含 N 个非负整数 a1,a2,…,aN ,表示数组 a 元素的值。

第 3∼Q+2 行每行表示一个询问,每个询问包含两个整数L,R,表示区间的左右端点

1≤N,Q≤5×105,1≤l≤r≤N,-109≤k,ai ≤109

输出描述

输出共 Q 行,每行包含一个整数,表示相应询问的答案。

样例输入

5 5
1 2 3 4 5
1 1 
1 2 
1 3
3 4
2 5

样例输出

1
2
3
4
5

详细答案

from math import *

maxn = 100001   
dp_max = [[0 for col in range(40)] for row in range(maxn)] #定义一个二维数组,初始化

def st_init():
    global dp_max
    for i in range(1,n+1): dp_max[i][0] = a[i]
    p = int(log2(n))
    for k in range(1, p+1):
        for s in range(1, n+2-(1<<k)):
            dp_max[s][k]=max(dp_max[s][k-1], dp_max[s+(1<<(k-1))][k-1])

def st_query(L,R):
    k = int(log2(R-L+1))
    return max(dp_max[L][k],dp_max[R-(1<<k)+1][k])

n,m =  map(int, input().split())
b = list(map(int,input().split()))
a = [0]*maxn
for i in range (1,n+1): a[i]=b[i-1]   #从a[1]开始。不用a[0]
st_init()
for i in range(1,m+1):
    L,R = map(int, input().split())
    print(st_query(L,R))

猜你喜欢

转载自blog.csdn.net/m0_51951121/article/details/122676209