倍增法 / st
表
基本
倍增法及 LCA
倍增,顾名思义就是成倍的增加。主要思想就是将问题的大区间分成 log n \log n logn 个小块,每个块的长度为一个尽可能大的二的整数次幂,对于每个块用类似动态规划的方法 O ( n log n ) O(n\log n) O(nlogn) 预处理出来这部分的信息,最终用这些小块整合成大区间。
能够把 O ( n ) O(n) O(n) 的时间复杂度降到 O ( log n ) O(\log n) O(logn)。
以一个经典的问题入手:
给定一棵树,若干组询问求 u , v u,v u,v 的最近公共祖先,即
LCA
。
倍增法求 LCA
的基本流程:
- 预处理出第 i i i 个点往上跳 2 j 2^j 2j 次能跳到的点( j j j 为非负整数),记为 f i , j f_{i,j} fi,j,显然 f i , 0 f_{i,0} fi,0 就是 i i i 的父亲;
- 预处理方法:从小到大枚举 j j j, f i , j ← f f i , j − 1 , j − 1 f_{i,j}\gets f_{f_{i,j-1},j-1} fi,j←ffi,j−1,j−1,意思是往上跳 2 j 2^j 2j 次,相当于先跳 2 j − 1 2^{j-1} 2j−1 次,再往上跳 2 j − 1 2^{j-1} 2j−1 次;
- 可以用
dfs/bfs
实现,保证自己祖先们的 f f f 已经完善,还可以顺便计算一下每个点的深度 d e p dep dep; - 对于一组询问 u , v u,v u,v(这里默认 d e p u > d e p v dep_u>dep_v depu>depv,即树中 u u u 在 v v v 的下面),先让 u u u 往上跳到与 v v v 深度相同;
- 往上跳的方法:从大到小尝试一个二的整数次幂 2 j 2^j 2j,如果 d e p u − 2 j ≥ d e p v dep_u-2^j\ge dep_v depu−2j≥depv,即 u u u 往上跳 2 j 2^j 2j 次后深度不会跳到 v v v 的上面(不会比 v v v 浅),那么 f u , j → u f_{u,j}\to u fu,j→u 并继续尝试 j − 1 j-1 j−1,否则说明此时再按照 2 j 2^j 2j 次往上跳就会比 v v v 更高(比 v v v 浅),所以不跳,继续尝试 j − 1 j-1 j−1;
- 当 d e p u = d e p v dep_u=dep_v depu=depv 时,即 u , v u,v u,v 已经在同一深度,如果 u = v u=v u=v,则说明 u u u(或 v v v)就是原来两个点的
LCA
,直接返回 u u u 即可; - 否则就要让 u , v u,v u,v 一起往上跳,直到 u , v u,v u,v 的父亲相同;
- 往上跳的方法:也是从大到小尝试一个二的整数次幂 2 j 2^j 2j,如果 f u , j ≠ f v , j f_{u,j}\ne f_{v,j} fu,j=fv,j,即往上跳 2 j 2^j 2j 次后 u , v u,v u,v 仍然在
LCA
的两棵不同的子树里,那么 f u , j → u , f v , j → v f_{u,j}\to u,f_{v,j}\to v fu,j→u,fv,j→v 并继续尝试 j − 1 j-1 j−1;否则说明再往上跳已经到达LCA
乃至其祖先了,所以不跳,继续尝试 j − 1 j-1 j−1; - 最终答案即为 f u , 0 f_{u,0} fu,0(或 f v , 0 f_{v,0} fv,0)。
给出一个模板:
const int maxn = 1e6 + 5;
const int LOG2 = 20; // 每个点能往后跳的极限。依情况而定,可以设大一些,但需要保证不小于log n。
int f[maxn][LOG2],dep[maxn];
vector<int> mp[maxn]; // 树
void dfs(int u,int fa) { // 预处理for (int i = 1;i <= LOG2;i ++) f[u][i] = f[f[u][i - 1]][i - 1];// 如果从u开始跳2^i步后点不存在,则默认为0(由于dep[0]也默认为0,可以认为跳到比根节点更浅了)。for (auto v : mp[u])if (v != fa) dfs(v,u);
}
int lca(int u,int v) {if (dep[u] < dep[v]) return lca(v,u); // 默认u在v的下面for (int i = LOG2;i >= 0;i --)if (dep[u] - (1 << i) >= dep[v]) // 往上跳2^i后不会比v浅// 如果此时跳2^i后点不存在,那么dep[u]-(1<<i)就是负数,符合逻辑。u = f[u][i]; // 跳if (u == v) return u; // 特判u直接跳到lca上了。for (int i = LOG2;i >= 0;i --)if (f[u][i] != f[v][i]) // 还没有跳到 LCA 及其祖先上// 为什么可以直接跳到LCA上也不跳?因为我们并不知道当前的公共祖先是不是最近的(最浅的),所以一律不跳,最后u,v的父亲一定是LCA。// 同理,此处如果跳2^i后点不存在,则f[u][i]和f[v][i]都为0,0==0成立所以不会往上跳,符合逻辑。u = f[u][i], v = f[v][i];return f[u][0];
}
可见,我们总是以一个比较大的二的整数次幂开始尝试跳的。
为什么倍增法一般用二的整数次幂为长度划分小块?
以求 LCA
为例,如果你以二的整数次幂划分( f i , j f_{i,j} fi,j 表示 i i i 向上跳 2 j 2^j 2j 步到达的点),则在跳的过程中发现 2 x 2^x 2x 已经超过目标深度(比 v v v 的深度小了或者已经到达 LCA
及其祖先上),因为 2 x − 1 + 2 x − 1 = 2 x 2^{x-1}+2^{x-1}=2^x 2x−1+2x−1=2x,即在以 2 x − 1 2^{x-1} 2x−1 的跨度先后跳两次也会超过目标深度。
如果你发现 2 x 2^x 2x 没有到达目标深度(比 v v v 的深度还大或者仍没有到达一个相同的祖先),因为 x x x 是从一个较大值到小尝试的,显然对于第一个能跳的 2 x 2^x 2x, 2 x + 1 2^{x+1} 2x+1 绝对跳不了;以此类推,如果 2 x 2^x 2x 能跳两次或者更多,则因为 2 x + 2 x = 2 x + 1 2^x+2^x=2^{x+1} 2x+2x=2x+1,即跳这两次及以上可以等同于跳若干次跨度为 2 x + 1 2^{x+1} 2x+1 后再跳一次 2 x 2^x 2x(或者不用跳);对于 2 x + 1 2^{x+1} 2x+1 跳若干次的情况也是一样的。
综上,对于每个二的整数次幂只需尝试一次,保证了 O ( log n ) O(\log n) O(logn) 的复杂度,实现也方便。如果以其他数的整数次幂划分,以三为例,如果 3 x 3^x 3x 能跳,因为 3 x + 3 x ≠ 3 x + 1 3^x + 3^x\ne3^{x+1} 3x+3x=3x+1,即 3 x 3^x 3x 可能还可以再跳一次,虽然也能写,但肯定比二复杂。所以不常用。
st
表
倍增思想再进一步就可以得到 st
表。
ST 表是用于解决 可重复贡献问题 的数据结构。 ——oi-wiki
什么是可重复贡献问题?我们定义一种运算 op \operatorname{op} op,使得 x op x = x x\operatorname{op}x=x xopx=x。进一步推广到区间上,设 a [ l , r ] a_{[l,r]} a[l,r] 表示 a l op a l + 1 op … op a r − 1 op a r a_l\operatorname{op}a_{l+1}\operatorname{op}\dots\operatorname{op}a_{r-1}\operatorname{op}a_r alopal+1op…opar−1opar。对于两个相交的区间 [ l 1 , r 1 ] , [ l 2 , r 2 ] [l1,r1],[l2,r2] [l1,r1],[l2,r2](相交部分为 [ l 2 , r 1 ] [l2,r1] [l2,r1]),则
a [ l 1 , r 1 ] op a [ l 2 , r 2 ] = a [ l 1 , l 2 ) op a [ l 2 , r 1 ] op a [ l 2 , r 1 ] op a ( r 1 , r 2 ] = a [ l 1 , l 2 ) op a [ l 2 , r 1 ] op a ( r 1 , r 2 ] = a [ l 1 , r 2 ] \begin{aligned} {} &\ a_{[l1,r1]}\operatorname{op}a_{[l2,r2]} &\\ = &\ a_{[l1,l2)}\operatorname{op}a_{[l2,r1]}\operatorname{op}a_{[l2,r1]}\operatorname{op}a_{(r1,r2]} &\\ = &\ a_{[l1,l2)}\operatorname{op}a_{[l2,r1]}\operatorname{op}a_{(r1,r2]} &\\ = &\ a_{[l1,r2]} \end{aligned} === a[l1,r1]opa[l2,r2] a[l1,l2)opa[l2,r1]opa[l2,r1]opa(r1,r2] a[l1,l2)opa[l2,r1]opa(r1,r2] a[l1,r2]
由上可知:以 op \operatorname{op} op 为基本运算,我们可以用两个相交区间的信息推出它们的并的信息。满足 op \operatorname{op} op 性质的预算有按位与(&
)、按位或(|
)、最大值、最小值等等。
st
表就由此诞生了。先预处理出以每个点为起点、长度为二的整数次幂的区间的信息(比如说区间最大值);对于询问 [ l , r ] [l,r] [l,r] 这一区间的信息,用预处理好的 [ l , l + 2 x ) [l,l+2^x) [l,l+2x) 和 [ r − 2 x − 1 , r ] [r-2^x-1,r] [r−2x−1,r] 这两段区间的并整合出 [ l , r ] [l,r] [l,r] 的信息,其中 2 x 2^x 2x 为不大于 r − l + 1 r-l+1 r−l+1 的最大二的整数次幂。
也已一个经典的 RMQ
问题为例:
给定一个数组 a a a,若干次形如 [ l , r ] [l,r] [l,r] 的询问求 [ l , r ] [l,r] [l,r] 中的最大值。
RMQ
基本流程:
- 设 f i , j f_{i,j} fi,j 表示 [ i , i + 2 j ) [i,i + 2^j) [i,i+2j) 中的最大值, f i , 0 ← a i f_{i,0}\gets a_i fi,0←ai,用与
LCA
差不多的预处理方法把 f f f 预处理出来; - 具体地,先从小到大枚举 j j j,后枚举 i i i,则 f i , j ← max ( f i , j − 1 , f i + 2 j , j − 1 ) f_{i,j}\gets\max(f_{i,j-1},f_{i+2^j,j-1}) fi,j←max(fi,j−1,fi+2j,j−1),意为用 [ i , i + 2 j − 1 ) , [ i + 2 j − 1 , i + 2 j ) [i,i+2^{j-1}),[i+2^{j-1},i+2^j) [i,i+2j−1),[i+2j−1,i+2j) 两者的最大值取一个 max \max max 整合出 [ i , i + 2 j ) [i,i+2^j) [i,i+2j) 的最大值;
- 对于每个询问 [ l , r ] [l,r] [l,r],令 x = ⌊ log ( r − l + 1 ) ⌋ x=\lfloor\log(r-l+1)\rfloor x=⌊log(r−l+1)⌋,则 [ l , r ] [l,r] [l,r] 的最大值即为 max ( f l , x , f r − 2 x + 1 , x ) \max(f_{l,x},f_{r-2^x+1,x}) max(fl,x,fr−2x+1,x)。
也给一个模板:
const int maxn = 100005;
const int maxl = 30; // 与LCA同理,可以大一些。
int a[maxn],f[maxn][maxl],log_2[maxn * 3];
// a为原数组,log_2为预处理的log(x)向下取整。
int n;
void pre() {for (int k = 0;k <= maxl;k ++) // 很好理解的预处理,其实意义不大,可以用STL中给的函数。for (int i = (1 << k);i < (1 << (k + 1)) && i <= n;i ++) log_2[i] = k;for (int i = 1;i <= n;i ++) f[i][0] = a[i];for (int j = 1;j < maxl;j ++) // 合并出大区间for (int i = 1;i + (1 << j) - 1 <= n ;i ++) // 一定要先枚举j再枚举i,原因可以看转移方程理解一下。f[i][j] = max(f[i][j - 1],f[i + (1 << (j - 1))][j - 1]);
}
int query(int L,int R) { // 查询[l,r]的最大值。int t = log_2[R - L + 1];return max(f[L][t],f[R - (1 << t) + 1][t]);
}
为什么 [ l , r ] [l,r] [l,r] 可以如上文那么拆?不会多或少吗?
仍然令 2 x 2^x 2x 为不大于 r − l + 1 r-l+1 r−l+1 的最大二的整数次幂,即 2 x ≤ r − l + 1 2^x\le r-l+1 2x≤r−l+1。显然 [ l , l + 2 x ) [l,l+2^x) [l,l+2x) 和 [ r − 2 x − 1 , r ] [r-2^x-1,r] [r−2x−1,r] 都被 [ l , r ] [l,r] [l,r] 包含。如果按上文所说划分,但两区间不相交,即
l + 2 x − 1 < r − 2 x − 1 l+2^x-1<r-2^x-1 l+2x−1<r−2x−1
移项,得
l + 2 x + 1 < r l+2^{x+1}<r l+2x+1<r
此时可以发现存在一个比 2 x 2^x 2x 更大的 2 x + 1 2^{x+1} 2x+1 满足不大于 r − l + 1 r-l+1 r−l+1 的二的整数次幂,显然 2 x 2^x 2x 并不是不大于 r − l + 1 r-l+1 r−l+1 的最大二的整数次幂,与之前矛盾。
例题
模板题(黄题)
- P3865 【模板】ST 表
- P3379 【模板】最近公共祖先(LCA)
按照上文做就行了。
中等题(绿题 → \to → 蓝题)
- P7167 [eJOI2020 Day1] Fountain
“求水最后会流到哪一个圆盘停止”提示我们可以用倍增跳,相对于求 lCA
而言,这里判断能不能跳就要比较水量,而水量在跳的过程中又会流失,所以还需要预处理出跳这么多步会流掉多少水。其他就没什么了。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
const int inf = 1e9;
int n,q,d[maxn],c[maxn];
int st[maxn],top;
int fr[maxn],head[maxn],cnt;
struct Edge { int next,v; } e[maxn];
void addEdge(int u,int v) {e[++ cnt] = Edge{head[u],v};head[u] = cnt;
}
int f[maxn][21],g[maxn][21],dep[maxn];
void dfs(int u,int fa) {dep[u] = dep[f[u][0] = fa] + 1;g[u][0] = c[fa];for(int i = 1;(1 << i) <= dep[u];i ++) f[u][i] = f[f[u][i - 1]][i - 1],g[u][i] = g[f[u][i - 1]][i - 1] + g[u][i - 1];for(int i = head[u],v;i;i = e[i].next) {v=e[i].v;dfs(v,u);}
}
int main() {scanf("%d%d",&n,&q);for(int i = 1;i <= n;i ++) scanf("%d%d",&d[i],&c[i]);d[n + 1] = c[n + 1] = inf;st[++ top] = 1;for(int i = 2;i <= n + 1;i ++) {while (top > 0 && d[i] > d[st[top]]) fr[st[top --]] = i;st[++ top] = i;}for(int i = 1;i <= n;i ++) addEdge(fr[i],i);dfs(n + 1,0);for(int i = 1,u,v,ans;i <= q;i ++) {scanf("%d%d",&u,&v);if (c[u] >= v) {printf("%d\n",u);continue;}v -= c[u], ans = 0;for(int i = 20;i >= 0;i --) {if (g[u][i] <= v && (1 << i) <= dep[u]) v -= g[u][i], u = f[u][i];if (v==0) ans = u;}if (ans == 0) ans = f[u][0];if (ans > n) puts("0");else printf("%d\n",ans);}return 0;
}
- P5629 【AFOI-19】区间与除法
题解
难题(紫题)
- P3295 [SCOI2016] 萌萌哒 紫题
其实这题难在并查集的部分上:)
最终统计答案显然是将相同部分的数视为一个数字,计算贡献。
令同一个并查集中的元素必须相同。瓶颈在于区间与区间之间的处理,暴力方法显然是每个点每个点挨个合并到一个集合里。我们来联想一下区间上的连边优化:线段树、建虚点、分块 … \dots … 这里我们考虑分块,分成 log n \log n logn 块,相当于建一个 st
表。
令 f i , j f_{i,j} fi,j 表示 [ i , i + 2 j ) [i,i+2^j) [i,i+2j) 这个区间所属的集合。对于每个操作 [ l 1 , r 1 ] , [ l 2 , r 2 ] [l1,r1],[l2,r2] [l1,r1],[l2,r2],我们把这两个区间按照 2 2 2 的幂次分块合并。等所有区间都处理完了,我们需要把区间上的集合信息降到点上去。
对于一段大区间 [ i , i + 2 j ) [i,i+2^j) [i,i+2j),将它的集合信息降到 [ i , i + 2 j − 1 ) , [ i + 2 j − 1 , i + 2 j ) [i,i+2^{j-1}),[i+2^{j-1},i+2^j) [i,i+2j−1),[i+2j−1,i+2j) 两个小区间上,设 [ i , i + 2 j ) [i,i+2^j) [i,i+2j) 所在集合在并查集中的根节点为 [ k , k + 2 j ) [k,k + 2^j) [k,k+2j),则将 [ i , i + 2 j − 1 ) [i,i+2^{j-1}) [i,i+2j−1) 和 [ k , k + 2 j − 1 ) [k,k+2^{j-1}) [k,k+2j−1) 放到同一个集合,将 [ i + 2 j − 1 , i + 2 j ) [i+2^{j-1},i+2^j) [i+2j−1,i+2j) 和 [ k + 2 j − 1 , k + 2 j ) [k+2^{j-1},k+2^j) [k+2j−1,k+2j) 放到同一个集合。这样一一对应,降到点上时也就是一一对应的了。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e5 + 5;
const int P = 1e9 + 7;
int fa[maxn][30],n,m;
int find(int x,int b) {return x == fa[x][b] ? x : fa[x][b] = find(fa[x][b],b);
}
void Union(int x,int y,int b) {if (find(x,b) == find(y,b)) return ;fa[find(x,b)][b] = find(y,b);
}
ll ans;
int main() {scanf("%d%d",&n,&m);int l1,r1,l2,r2;for (int i = 1;i <= n;i ++)for (int j = 0;j <= 20;j ++)fa[i][j] = i;while (m --) {scanf("%d%d%d%d",&l1,&r1,&l2,&r2);for (int i = 20;i >= 0;i --) if (l1 + (1 << i) - 1 <= r1) {Union(l1,l2,i);l1 += (1 << i), l2 += (1 << i);}}for (int i = 20;i;i --) // 把集合信息降下去for (int j = 1;j + (1 << i) - 1 <= n;j ++) {int k = find(j,i);Union(j,k,i - 1); Union(j + (1 << (i - 1)),k + (1 << (i - 1)),i - 1);}for (int i = 1;i <= n;i ++)if (fa[i][0] == i) {if (ans == 0) ans = 9;else ans = (ans * 10ll) % P;}printf("%lld",ans);return 0;
}