OI学习笔记1:倍增算法

倍增

一、倍增

倍增,顾名思义,成倍增长。一般我们在进行递推时,如果状态空间很大,通常的线性递推无法满足时间与空间复杂度的要求,那么我们可以通过成倍增长的方式,只递推状态空间中在2的整数次幂位置上的值作为代表。

当需要其他位置上的值时,我们通过“任意整数可以表示成若干个2的次幂项的和”这一性质,使用之前求出的代表值拼成所需的值。所以使用倍增算法也要求我们递推的问题的状态空间关于2的次幂具有可划分性。本文中,我们研究序列上的倍增算法。

  • 试想这样一个问题

给定一个长度为 \(N\) 的数列 \(A\),然后进行若干次访问,每次给定一个整数 \(T\),求出最大的 \(k\) 满足 \(\sum_{i=1}^{k}A[i]\leq T\)
你的算法必须是在线的(必须即时回答每一个询问),假设\(0\leq T\leq \sum_{i=1}^{N}A[i]\)

最朴素的做法肯定是暴力枚举 \(k\) ,其次是处理前缀和数组后二分答案,但这些方法的时间复杂度都不是很优。

我们可以设计这样的倍增算法:

  1. \(p=1,k=0,sum=0\)
  2. 比较“ \(A\) 数组 \(k\) 之后的 \(p\) 个数的和”与 \(T\) 的关系。
if sum + S[k + p] - S[k] <= T
    then sum += S[k + p] - S[k], k += p, p *= 2
else then p /= 2
  1. 重复上一步,知道 \(p\) 的值变为 \(0\),此时 \(k\) 就是答案。
  • 【例题】Genius ACM

给定一个整数 \(M\),对于任意一个整数集合 \(S\),定义校验值如下:
从集合 \(S\) 中取出 \(M\) 对数(即 \(2 * M\) 个数,不能重复使用集合中的数,如果 \(S\) 中的整数不够 \(M\) 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个数就称为集合 \(S\) 的校验值。
现在给定一个长度为 \(N\) 的数列 \(A\) 以及一个整数 \(T\)。我们要把 \(A\) 分成若干段,使得每一段的“校验值”都不超过 \(T\)。求最少需要分成几段。

解题思路:我们从头开始对 \(A\) 进行分段,让每一段尽量长,到达结尾时分成的段数就是答案。

因此需要解决的问题为:当确定一个左端点 \(L\) 后,右端点 \(R\) 在满足 \(A[L]\) ~ \(A[R]\) 的“校验值”不超过 \(T\) 的前提下,最大能取到多少。

问题解决:
1.怎么求长度为 \(N\) 的一段的校验值?排序后取左右端点,再求校验值即可。
2.区间的校验值求法,我们采用与上面例子类似的倍增方法。

算法思路:

  1. 初始化 \(p=1,R=L\)
  2. 求出 \([L,R+p]\) 这一段区间的校验值,若校验值 \(\leq T\),则 \(R+=p,p *= 2\),否则 \(p/=2\)
  3. 重复上一步,直到 \(p\) 的数值变为 \(0\),此时 \(R\) 即为所求。算法时间复杂度为 \(O(N\log^2 N)\)
//AcWing109, AC代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 500005;
long long a[MAXN], b[MAXN], t;
int n, m, k;
long long sq(long long x){
	return x * x;
}
long long check(int L, int R){
	int cnt = 0;
	for(int i = L; i < R; i++) b[cnt++] = a[i];
	sort(b, b + cnt);
	long long sum = 0;
	for(int i = 0; i < m && i < cnt; i ++, cnt --) sum += sq(b[i] - b[cnt - 1]);
	return sum;
}
int main(){
	scanf("%d", &k);
	while(k--){
		scanf("%d%d%lld", &n, &m, &t);
		for(int i = 0; i < n; i++) scanf("%lld", &a[i]);
		int ans = 0;
		int L = 0, R = 0;
		while(R < n){
			int p = 1;
			while(p){
				if(R + p <= n && check(L, R + p) <= t)
					R += p, p *= 2;
				else p /= 2;
			}
			L = R;	
			ans++;
		}
		printf("%d\n", ans);
	}
	return 0;
}

二、ST算法

\(RMQ\) 问题(区间最值问题)中,著名的 \(ST\) 算法就是倍增的产物。\(RMQ\) 问题描述如下:

给定一个长度为 \(N\) 的数列 \(A\),并有 \(M\) 次询问,每次询问给定下标 \(l,r\),在线回答“数列 \(A\) 中下标在 \(l\) ~ \(r\) 之间的数的最大值”。

对于这种问题,\(ST\) 算法能做到在 \(O(N \log^2 N)\) 时间的预处理后,以 \(O(1)\) 的时间复杂度回答每个询问。

问题解决 && 算法思路:

  • 初始化

一个序列的子区间个数显然有 \(n^2\) 个,根据倍增思想,我们在这个状态空间中选择一些2的整数次幂的位置作为代表值。

\(F[i,j]\) 表示数列 \(A\) 中下标在子区间 \([i,i + 2^j - 1]\) 里的数的最大值,也就是从 \(i\) 开始的 \(2^j\) 个数中的最大值。递推边界显然为 \(F[i,0]=A[i]\),即数列 \(A\) 在子区间 \([i,i]\) 中的最大值。

递推时,将子区间成倍增长,有公式

\[F[i,j]=\max(F[i,j-1], F[i + 2^{j-1}, j-1]) \]

即长度为 \(2^j\) 的区间的最大值时左右两半长度为 \(2^{j-1}\) 的子区间的最大值中的最大值。

void ST_prework(){
	for(int i = 1; i <= n; i++) f[i][0] = a[i];
	int t = log(n) / log(2) + 1;
	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]);
}
  • 询问

当询问任意区间 \([l,r]\) 的最值时,先计算出一个 \(k\),使得 \(2^k\leq r-l+1\leq 2^{k+1}\),也就是2的 \(k\) 次幂小于区间长度的 \(k\) 的最大值。那么从 \(l\) 开始的 \(2^k\) 个数与以 \(r\) 结尾的 \(2^k\) 个数一定覆盖了区间 \([l,r]\),这两段的最大值分别为 \(F[l][k], F[r - 2^k+1][k]\),二者的最大值就是整个区间的最大值。

int ST_query(int l, int r){
	int k = log(r - l + 1) / log(2);
	return max(f[l][k], f[r - (1<<k) + 1][k]);
}

以上程序使用 cmath 库中的 \(\log\) 函数,如果想要进一步追求效率,可以先用 \(O(N)\) 的时间预处理 \(1\) ~ \(N\) 每个数字所对应的 \(\log\) 值。

void init(){
	for(int i = 1; i <= n; i++) Log[i] = log(i);
}

关于求解 \(LCA\) 的应用将在后面补充。

posted @ 2021-08-26 19:27  聂玄HankNie  阅读(592)  评论(0编辑  收藏  举报