倍增法 / 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 的最大二的整数次幂,与之前矛盾。
例题
模板题(黄题)
按照上文做就行了。
中等题(绿题 → \to → 蓝题)
“求水最后会流到哪一个圆盘停止”提示我们可以用倍增跳,相对于求 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;
}
难题(紫题)
其实这题难在并查集的部分上:)
最终统计答案显然是将相同部分的数视为一个数字,计算贡献。
令同一个并查集中的元素必须相同。瓶颈在于区间与区间之间的处理,暴力方法显然是每个点每个点挨个合并到一个集合里。我们来联想一下区间上的连边优化:线段树、建虚点、分块
…
\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;
}