倍增专题 ST
倍增
1 2 3 4 5 6 7 | 倍增: 倍增就是翻倍, 它能让线性的处理转化为对数级的处理, 优化了时间复杂度 注: 相当于二分的反方向 原理: 对于一种操作f(x) 通过计算f(x), f^2(x), f^4(x), ...f2^k(x)可以以Log2(n)的速度 求解出f^n(x) 解释:任意整数都可以拆分为若干以2为底的幂项和 以这个作为出发点就可以引发出很多基础算法等等 |
快速幂: (就是利用了倍增的思想)
1 | 快速幂求解的就是a ^ b % p的解 |
1 | 根据倍增不难得出这个公式 |
Code展示:
1 2 3 4 5 6 7 | using i64 = int64_t; constexpr i64 mod = 1e9 + 7; // 998244353 i64 power (i64 a, i64 b, i64 p = mod) { i64 res = 1; for ( ; b > 0; b >>= 1, a = a * a % p) if (b & 1) res = res * a % p; return res; } |
龟速乘(快速乘):
推出
Code:
1 2 3 4 5 6 | i64 ksc(i64 a, i64 b, i64 mod) { i64 res = 0; for (; b; b >>= 1, a = (a << 1) % mod) if (b & 1) res = (res + a) % mod; return res; } |
1 2 3 | 跳跃查询:通过预处理构建一个可以跳跃到更远的节点或区间的表 预处理:在预处理阶段构建一个st表 能过快速查询结果 复杂度:O(log2(n)) |
博客资料来源:
例题:
P4155 [SCOI2015] 国旗计划
题意:
1 | 在一个环上有一些区间, 询问在当前i区间强制选择的前提下 (1 <= i <= n), 用最少区间覆盖整个环的个数 |
分析题目中的关键信息:
1 | 每名边防战士的奔袭区间都不会被其他边防战士的奔袭区间所包含 |
1 2 3 4 5 6 7 | 首先对于环形题目首先想到破环成链, 我们从关键信息下手不难发现, 每个战士的区间都不会被其他战士的区间所覆盖 那么可以得到左区间l, 和右区间r 明显是严格递增的, 其次在每次询问的时候, 选择下一个区间的时候必然是需要右 交集的, 那么我们可以通过排序对l进行排序, 每一次找到选择排外序后尽量选择a[idx].l靠近a[idx].r最右边的区间 在当前图中a[1].[l, r]这个区间我们应当选择a[3].[l, r]这个区间 目前所用的基础算法是一个典型的贪心问题(区间覆盖问题): 通过局部最优推向全局最优(是一个最优策略的贪心问题) |
复杂度分析:
1 2 | 在这里光用贪心的写法, 必然是超时的 是因为在每次查询的时候需要从强制选择当前的点继续循环下去 所利用的时间是O(n) 那么n次查询也成了(n ^ 2) 在这个复杂度条件下, 光使用贪心算法是不行的 |
修改点:
1 | 修改点只能在每次询问需要枚举到的线段中做手脚, 那么可以使用倍增预处理, 以O(1)的时间复杂度输出查询的结果 |
解释为什么使用倍增(理由):
1 2 3 4 5 6 | 倍增算法不一定需要满足递增关系,但通常用于处理某种结构性或递归性的关系。这种关系可以是递推的、可分解的,而不一定是单纯的递增关系, 那么在这里当前的战士到下一个战士的 路径其实是固定的, 假设当前i的转移对象f[i], 那么f[i]的下一个转移对象是f[f[i]] 它的路径已经是固定好了, 那么问题就变成了加速转移, 那么倍增它来了 原本暴力求解是我一步一步跳, 这里我倍增的跳, 那么我们可以建张表表示st[i][j]表示i战士我直接跳2 ^ j后的下一个战士 指数从大到小枚举, 确保下一次跳跃的战士的覆盖范围小于i + m, 记录ans, "最后再来一次转移使得整个区间能过覆盖i + m" , 输出ans + 1即可 |
递归公式:
关键代码讲解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 区间扩展: (破环成链): if (a[i].l > a[i].r) a[i].r += m; a[i + n] = {a[i].l + m, a[i].r + m, a[i].id}; 倍增表的构建: for ( int j = 1; (1 << j) <= len; j++) { for ( int i = 1; i <= len; i++) { st[i][j] = st[st[i][j - 1]][j - 1]; } } 倍增查询过程: ed = i + m for ( int j = 20; j >= 0; j--) { int to = st[cbt][j]; if (to && a[to].r < ed) { cnt += (1 << j); cbt = to; } } 结果输出: ans[a[i].id] = cnt + 1; 注意: 由于题目很坑 尽量不要在查询的时候输出结果 这是错误的, 因为排完序后的结果是不一样的 |
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | #include <bits/stdc++.h> using namespace std; using i64 = int64_t; constexpr int N = 2e5 + 5; struct node { int l, r, id; bool operator < ( const node &b) const { return l < b.l; } } a[N << 1]; int st[N << 1][25], ans[N] {}, len; void solve() { int n, m; cin >> n >> m; for ( int i = 1; i <= n; i++) { cin >> a[i].l >> a[i].r; if (a[i].l > a[i].r) a[i].r += m; a[i].id = i; } sort(a + 1, a + n + 1); len = n << 1; for ( int i = 1; i <= n; i++) { a[i + n] = {a[i].l + m, a[i].r + m, a[i].id}; } int idx = 1; for ( int i = 1; i <= len; i++) { while (idx <= len && a[idx].l <= a[i].r) idx++; st[i][0] = idx - 1; } for ( int j = 1; (1 << j) <= len; j++) { for ( int i = 1; i <= len; i++) { st[i][j] = st[st[i][j - 1]][j - 1]; } } for ( int i = 1; i <= n; i++) { int ed = a[i].l + m, cbt = i, cnt = 1; for ( int j = 20; j >= 0; j--) { int to = st[cbt][j]; if (to && a[to].r < ed) { cnt += (1 << j); cbt = to; } } ans[a[i].id] = cnt + 1; } for ( int i = 1; i <= n; i++) { cout << ans[i] << (i == n ? "\n" : " " ); } } int main() { cin.tie(0) -> sync_with_stdio( false ); int t = 1; // cin >> t; while (t--) { solve(); } return 0; } |
ST表
介绍ST表 && 什么是可重复贡献问题
1 2 3 4 5 6 7 | ST表: 用于解决 '可重复贡献问题' 的数据结构 '可重复贡献问题' : 对于位运算opt, 满足 x opt x = x 那么引申到一个区间 就是可重复贡献问题 举例: 最大值(RMQ问题) max(x, x) = x, 那么它就是一个 '可重复贡献问题' , gcd 有 gcd(x, x) = x, 那么gcd也是 '可重复贡献问题' , 不够对于区间和就不具备这类性质了, 因此, 只有满足 x opt x这类结合律才满足ST表解决 |
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | //https://github.com/Ckeyliuhi/acm-iters #include <bits/stdc++.h> using i64 = int64_t; constexpr int N = 1e3 + 5, M = 26; int st1[N][M], st2[N][M], n, m, a[N], Log[N]; void init () { Log[0] = -1; for ( int i = 1; i <= n; i++) { Log[i] = Log[i >> 1] + 1; } for ( int i = 1; i <= n; i++) { st1[i][0] = st2[i][0] = a[i]; } for ( int j = 1; j <= Log[n]; j++) { for ( int i = 1; i + (1 << j) - 1 <= n; i++) { st1[i][j] = std::__gcd(st1[i][j - 1], st1[i + (1 << (j - 1))][j - 1]); } } } int query ( int l, int r) { int idx = Log[r - l + 1]; return std::__gcd(st1[l][idx], st1[r - (1 << idx) + 1][idx]); } void solve() { std::cin >> n >> m; for ( int i = 1; i <= n; i++) { std::cin >> a[i]; } init (); while (m--) { int l, r; std::cin >> l >> r; std::cout << query(l, r) << '\n' ; } } int main() { std::cin.tie(0) -> sync_with_stdio( false ); int t = 1; // std::cin >> t; while (t--) { solve(); } return 0; } |