『学习笔记』倍增

1|0概念

在进行递推时,如果一个一个递推,时间复杂度是线性的,在 \(n\) 巨大的时候就会严重超时。于是我们采用成倍增长的方式进行递推,把所有 \(f_{2^i}\) 求出来。当我们想要某个位置的值时,我们利用十进制与二进制的每个数一一对应的性质即每个数都能拆成多个 \(2\) 的整数次幂的和(二进制拆分),使用原本已求出来的值来求我们想求出的值。

倍增的几大用处有 ST 表,LCA 和(矩阵)快速幂。

2|0ST 表

2|1引入

ST 表处理的是所有符合结合律且可重复贡献的信息查询(包括但不限于 RMQ、最大公因数、最小公倍数、按位与、按位或)。它是基于倍增和动态规划的思想,可以实现离线 \(\mathcal{O}(n\log_2n)\) 预处理、在线 \(O(1)\) 查询,但是不支持在线修改。

2|2例题

luogu P3865 【模板】ST 表

1|0方法一:暴力查找

对于每次询问,查找 \(\max\limits_{i\in[l,r]}\{a_i\}\),最直接的做法就是每次询问都遍历一遍查询最大值。
时间复杂度 \(\mathcal{O}(n\times m)\),空间复杂度 \(\mathcal{O}(n)\)

点击查看代码
#include<bits/stdc++.h> using namespace std; const int N = 1e5 + 5; int n, m, a[N]; int main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; i++) { cin >> a[i]; } while(m--) { int l, r, maxi = -1; cin >> l >> r; for(int i = l; i <= r; i++) { maxi = max(maxi, a[i]); } cout << maxi << '\n'; } return 0; }

1|0方法二:暴力预处理

不难想到由于会有重复的区间,所以可以预处理出所有 \([l,r]\) 的最大值,询问时直接输出即可。
此做法适用于 \(n\) 不大,但 \(m\) 很大,时间复杂度 \(\mathcal{O}(n^2)\),空间复杂度 \(\mathcal{O}(n^2)\)

for(int i = 1; i <= n; i++) { f[i][i]= a[i]; for(int j = i + 1; j <= n; j++) { f[i][j] = max(f[i][j - 1], a[i]); } }

1|0方法三:ST 表预处理

我们令 \(f_{i,j}\) 表示从 \(i\) 开始的 \(2^j\) 个元素的最大值,需要满足 \(1\le i\le n\)\(i+2^j-1\le n\)
显然 \(f_{i,0}=a_i\)。我们从 \(i-1\) 转移到 \(i\),在 \([i,i+2^j-1]\) 内长度为 \(2^{j-1}\) 的子区间有两个,分别是 \([i,i+2^{j-1}-1]\)\([i+2^{j-1},i+2^j-1]\),两个子区间的最大值是 \(f_{i,j-1}\)\(f_{i+2^{j-1},j-1}\)
从而得到状态转移方程为:

\[f_{i,j}=\max\{f_{i,j-1},f_{i+2^{j-1},j-1}\} \]

预处理完后我们来思考一下对于任意区间 \([l,r]\) 的最大值该分成哪几个子区间。
\(k=\log_2\{len_{[l,r]}\}=\log_2\{r-l+1\}\),则第一个想到的子区间必然是 \([l,l+2^k-1]\),然而剩下的子区间 \([l+2^k,r]\) 的长度不一定为 \(2\) 的整数次幂,这就体现了区间最大的可重复贡献的性质,这也就代表两个子区间可以有交集,所以另一个区间是 \([r-2^k+1,r]\)
由于 \(\log\) 函数常数较大,所以可以递推预处理出 \(1\sim n\)\(\log\) 值,\(h_i=h_{\left\lfloor i\div 2\right\rfloor}+1\)

点击查看代码
#include<bits/stdc++.h> using namespace std; const int N = 2e5 + 5; int n, m, a[N], f[N][25], h[N]; int main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; i++) { cin >> a[i]; f[i][0] = a[i]; } for(int i = 1; i <= 20; i++) { for(int j = 1; j + (1 << i) - 1 <= n; j++) { f[j][i] = max(f[j][i - 1], f[j + (1 << (i - 1))][i - 1]); } } for(int i = 2; i <= n; i++) { h[i] = h[i / 2] + 1; } while(m--) { int l, r, s, ans; cin >> l >> r; s = h[r - l + 1]; ans = max(f[l][s], f[r - (1 << s) + 1][s]); cout << ans << '\n'; } return 0; }

