Skip to content

倍增与 ST 表

倍增

倍增即成倍增长。是指我们在进行递推时,如果状态空间很大,线性递推无法满足时空要求,此时可以考虑成倍增长的方式,只递推状态空间在 2 的整数次幂位置上的值作为代表,其他值可以通过“任意整数可以表示成若干个 2 的次幂项的和”这一性质得到。

算法竞赛进阶指南原书例题:给定一个长度为 N 的数列 A,然后进行若干次询问,每次给定一个整数 T,求出最大的 k,满足 i=1kA[i]T

倍增算法

  1. p=1,k=0,sum=0
  2. 比较“A 数组中 k 之后的 p 个数的和”与 T 的大小,也就是说,如果 sum+S[k+p]S[k]T,则令 sum+=S[k+p]S[k],k+=p,p=2,即累加上这 p 个数的和,然后把 p 的跨度增长一倍。否则 p/=2
  3. p0 时,结束循环,此时 k 即为答案。
cpp
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int n, a[N], t, s[N];

int main() {
	cin >> n >> t;
	for(int i = 1; i <= n; i++) {
        cin >> a[i];
		s[i] = s[i - 1] + a[i];
	}

	int p = 1, k = 0, sum = 0;
	while(p) {
		if (k + p <= n && sum + s[k + p] - s[k] <= t) {
			sum += s[k + p] - s[k];
			k += p;
			p *= 2;
		} else {
			p /= 2;
		}
	}

	cout << k << endl;

	return 0;
}

问题思考AcWing 109. 天才 ACM

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

typedef long long ll;
const int N = 1e6 + 10;
ll t, n, m, K, a[N], tot, b[N];

bool calc(ll l,ll k,ll p) {
	ll res = 0;
	for(ll i = l; i <= k + p; i++) b[i - l] = a[i];

    ll len = p + k - l + 1;
	sort(b, b + len);

    if (len >= 2 * m) {
		for(ll i = 0; i < m; i++) {
			res += (b[i] - b[len - i - 1]) * (b[i] - b[len - i - 1]);
		}
	} else {
		ll temp = len / 2;
		for(ll i = 0; i < temp; i++) {
			res += (b[i] - b[len - i - 1]) * (b[i] - b[len - i - 1]);
		}
	}

	if (res <= K) return true;
	return false;
}

int main() {
	cin >> t;
	while (t--) {
		tot = 0;
		cin >> n >> m >> K;

		for (ll i = 0; i < n; i++) cin >> a[i];

		ll l = 0;
		while (l < n) {
			ll k = l, p = 1;
			while (p) {
				if (calc(l, k, p) && k + p < n) {
					k += p;
					p *= 2;
				} else {
					p /= 2;
				}
			}
			tot++;
			l = k + 1;
		}
		cout << tot << endl;
	}

	return 0;
}

ST 表

ST 表(Sparse Table,稀疏表)是用于解决可重复贡献问题的数据结构。

例如:给定 n 个数, m 个询问,对于每个询问,求解区间 [l, r] 的最大值。

朴素想法:考虑使用区间 DP,

  1. 枚举所有的左端点和右端点,记录所有区间的最大值,用 f[i][j] 来表示区间 [i, j] 的最大值。
  2. 状态转移,考虑当前状态 f[i][j] 由何状态转变而来?当前区间 [i][j] 最后加入进来的值可能时 ai 也可能是 aj
    1. aif[i][j] = max(f[i + 1][j], a[i])
    2. ajf[i][j] = max(f[i][j - 1], a[j])
  3. 状态转移方程:f[i][j] = max(max(f[i][j - 1], a[j]), max(f[i + 1][j], a[i]))
  4. 边界条件:i == jf[i][j] = a[i]
  5. cpp
    for (int len = 1; len <= n; len++) { // 区间长度
    	for (int i = 1; i + len - 1 <= n; i++) { // 左端点
    		int j = i + len - 1; // 右端点
    		if (i == j) f[i][j] = a[i];
    		else f[i][j] = max(max(f[i][j - 1], a[j]), max(a[i], f[i + 1][j]));
    	}
    }
  6. 问题与思考:时间复杂度?空间复杂度?有没有更好的状态表示?

算法思想

ST 表基于倍增思想,预处理时间复杂度为 O(nlogn),查询 O(1)。ST 表不支持修改操作。

具体实现如下:

预处理

  1. f(i,j) 表示区间 [i,i+2j1] 的最大值,f(i,0)=ai
  2. 状态转移方程:f(i,j)=max(f(i,j1),f(i+2j1,j1))

ST表

查询

对于每一个询问,可以将其分成两部分:[l,l+2k1][r2k+1,r],这里的 k=log2rl+1

算法实现

cpp
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;

const int N = 1e5 + 10, M = 18;
int a[N], f[N][20], n, m, lg2[N];

void init() {
	for (int j = 0; j < M; j++) {
		for (int i = 1; i + (1 << j) - 1 <= n; i++) {
			if (!j) f[i][j] = a[i];
			else f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
		}
	}
}

int query(int l, int r) {
	int k = lg2[r - l + 1];
	// 使用cmath 中的 log函数 以10为底
	// int k = log(r - l + 1) / log(2);

	return max(f[l][k], f[r - (1 << k) + 1][k]);
}

int main() {
	scanf("%d%d", &n, &m);

	for (int i = 2; i <= n; i++) lg2[i]= lg2[i >> 1] + 1;

	for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
	init();

	while (m--) {
		int x, y; scanf("%d%d", &x, &y);
		printf("%d\n", query(x, y));
	}

	return 0;
}

注意:可以预处理 log

{log21=0(n=1)log2n=log2n2+1(n2)
  • log2n=log2(2n2)=log22+log2n2=1+log2n2

经典例题

  1. YBT 1544.天才的记忆
  2. 洛谷 P3865. 【模板】ST 表
  3. 洛谷 P2880. 平衡队列
  4. 洛谷 P7167. 喷泉
  5. 洛谷 P2415. 集合求和
  6. 洛谷 P1257. 平面上的最接近点对
  7. 洛谷 P1228. 地毯填补问题
  8. 洛谷 P2345. 奶牛集会
  9. 洛谷 P3509. 青蛙
  10. 洛谷 P3517. 分段
  11. 洛谷 P4155. 覆盖计划
  12. 洛谷 P1816. 忠诚
  13. 洛谷 P6648. 三角形
  14. 洛谷 P7562. 活动