人面前人脉圈人没齐人煤气
放了十天的假,题做不动,直接摆烂又觉得过不去,就开始研究这玩意儿玩
rmq是这样一类问题:给定长度为 的初始序列,有若干次询问,每次询问序列在区间 内的最小(大)值。
在下文中,复杂度统一用 表示。其中 表示预处理复杂度, 表示单次询问复杂度。
目前主流的 rmq 解法大概有三种:
- st表,最实用好写的算法。。
- 线段,。
- Method of Four Russians。。常数较大。
第一第二种就直接略过了。以下叙述第三种方法。
首先对序列建立笛卡尔树,rmq 转 lca。然后对笛卡尔树建立欧拉序,lca 转 rmq我怕不是有病
这样转出来的序列有一个特殊的性质,相邻两项相差不超过 。
我们令块长 。对于整块建立一个 st 表,这部分时间复杂度 。
对于散块,由于相邻两项差不超过 且不可能 ,因此一个块的差分数组最多只有 种情况。
差分数组相同的块显然是本质相同的,不管如何查询,查询出来最小值的位置始终一样。
所以对于每种情况都预处理出每个区间的最小值。这部分时间复杂度 。
查询就整块查 st 表,散块查对于每种差分数组预处理出的结果。
以下代码能够通过洛谷 P3865。
#include <cstdio>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1 ++)
const int S = 10;
inline int min(const int x, const int y) {return x < y ? x : y;}
char buf[100000], *p1, *p2;
inline int read() {
char ch;
int x = 0;
while ((ch = gc) < 48);
do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
return x;
}
int a[100005], b[200005], stk[100005], ls[100005], rs[100005], fst[100005], dep[100005], top, cnt;
int st[15][20100], state[20100], d[1 << 10][10][10], seq[10], Log[20100];
void dfs(int u) {
b[fst[u] = ++ cnt] = u;
if (ls[u]) dep[ls[u]] = dep[u] + 1, dfs(ls[u]), b[++ cnt] = u;
if (rs[u]) dep[rs[u]] = dep[u] + 1, dfs(rs[u]), b[++ cnt] = u;
}
inline int getmin(int l, int r) {
if (l > r) return 0;
int k = Log[r - l + 1];
return dep[st[k][l]] < dep[st[k][r - (1 << k) + 1]] ? st[k][l] : st[k][r - (1 << k) + 1];
}
int query(int l, int r) {
if (l > r) l ^= r ^= l ^= r;
int i = (l - 1) / S + 1, j = (r - 1) / S + 1;
if (i == j) return a[b[d[state[i]][l - (i - 1) * S - 1][r - (i - 1) * S - 1] + (i - 1) * S + 1]];
int ans = getmin(i + 1, j - 1);
int x = b[d[state[i]][l - (i - 1) * S - 1][S - 1] + (i - 1) * S + 1];
int y = b[d[state[j]][0][r - (j - 1) * S - 1] + (j - 1) * S + 1];
if (dep[x] < dep[ans]) ans = x;
if (dep[y] < dep[ans]) ans = y;
return a[ans];
}
int main() {
int n = read(), q = read(), m = 0;
for (int i = 1; i <= n; ++ i) {
a[i] = -read();
while (top && a[stk[top]] > a[i]) -- top;
rs[stk[top]] = i;
ls[i] = stk[top + 1];
stk[++ top] = i;
for (int j = top + 1; stk[j]; ++ j) stk[j] = 0;
}
dfs(stk[1]);
n = (cnt + 9) / 10 * 10;
for (int i = 0; i < 1 << S; ++ i) {
for (int j = 0; j < S - 1; ++ j) seq[j + 1] = seq[j] + (i & 1 << j ? -1 : 1);
for (int l = 0; l < S; ++ l) {
d[i][l][l] = l;
for (int r = l + 1; r < S; ++ r)
if (seq[d[i][l][r - 1]] <= seq[r]) d[i][l][r] = d[i][l][r - 1];
else d[i][l][r] = r;
}
}
dep[0] = 1e9;
for (int i = 1; (i - 1) * S + 1 <= n; ++ i) {
st[0][i] = 0, ++ m;
for (int j = (i - 1) * S + 1; j <= i * S; ++ j)
if (dep[b[j]] < dep[st[0][i]]) st[0][i] = b[j];
for (int j = i * S; j > (i - 1) * S + 1; -- j)
state[i] = (state[i] << 1) | (dep[b[j - 1]] >= dep[b[j]]);
}
for (int i = 2; i <= m; ++ i) Log[i] = Log[i >> 1] + 1;
for (int i = 1; i <= 14; ++ i)
for (int j = 1; j + (1 << i) - 1 <= m; ++ j)
if (dep[st[i - 1][j]] <= dep[st[i - 1][j + (1 << i - 1)]]) st[i][j] = st[i - 1][j];
else st[i][j] = st[i - 1][j + (1 << i - 1)];
while (q --) {
int l = read(), r = read();
printf("%d\n", -query(fst[l], fst[r]));
}
return 0;
}
In fact,尽管这种方法理论复杂度是非常严格的,但是不仅常数大还难写。
所以就有了某些 rmq 的神秘做法。
对序列分块,以 为块长,整块预处理 st 表耗时 。对于每块预处理出前缀后缀最大值。
对于至少跨过两个块的查询,可以做到 。对于两个端点在同一块内的查询,直接暴力。
不难发现,两端点在同一块内的概率是 这个级别。而在同一块内复杂度为 级别,因此总的期望复杂度是 的。
以下代码可以通过洛谷 P3793。
#include <cstdio>
#include <cmath>
inline int max(const int x, const int y) {return x > y ? x : y;}
inline int min(const int x, const int y) {return x < y ? x : y;}
inline void max2(int& x, const int y) {if (x < y) x = y;}
namespace GenHelper
{
unsigned z1,z2,z3,z4,b;
unsigned rand_()
{
b=((z1<<6)^z1)>>13;
z1=((z1&4294967294U)<<18)^b;
b=((z2<<2)^z2)>>27;
z2=((z2&4294967288U)<<2)^b;
b=((z3<<13)^z3)>>21;
z3=((z3&4294967280U)<<7)^b;
b=((z4<<3)^z4)>>12;
z4=((z4&4294967168U)<<13)^b;
return (z1^z2^z3^z4);
}
}
void srand(unsigned x)
{using namespace GenHelper;
z1=x; z2=(~x)^0x233333333U; z3=x^0x1234598766U; z4=(~x)+51;}
int read()
{
using namespace GenHelper;
int a=rand_()&32767;
int b=rand_()&32767;
return a*32768+b;
}
int a[20000005], b[5005], c[20000005], d[20000005], s[5005], st[5005][15], S;
inline int Query(const int L, const int R) {
int k(log2(R - L + 1));
return max(st[L][k], st[R - (1 << k) + 1][k]);
}
inline int query(const int l, const int r) {
int i((l - 1) / S + 1), j((r - 1) / S + 1), ans(0);
if (i == j) {
for (int k(l); k <= r; ++ k) ans = max(ans, a[k]);
return ans;
}
if (i + 1 < j) ans = Query(i + 1, j - 1);
return max(ans, max(d[l], c[r]));
}
int main() {
int n, m, s;
unsigned long long ans(0);
scanf("%d%d%d", &n, &m, &s);
S = sqrt(n);
srand(s);
int len(ceil(n * 1.0 / S));
for (int i(1); i <= n; ++ i) max2(b[(i - 1) / S + 1], a[i] = read());
for (int i(1); i <= len; ++ i) {
int st((i - 1) * S + 1), ed(min(n, i * S));
c[st] = a[st];
for (int j(st + 1); j <= ed; ++ j) c[j] = max(c[j - 1], a[j]);
d[ed] = a[ed];
for (int j(ed - 1); j >= st; -- j) d[j] = max(d[j + 1], a[j]);
}
for (int i(1); i <= len; ++ i) st[i][0] = b[i];
for (int j(1); 1 << j <= len; ++ j)
for (int i(1); i + (1 << j) - 1 <= len; ++ i)
st[i][j] = max(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
while (m --) {
int l, r;
l = read() % n + 1, r = read() % n + 1;
if (l > r) l ^= r ^= l ^= r;
ans += query(l, r);
}
printf("%llu", ans);
return 0;
}
前面介绍的算法,一个不可能考场上去写,一个害怕被卡,所以这里来一个 practical 的算法。
考虑将所有询问按照右端点从左到右处理。显然,如果 且 ,则右端点在 之后的询问中 不可能发挥作用。
因此维护一个单调栈,对于每个询问二分即可。
二分?真要去二分是不可能的,我们可以搞个并查集维护出目前 后面第一个在栈中的元素。并查集的复杂度是近似线性的,所以这个算法也是近线性的。
而且这个并查集合并是 的,常数就更小了。经过实测,发现能够通过 P3793,说明这玩意儿确实快。
这玩意儿甚至比 st 表还短,考场卡常利器。
唯一的缺点是需要离线,不过一般很少有强制在线还卡常的 rmq 吧(?)
以下代码能够通过洛谷 P3793。
#include <cstdio>
#include <algorithm>
#include <cstring>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1 ++)
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
char buf[100000], *p1, *p2;
inline int read() {
char ch;
int x = 0;
while ((ch = gc) < 48);
do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
return x;
}
namespace GenHelper
{
unsigned z1,z2,z3,z4,b;
unsigned rand_()
{
b=((z1<<6)^z1)>>13;
z1=((z1&4294967294U)<<18)^b;
b=((z2<<2)^z2)>>27;
z2=((z2&4294967288U)<<2)^b;
b=((z3<<13)^z3)>>21;
z3=((z3&4294967280U)<<7)^b;
b=((z4<<3)^z4)>>12;
z4=((z4&4294967168U)<<13)^b;
return (z1^z2^z3^z4);
}
}
void srand(unsigned x)
{using namespace GenHelper;
z1=x; z2=(~x)^0x233333333U; z3=x^0x1234598766U; z4=(~x)+51;}
int rnd()
{
using namespace GenHelper;
int a=rand_()&32767;
int b=rand_()&32767;
return a*32768+b;
}
struct Edge {int to, nxt;} e[20000005];
int a[20000005], fa[20000005], head[20000005], stk[20000005], top, tot;
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}
inline void AddEdge(int u, int v) {
e[++ tot].to = v, e[tot].nxt = head[u], head[u] = tot;
}
int main() {
unsigned long long ans = 0;
int n = read(), q = read(), s = read();
srand(s);
for (int i = 1; i <= n; ++ i) a[i] = rnd(), fa[i] = i;
for (int i = 1; i <= q; ++ i) {
int l = rnd() % n + 1, r = rnd() % n + 1;
if (l > r) l ^= r ^= l ^= r;
AddEdge(r, l);
}
for (int i = 1; i <= n; ++ i) {
while (top && a[stk[top]] < a[i]) fa[stk[top --]] = i;
stk[++ top] = i;
for (int j = head[i]; j; j = e[j].nxt) ans += a[find(e[j].to)];
}
printf("%llu", ans);
return 0;
}
还有一个貌似是最快的严格算法,复杂度也是 的(其实严格来说是 的,但对于有意义的 都有 ,所以我们认为它是线性的)。
按照四毛子的思路,以 为块长分块。块间预处理用 st 表。
然后对于每一个块,我们可以模拟单调栈的过程一个一个加入该块中的数。
假设我们知道这个块加入 这些数后的单调栈状态,就能回答所有右端点为 的 rmq 询问。
注意到单调栈状态可以表示为“每一个数当前是否在栈中"。每一块有 个数,因此可以把当前单调栈状态压到一个 bitset 里面去。
实际上,一般 都小于 ,所以一个整数就足够了。
记下了每个时刻的单调栈状态,散块查询就可以__builtin_ctz
解决了。
没写,目测比四毛子好写很多。也是比较 practical 的。
简单来说,除了四毛子都是 practical 的。
本文作者:zqs2020
本文链接:https://www.cnblogs.com/stinger/p/16567447.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步