什么是倍增
- 倍增是根据已经得到的信息,将考虑的范围扩大一倍,从而加速操作的一种思想。
倍增方法应用之一快速幂代码
\\ a^b%p
#include <iostream>
#define ll long long
int main() {
int a, b, p, ans = 1;
std::cin >> a >> b >> p;
while (b) {
if (b & 1) ans = (ll)ans * a % p;
a = (ll)a * a % p;
b >>= 1;
}
std::cout << ans % p << '\n';
return 0;
}
int fast_pow(int a, int p) {
int ans = 1;
for (; p; p >>= 1, a = a * a)
if (p & 1)
ans = ans * a;
return ans;
}
倍增方法应用之二快速乘思想
计算 (a∗b)%p (a, b均是int64
范围内,p是int
范围)
解决思路:
- 设 b 的二进制表示为 b=Ck\*2k+Ck−1\*2k−1++C0\*20,其中 Ci∈{0,1}
- 那么 a\*b=a\*(k∑i=0Ci\*2i)=k∑i=0(a\*2i)\*Ci
- 由于 a\*2i=(a\*2i−1)\*2,而 Ci 可以由 b&1 导出,可以进行推导
倍增方法应用之三矩阵快速幂
两个前驱知识需要了解一下
a. 矩阵乘法
- 理解普通矩阵相乘
- 基本性质:满足结合律:(AB)C=A(BC)
b. 斐波那契数列作为应用:f[i]=f[i−1]+f[i−2],利用矩阵快速幂加快计算速度。
[01 11 ]\*[f1 f2 ]=[0\*f1+1\*f2 1\*f1+1\*f2 ]=[f2 f3 ]
[01 11 ]\*[f2 f3 ]=[0\*f2+1\*f3 1\*f2+1\*f3 ]=[f3 f4 ]
[01 11]\*[01 11]\*[f1 f2]=[01 11]\*[f2 f3]=[01 11]2\*[f2 f3]=[f3 f4]
[01 11 ]n−1\*[f1 f2 ]=[fn fn+1 ]
核心是矩阵倍增
倍增方法应用之四RMQ问题ST表
RMQ
问题是一个经典的可以用倍增来解决的问题:
给定一个长度为 n 的序列 A[1⋯n],有 q 次询问,每次询问给出 x,y,回答 A[x⋯y] 中的最大值。(也可以是最小值,此处以最大值为例)通常 n,q⩽
首先介绍利用倍增解决RMQ
问题的算法:ST(Sparse \, Table)算法
一般RMQ问题的ST算法
对于序列 A[1 \cdots n],我们构造一个二维数组 st[1 \cdots n][0 \cdots \log_2 n]
st[i][j] 表示从 i 这个位置开始,向后 2^j 个位置(包括 i)的最大值,即 Max\{A[i \cdots i + 2^j - 1]\}
这里如何构造ST
表呢?
这里利用倍增思想
构造数组 B,使得 B[i,j] 表示 A[i \cdots i + 2 ^ j - 1] 中最小数的下标。可以在 O(N\log_2 N) 的时间内得到这个数组。然后对于每对 i\, , j,令 k = \left\lfloor {{{\log }_2}\left( {j - i + 1} \right)} \right\rfloor ,则比较 A[B[i,k]] 与 A[B[j-2^k+1,k]] 的大小就可以得到结果了。每次询问时间复杂度为 O(1)
显然:st[i][0] = A[i]
除此之外,任何一个 st[i][j] 所表示的区间长度都是 2 的整数次方,即 2^j
我们将该区间从中间划分为两段,各长 2^{j-1}
容易得到,两段区间的起点分别为 i 和 i + 2 ^ {j - 1}
于是容易想到:
st[i][j] = max\{st[i][j-1], st[i + (1 << j - 1)][j-1]\}
我们先从 1 到 n 枚举 j,再顺序枚举 i,即可构造 st 数组
for (int i = 1; i <= n; ++i) st[i][0] = A[i];
for (int j = 1; (1 << j) <= n; ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i)
st[i][j] = max(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
时间复杂度是 O(n\log n)
构造出 st 表后该如何询问呢?
对于每次询问给出的x \, , y,其长度 len=y-x+1
我们先找到最大的且小于等于 len 的 2 的整数次幂,例如是2^k
那么 [x \cdots y] 这个区间的前 2^k 和 后 2^k 个位置合起来可以完全覆盖该区间
于是 Max\{A[x \cdots y]\} = max(st[x][k], st[y - (1 << k) + 1][k])
k = \left\lfloor {{{\log }_2}\left( {j - i + 1} \right)} \right\rfloor
在C++中可写为k=(int)(log(y-x+1) / log2)
int query(int x, int y) {
int k = (int)(log(y - x + 1) / log(2));
int m = max(st[x][k], st[y - (1 << k) + 1][k]);
return m;
}
时间复杂度 O(1)
倍增方法应用之五RMQ问题树上最近公共祖先
树上最近公共祖先,简称LCA
,是关于树的一个非常基础的概念:
给定一颗有根树,一个点到根节点的路径上的所有点称为该点的祖先。
若点 u 同时是点 x, y 的祖先,那么 u 称为 x, y 的公共祖先
。x,y 的所有公共祖先中,深度最大的点称为 x,y 的最近公共祖先
。
另一种理解:x 到 y 的路径上深度最小的点为 x,y 的最近公共祖先。
例如在该图中
以 1 为根节点
节点 5 和 6 的最近公共祖先就是节点 2
节点 5 和 1 的最近公共祖先就是节点 1
在倍增求解LCA的算法中,我们首先需要预处理出每个节点的深度(以 1 为根节点且深度为 1)
void dfs(int u, int fa) {
for (int o = b[u]; o != -1; o = e[o].next) {
if (e[o].v == fa) continue;
d[e[o].v] = d[u] + 1;
dfs(e[o].v, u);
}
}
int main() {
d[1] = 1;
dfs(1, 0);
}
接下来,我们需要构造一个数组 f[i][j],表示节点 i 向上走 2^j 步后到达哪个节点。如何构造该数组呢?
显然:
f[i][0] = i 的父节点
对于 j \neq 0,我们可以先向上走 2 ^ {j-1} 步,再向上走 2 ^ {j - 1} 步,其中第一次走 2 ^ {j - 1} 步后到达 f[i][j - 1]
于是f[i][j] = f[f[i][j-1]][j-1]
于是dfs可以改为如下形式:
void dfs(int u) {
for (int i = 1; i <= 16; ++i)
f[u][i] = f[f[u][i - 1]][i - 1];
for (int o = b[u]; o != -1; o = e[o].next) {
if (e[o].v == f[u][0]) continue;
d[e[o].v] = d[u] + 1;
f[e[o].v][0] = u;
dfs(e[o].v, u);
}
}
int main() {
d[1] = 1;
dfs(1);
}
这样我们构造出了 f[i][j]
接下来考虑如何求解 x,y 的 LCA
该算法主要分为两步:
1、将 x,y 调整到同一深度
2、求解 x,y 的LCA
我们不妨令 d[x]>d[y](否则swap(x,y)),考虑将 x 走到与 y 同一深度
我们当然可以让 x 一步一步向上爬,但是太慢
假设 x 需要向上走 a 步可以与 y 同一深度
我们可以将 a 按照二进制分解
例如 a=16,其二进制为 11010,即a=2^4+2^3+2^1
我们可以让 x 先走 2^4 步,再走 2^3 步,再走 2^1 步与 y 同深度,这一过程可以直接查询 f[i][j] 表
实际上,我们并不需要像快速幂一样将 a 模 2 来获得二进制,我们可以从大到小枚举 i,若 d[f[x][i]] \geq d[y],则令 x = f[x][i]
if (d[x] < d[y]) swap(x, y);
for (int i = 16; i >= 0; --i)
if (d[f[x][i]] >= d[y])
x = f[x][i];
这一步时间复杂度为 O(\log n)
在将 x,y 调整到同一深度后,进行求LCA
的第二步操作
在此之前,一定注意:若 x 已经与 y 相同,则 LCA = x!!!
接下来就是 x, y 深度相同后点不同的情况
对于这种情况,我们每次将 x,y 向上走同样的距离,并使得 x,y 在保持不同的情况下尽量靠上,如此以后,LCA=f[x][0]
考虑如何进行这一步操作
例题1:Doubling
在 N 个洞的沙坑中住着一只蚂蚁。这只蚂蚁的动作很有规律,在进入洞 i 的第 2 天就会移动到洞 A_i。
关于那个,请处理以下 Q 个询问:
- 第 j 个询问:如果蚂蚁当前在洞 X_j,Y_j 天后在哪个洞?
限制:
- 1 \leqslant N, Q \leqslant 10^5
- 1 \leqslant A_i, X_j \leqslant N
- 1 \leqslant Y_j \leqslant 10^9
算法分析
记 dp[d][x]
表示蚂蚁当前在洞 x 时,在经过 2^d 天后进入哪个洞
C++ 代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, q;
cin >> n >> q;
vector<int> a(n);
rep(i, n) cin >> a[i];
rep(i, n) a[i]--;
const int D = 30;
vector dp(D, vector<int>(n));
rep(d, D)rep(i, n) {
if (d == 0) dp[d][i] = a[i];
else dp[d][i] = dp[d-1][dp[d-1][i]];
}
rep(qi, q) {
int x, y;
cin >> x >> y;
--x;
int ans = x;
rep(d, D) if (y>>d&1) ans = dp[d][ans];
++ans;
cout << ans << '\n';
}
return 0;
}
例题2:Calculator
对于 1, 2, \cdots, N 中的每个数,求出执行以下操作 K 次后得到的整数。
- 用自身减去它在十进制表示下的每一位上的数
限制:
- 1 \leqslant N \leqslant 3 \times 3 \times 10^5
- 1 \leqslant K \leqslant 10^9
算法分析
记 dp[d][x]
表示对 x 执行 2^d 次操作后得到的整数
C++ 代码
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, k;
cin >> n >> k;
const int D = 30;
vector dp(D, vector<int>(n+1));
for (int i = 1; i <= n; ++i) {
string s = to_string(i);
int now = i;
for (char c : s) {
now -= c-'0';
}
dp[0][i] = now;
}
for (int d = 1; d < D; ++d) {
for (int i = 1; i <= n; ++i) {
dp[d][i] = dp[d-1][dp[d-1][i]];
}
}
for (int i = 1; i <= n; ++i) {
int ans = i;
rep(d, D) if (k>>d&1) ans = dp[d][ans];
cout << ans << '\n';
}
return 0;
}
%%%%%%%%%