倍增法/st表 算法解析+例题

倍增法 / 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,jffi,j1,j1,意思是往上跳 2 j 2^j 2j 次,相当于先跳 2 j − 1 2^{j-1} 2j1 次,再往上跳 2 j − 1 2^{j-1} 2j1 次;
  • 可以用 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 depu2jdepv,即 u u u 往上跳 2 j 2^j 2j 次后深度不会跳到 v v v 的上面(不会比 v v v 浅),那么 f u , j → u f_{u,j}\to u fu,ju 并继续尝试 j − 1 j-1 j1,否则说明此时再按照 2 j 2^j 2j 次往上跳就会比 v v v 更高(比 v v v 浅),所以不跳,继续尝试 j − 1 j-1 j1​;
  • 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,ju,fv,jv 并继续尝试 j − 1 j-1 j1;否则说明再往上跳已经到达 LCA 乃至其祖先了,所以不跳,继续尝试 j − 1 j-1 j1
  • 最终答案即为 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 2x1+2x1=2x,即在以 2 x − 1 2^{x-1} 2x1 的跨度先后跳两次也会超过目标深度。

如果你发现 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+1opopar1opar。对于两个相交的区间 [ 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] [r2x1,r] 这两段区间的并整合出 [ l , r ] [l,r] [l,r] 的信息,其中 2 x 2^x 2x 为不大于 r − l + 1 r-l+1 rl+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,0ai,用与 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,jmax(fi,j1,fi+2j,j1),意为用 [ 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+2j1),[i+2j1,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(rl+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,fr2x+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 rl+1 的最大二的整数次幂,即 2 x ≤ r − l + 1 2^x\le r-l+1 2xrl+1。显然 [ l , l + 2 x ) [l,l+2^x) [l,l+2x) [ r − 2 x − 1 , r ] [r-2^x-1,r] [r2x1,r] 都被 [ l , r ] [l,r] [l,r] 包含。如果按上文所说划分,但两区间不相交,即
l + 2 x − 1 < r − 2 x − 1 l+2^x-1<r-2^x-1 l+2x1<r2x1
移项,得
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 rl+1 的二的整数次幂,显然 2 x 2^x 2x 并不是不大于 r − l + 1 r-l+1 rl+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+2j1),[i+2j1,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+2j1) [ k , k + 2 j − 1 ) [k,k+2^{j-1}) [k,k+2j1) 放到同一个集合,将 [ i + 2 j − 1 , i + 2 j ) [i+2^{j-1},i+2^j) [i+2j1,i+2j) [ k + 2 j − 1 , k + 2 j ) [k+2^{j-1},k+2^j) [k+2j1,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;
}
  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值