数据结构——RMQ(ST表)问题
数据结构——RMQ(ST表)问题
问题描述
对于序列 \(A[1\dots n]\),有 \(m\) 组询问 \(\langle l,r\rangle\),求 \(\max_{i=l}^rA_i\)。
我们下面使用 \(\mathcal O(A)\sim\mathcal O(B)\) 表示预处理 \(\mathcal O(A)\),单次询问 \(\mathcal O(B)\) 的时间复杂度。
线段树解法
时间复杂度:\(\mathcal O(n)\sim\mathcal O(\log n)\)。
空间复杂度:\(\mathcal O(n)\)。
#include <bits/stdc++.h> using namespace std; constexpr int N = 2e5 + 10; int n, a[N]; #define ls(x) ((x) << 1) #define rs(x) ((x) << 1 | 1) int seg[N << 2]; void push_up(int k) { seg[k] = max(seg[ls(k)], seg[rs(k)]); } void build(int k = 1, int l = 1, int r = n) { if (l == r) return void(seg[k] = a[l]); int mid = l + r >> 1; build(ls(k), l, mid); build(rs(k), mid + 1, r); push_up(k); } int query(int p, int q, int k = 1, int l = 1, int r = n) { if (l >= p && r <= q) return seg[k]; int mid = l + r >> 1; if (p > mid) return query(p, q, rs(k), mid + 1, r); if (q < mid + 1) return query(p, q, ls(k), l, mid); else return max(query(p, q, ls(k), l, mid), query(p, q, rs(k), mid + 1, r)); } signed main() { ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr); cin >> n; copy_n(istream_iterator<int>(cin), n, a + 1); build(); int q, l, r; cin >> q; while (q--) cin >> l >> r, cout << query(l, r) << endl; return 0; }
修改复杂度 \(\mathcal O(\log n)\)。
ST 表
ST 表可以做到 \(\mathcal O(n\log n)\) 预处理,\(\mathcal O(1)\) 求出序列区间最大值。
按照最基础的思想,设 \(f(i,j)\) 表示区间 \([i,j]\) 的最大值,考虑上述倍增思想。
重新设计状态,
用 \(f(i,j)\) 表示区间 \([i,i+2^j-1]\) 的最大值,也就是从 \(i\) 开始的 \(2^j\) 个数。
考虑这样子递推的边界,
- 显然 \(f(i,0)=a_i\)。
- 显然 \(f(i,j)=\max\{f(i,j-1),f(i+2^{j-1},j-1)\}\)。
这么折半的预处理,可以做到 \(\mathcal O(n\log n)\) 的复杂度。
考虑查询,如果我们按照朴素的思想去处理的话,也是 \(\mathcal O(n\log n)\) 的,但是
有一个很简单的性质,\(\max\{x,x\}=x\),这意味着我们可以重复计算一个区间的最大值。
于是,我们可以把区间中一部分重复的区间跳过,直接去计算:
能覆盖整个区间的两个左右端点上的整个区间,就可以做到 \(\mathcal O(1)\)。
除 RMQ 以外,还有其它的「可重复贡献问题」。例如「区间按位与」、「区间按位或」等。
ST 表能较好的维护「可重复贡献」的区间信息(同时也应满足结合律),时间复杂度较低。
可重复贡献问题是指满足 \(x\operatorname{opt} x=x\) 的运算对应的区间询问。
例如,\(\max(x,x)=x\),\(\operatorname{gcd}(x,x)=x\),等等。
所以 RMQ 和区间 GCD 就是一个可重复贡献问题,像区间和就不具有这个性质。
如果预处理区间重叠了,则会导致重叠部分被计算两次,这是我们所不愿意看到的。
#include <bits/stdc++.h> using namespace std; constexpr int N = 2e5 + 10; constexpr int K = 20; int n, a[N]; int st[N][K]; #define pow2(x) (1 << (x)) void build() { for (int i = 1; i <= n; ++i) st[i][0] = a[i]; for (int k = 1; k < K; ++k) for (int i = 1; i + pow2(k) - 1 <= n; ++i) st[i][k] = max(st[i][k - 1], st[i + pow2(k - 1)][k - 1]); } int query(int p, int q) { int k = log2(q - p + 1); return max(st[p][k], st[q - pow2(k) + 1][k]); } signed main() { ios::sync_with_stdio(false); cin.tie(nullptr), cout.tie(nullptr); cin >> n; copy_n(istream_iterator<int>(cin), n, a + 1); build(); int q, l, r; cin >> q; while (q--) cin >> l >> r, cout << query(l, r) << endl; return 0; }
不支持修改(复杂度很差)。
UPD on 20240725:更新下面内容。
为了空间局部性,我们把 ST 表第二维放到前面,如下,
int st[K][N]; #define pow2(x) (1 << (x)) void build() { for (int i = 1; i <= n; ++i) st[0][i] = a[i]; for (int k = 1; k < K; ++k) for (int i = 1; i + pow2(k) - 1 <= n; ++i) st[k][i] = min(st[k - 1][i], st[k - 1][i + pow2(k - 1)]); } int query(int p, int q) { int k = __lg(q - p + 1); return min(st[k][p], st[k][q - pow2(k) + 1]); }
同时使用 __lg
计算 \(\log_2\),这样可以获得更快的运行速度。
评测记录:
-
\(f(i,k)\):AC 659 ms https://judge.yosupo.jp/submission/223238.
-
\(f(k,i)\):AC 624 ms https://judge.yosupo.jp/submission/223236.
本文来自博客园,作者:RainPPR,转载请注明原文链接:https://www.cnblogs.com/RainPPR/p/18288455
如有侵权请联系我(或 2125773894@qq.com)删除。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战