2|3练习

  1. luogu P3865 【模板】ST 表:区间最大

  2. luogu P1816 忠诚:区间最小

  3. luogu P2880 [USACO07JAN] Balanced Lineup G:区间最大、区间最小

  4. luogu P2471 [SCOI2007] 降雨量:区间最大

  5. luogu P1440 求m区间内的最小值:区间最小

  6. luogu P2251 质量检测:区间最小

3|0LCA 最近公共祖先

3|1引入

在一棵有根树上的两个节点 \(u,v\),它两的祖先集合 \(f(u)\)\(f(v)\) 的交集 \(f(u)\cap f(v)\) 里深度最大的节点元素。

3|2例题

luogu P3379 【模板】最近公共祖先(LCA)

1|0方法一:暴力查找

我们先把 \(u\) 的所有祖先标记出来,接着让 \(v\) 向上推进,如果第一次找到一个被标记的节点 \(w\),则其一定是 \(u,v\) 的最近公共祖先。

点击查看代码
#include<bits/stdc++.h> using namespace std; const int N = 5e5 + 5; int n, m, s, f[N], ans; bool vis[N]; vector<int> e[N]; inline void dfs(int x, int fa) { f[x] = fa; for(int i : e[x]) { if(i == fa) { continue; } dfs(i, x); } } inline void dfs1(int x) { vis[x] = 1; if(x == s) { return ; } dfs1(f[x]); } inline void dfs2(int x) { if(ans) { return ; } if(vis[x]) { ans = x; return ; } dfs2(f[x]); } int main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n >> m >> s; for(int i = 1; i < n; i++) { int x, y; cin >> x >> y; e[x].push_back(y); e[y].push_back(x); } dfs(s, 0); while(m--) { ans = 0; int a, b; cin >> a >> b; memset(vis, 0, sizeof(vis)); dfs1(a); dfs2(b); cout << ans << '\n'; } return 0; }

1|0方法二:倍增求 LCA

先 dfs 预处理出每个节点的深度和 \(2^k\) 的祖先。
代码如下:

int f[N][21], dep[N]; inline void dfs(int u, int fa) { dep[u] = dep[fa] + 1; f[u][0] = fa; for(int i = 1; i <= 20; i++) { f[u][i] = f[f[u][i - 1]][i - 1]; } for(int i = head[u]; i; i = e[i].next) { int v = e[i].to; if(v == fa) { continue; } dfs(v, u); } }

然后对于两个节点 \(u,v\),不妨设 dep[u] >= dep[v]。枚举 \(i\)\(20\)\(0\),每次只要 dep[fa[u][i]] >= dep[v],那么就 u = fa[u][i]。这个过程类似于二进制拆分。
\(u,v\) 的深度相同时,再让 \(u,v\) 同时往上推进,直到相等。
代码如下:

inline int lca(int x, int y) { if(dep[x] < dep[y]) { swap(x, y); } for(int i = 20; i >= 0; i--) { while(dep[f[x][i]] >= dep[y]) { x = f[x][i]; } if(x == y) { return x; } } for(int i = 20; i >= 0; i--) { if(f[x][i] != f[y][i]) { x = f[x][i], y = f[y][i]; } } return f[x][0]; }

3|3练习

  1. luogu P3379 【模板】最近公共祖先(LCA)

  2. luogu P2420 让我们异或吧(注:此题由于按位异或具有任何数和自己异或结果为零的性质,所以可以不用 LCA)

  3. luogu P3398 仓鼠找 sugar

4|0快速幂

4|1引入

由于直接求 \(a^b\bmod p\) 时间复杂度为 \(\mathcal{O}(b)\),所以可以采用 \(a^b=(a^{\left\lfloor b\div 2\right\rfloor})^2\times\begin{cases}a&\text{if }b\equiv1\pmod 2\\1&\text{if }b\equiv0\pmod2\end{cases}\) 来进行递推或递推。

4|2例题

luogu P1226 【模板】快速幂

1|0方法一:递归

显然,\(a^0=1\),所以递归边界为 \(b=0\)。令 tmp = qpow(b / 2) 转移就是 qpow(b) = t * t % p * (b & 1 ? a : 1) % p
代码如下:

inline int qpow(int a, int n) { if(!n) { return 1; } int tmp = qpow(a, n / 2); if(n & 1) { return tmp * tmp % p * a % p; } else { return tmp * tmp % p; } }

1|0方法二:递推

思路同上,代码如下:

inline int qpow(int a, int n) { int ans = 1; while(n) { if(n & 1) { ans = (ans * a) % p; } a = (a * a) % p; n >>= 1; } return ans % p; }

__EOF__

本文作者cyf1208
本文链接https://www.cnblogs.com/cyf1208/p/17749737.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyf1208  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示