倍增和 ST 表
$$\texttt{倍增}$$
倍增,顾名思义就是成倍增长。可以在 \(O(n\log n)\) 的预处理后将 \(O(n)\) 的询问降低到 \(O(\log n)\),大大降低时间复杂度。
倍增主要运用于 RMQ 问题或 LCA 问题。
思想
举个例子,假设现在给定了一个长度为 \(n(n\leqslant 10^5)\) 的整数数组 \(a\),你需要回答 \(q(q\leqslant 10^5)\) 组询问,每次询问给定 \(l,r\),求 \(\max\limits_{l\leqslant i \leqslant r} \{a_i\}\)。
暴力找?\(O(n\times q)\) 爆炸了,这时就需要使用倍增来处理了。为了将询问复杂度降低到对数级别,则需先预处理。对于每个 \(i\),预处理出 \([i,i],[i-2^0,i],[i-2^1,i],[i-2^2,i]\cdots [i-2^{\log n},i]\) 的最大值(如果左端点小于 \(0\) 则当 \(0\) 处理)。
具体实现
令 mx[i][j]
表示 \(\max\limits_{\max(1,i-2^j+1)\leqslant k \leqslant i} \{a_i\}\),则有 mx[i][0] = a[i]
,当 j
为正数时 mx[i][j] = max(mx[i][j - 1], mx[max(0, i - (1 << (j - 1)))][j - 1])
。
询问处理
对于一段区间 \([l,r]\),可以将长度 \(r-l+1\) 做一次二进制拆分,按位从高到低枚举,如果当前位(第 \(i\) 位)为 \(1\),则 mx[r][i]
会产生共享,统计进去,再右移 \(r\) 至 \(r-2^i\)。
时间复杂度:\(O(\log n)\)。
除了维护区间 RMQ,还可以 \(O(\log n)\) 维护树上两点间的 LCA。
首先预处理出每个节点的 \(2^0,2^1,2^2\cdots 2^{\log n}\) 级祖先,令 fa[i][j]
表示 \(i\) 的 \(2^j\) 级祖先,则可以先搜索找出每个节点的 \(2^0\) 级祖先,然后就有当 j
为正数时,fa[i][j] = fa[fa[i][j - 1]][j - 1]
。
当要求 \(x\) 和 \(y\) 的 LCA 时,首先让两个点跳到同一深度,令 h[i]
表示节点 \(i\) 的深度,且 h[x] <= h[y]
,那么就先让 \(y\) 跳到 \(y\) 的 h[y] - h[x]
级祖先处,如果此时 \(x=y\),那么直接返回 \(x\) 即可。
否则,按位(\(i\))从大到小枚举,如果 fa[x][i] != fa[y][i]
,那么 x = fa[x][i], y = fa[y][i]
,最后返回 fa[x][0]
即可。
$$\texttt{ST 表}$$
ST 表是用于解决可重复贡献问题的数据结构(例如解决区间 RMQ 或区间 gcd 等问题),基于倍增思想。
还是用上面的例子,求一段区间的最大值。倍增可以 \(O(\log n)\) 解决,而 ST 表则更是大优化,可以 \(O(1)\) 处理询问!
思想
首先和倍增一样预处理。
由于 RMQ 问题是可重复共献问题(即只要处理的区间在询问区间范围内且包含了范围内的所有可贡献元素,即使重复计算也不影响答案),所以可以考虑将其拆分成两个区间处理,只要这两个区间的并集为 \([l,l+1\cdots r]\)。
处理
首先预处理出 \(1\sim n\) 的 \(\log\),\(Log_i=\begin{cases} 0&i=1\\Log_{\left\lfloor \frac{i}{2}+1\right\rfloor}&i\geqslant 2 \end{cases}\)。
那么就好办了,对于一个区间 \([l,r]\) 最大值就是 max(mx[r][Log[r - l + 1]], mx[l + (1 << Log[r - l + 1]) - 1][Log[r - l + 1]])
。
询问 \(O(1)\) 处理。
P3865 【模板】ST 表
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m, mx[N][20], l, r, Log[N];
int main () {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m, Log[0] = -1;
for (int i = 1; i <= n; i++) {
cin >> mx[i][0], Log[i] = Log[i / 2] + 1;
}
for (int i = 1; i <= 16; i++) {
for (int j = (1 << i); j <= n; j++) {
mx[j][i] = max(mx[j][i - 1], mx[j - (1 << (i - 1))][i - 1]);
}
}
for (int i = 1; i <= m; i++) {
cin >> l >> r;
cout << max(mx[r][Log[r - l + 1]], mx[l + (1 << Log[r - l + 1]) - 1][Log[r - l + 1]]) << '\n';
}
return 0;
}