人面前人脉圈人没齐人煤气

放了十天的假,题做不动,直接摆烂又觉得过不去,就开始研究这玩意儿玩

rmq是这样一类问题:给定长度为 \(n\) 的初始序列,有若干次询问,每次询问序列在区间 \([l,r]\) 内的最小(大)值。

在下文中,复杂度统一用 \(\Theta(a)-\Theta(b)\) 表示。其中 \(\Theta(a)\) 表示预处理复杂度,\(\Theta(b)\) 表示单次询问复杂度。

目前主流的 rmq 解法大概有三种:

  1. st表,最实用好写的算法。\(\Theta(n\log n)-\Theta(1)\)
  2. 线段,\(\Theta(n)-\Theta(\log n)\)
  3. Method of Four Russians。\(\Theta(n)-\Theta(1)\)。常数较大。

第一第二种就直接略过了。以下叙述第三种方法。

首先对序列建立笛卡尔树,rmq 转 lca。然后对笛卡尔树建立欧拉序,lca 转 rmq我怕不是有病

这样转出来的序列有一个特殊的性质,相邻两项相差不超过 \(1\)

我们令块长 \(b=\lfloor\frac{\log n}{2}\rfloor\)。对于整块建立一个 st 表,这部分时间复杂度 \(\Theta(\frac{n}{b}\log n)=\Theta(n)\)

对于散块,由于相邻两项差不超过 \(1\) 且不可能 \(0\),因此一个块的差分数组最多只有 \(2^b\) 种情况。

差分数组相同的块显然是本质相同的,不管如何查询,查询出来最小值的位置始终一样。

所以对于每种情况都预处理出每个区间的最小值。这部分时间复杂度 \(\Theta(2^bb^2)=\Theta(\sqrt{n}\log^2 n)<\Theta(n)\)

查询就整块查 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 的神秘做法。

对序列分块,以 \(\sqrt{n}\) 为块长,整块预处理 st 表耗时 \(\Theta(\sqrt{n}\log n)\)。对于每块预处理出前缀后缀最大值。

对于至少跨过两个块的查询,可以做到 \(\Theta(1)\)。对于两个端点在同一块内的查询,直接暴力。

不难发现,两端点在同一块内的概率是 \(\Theta(\frac{1}{\sqrt{n}})\) 这个级别。而在同一块内复杂度为 \(\Theta(\sqrt{n})\) 级别,因此总的期望复杂度是 \(\Theta(n)\) 的。

以下代码可以通过洛谷 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 的算法。

考虑将所有询问按照右端点从左到右处理。显然,如果 \(i<j\)\(a_i>a_j\),则右端点在 \(j\) 之后的询问中 \(i\) 不可能发挥作用。

因此维护一个单调栈,对于每个询问二分即可。

二分?真要去二分是不可能的,我们可以搞个并查集维护出目前 \(i\) 后面第一个在栈中的元素。并查集的复杂度是近似线性的,所以这个算法也是近线性的。

而且这个并查集合并是 \(\Theta(1)\) 的,常数就更小了。经过实测,发现能够通过 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;
}

还有一个貌似是最快的严格算法,复杂度也是 \(\Theta(n)-\Theta(1)\) 的(其实严格来说是 \(\Theta(\frac{n\log n}{\omega})\) 的,但对于有意义的 \(n\) 都有 \(\log n<\omega\),所以我们认为它是线性的)。

按照四毛子的思路,以 \(b=\log n\) 为块长分块。块间预处理用 st 表。

然后对于每一个块,我们可以模拟单调栈的过程一个一个加入该块中的数。

假设我们知道这个块加入 \(1\sim i\) 这些数后的单调栈状态,就能回答所有右端点为 \(i\) 的 rmq 询问。

注意到单调栈状态可以表示为“每一个数当前是否在栈中"。每一块有 \(b\) 个数,因此可以把当前单调栈状态压到一个 bitset 里面去。

实际上,一般 \(b\) 都小于 \(\omega\),所以一个整数就足够了。

记下了每个时刻的单调栈状态,散块查询就可以__builtin_ctz\(\Theta(1)\) 解决了。

没写,目测比四毛子好写很多。也是比较 practical 的。

简单来说,除了四毛子都是 practical 的。

posted @ 2022-08-10 10:19  zqs2020  阅读(36)  评论(0编辑  收藏  举报