如何写出像2021CSP-S初赛最后一题一样炫酷的RMQ
前排提示,虽然这个算法名义上是标准RMQ,但是在比赛时写这个不如st表,线段树等简便的算法/数据结构,甚至只用笛卡尔树硬跳就能实现不错的常数比线段树小但是不带修的RMQ算法。
CSP-S初赛
当我在考场上看到最后一题:
-
笛卡尔树,好的,写过
-
LCA,好的,写过
-
欧拉序,好的,写过不少次了
-
ST表,好的,已经刻进DNA里了
-
分块,emmm……勉强吧
-
\(\pm 1\)RMQ…… 我后悔没学\(<\Theta(n),\Theta(1)>\)的LCA……
于是赛后赶紧补上。
先看看都有哪些RMQ
-
朴素算法 \(<\mathrm{O}(n)>\)
-
st表 \(<\mathrm{O}(n \log n),\mathrm{O}(1)>\)
-
Four Russian \(<\mathrm{O}(n \log \log n),\mathrm{O}(1)>\)
-
sqrt tree \(<\mathrm{O}(n \log \log n),\mathrm{O}(1)>\)且支持\(\mathrm{O}(\sqrt{n})\)修改
-
线段树 \(<\mathrm{O}(n),\mathrm{O}(\log n)>\)
-
猫树 \(<\mathrm{O}(n \log n),\mathrm{O}(1)>\)
-
转笛卡尔树然后做倍增LCA \(<\mathrm{O}(n \log n),\mathrm{O}(\log n)>\)
-
转笛卡尔树然后树剖LCA \(<\mathrm{O}(n),\mathrm{O}(\log n)>\)
-
转笛卡尔树然后暴力从根节点向下跳\(<\mathrm{O}(n),\mathrm{O}(?)>\)
-
标准RMQ \(<\mathrm{O}(n),\mathrm{O}(1)>\)
看个例题
emmm这个\(<\Theta(n\log n),\Theta(1)>\)就能过,为什么选这个?
因为这题最优解写的是不如这个好的非完全体Four Russian
好吧那来个更带劲的。
为什么这题开幕雷击看到瑞典铲车啊,这河里吗
题意就是给你一个特离谱的线性同余随机数生成器
关于线性同余和梅森缠绕器(c++11的mt19937)就不讲了。
反正就是用输入给你的随机种子来生成许多随机数,研究一下线性同余就知道这个玩意真的挺随机(但是依然不能满足蒙特卡洛这样对随机数要求太高的算法)的,看来不能用数学知识乱搞了。
然后看看数据范围
……
…………
………………
2e7………………
首先可以说st表已经爬了,非完全体的Four Russians(相当于开三个st表,常数略大,复杂度\(<\mathrm{O}(n\log \log n)>,\Theta(1)\))也可以爬了,线段树什么的查询都带\(\log\)更别想了。
于是来到了重头戏。
先想一想什么算法是\(<\Theta(n),\Theta(1)>\)的。
TarjanLCA?没什么关系。
\(\pm 1\)RMQ?这个确实是RMQ但是有条件,要求相邻两个数差值都是1。
这个RMQ能在线求LCA,因为在欧拉序列上相邻两个元素一定是父节点和子结点的关系,深度差为1.
于是考虑让区间的RMQ上树变成LCA
遇事不决,问题离线/上树
这时候就轮到笛卡尔树了
这张从OI-wiki上盗来的图最能让你明白笛卡尔树的性质。
可以发现在这张图中查询区间的最小值转化成了求两个结点的LCA,而使用单调栈构造笛卡尔树的过程是\(\mathrm{O}(n)\)的,实在是非常棒。
现在考虑LCA。
离线的TarjanLCA可以做到线性预处理,\(\Theta(1)\)回答每个询问,这明显已经可以解决上面的例题了。
让我们的要求更高一点,考虑在线查询的做法。
现在就要选择如何做LCA了。
-
树上倍增,80%的OIer第一个学会的非朴素算法的求LCA方法\(<\mathrm{O}(n \log n),\mathrm{O}(\log n)>\)
-
Tarjan,使用并查集的离线算法
-
树剖,常数小,逻辑清楚十分好写\(<\Theta(n),\mathrm{O}(\log n)>\)
-
转化为欧拉序然后做RMQ
好的我们成功的绕回了原来的问题复杂度取决于选择的RMQ算法
为了不让问题递归下去,我们要找出一个合式的对于欧拉序列求RMQ的算法。
根据定义,欧拉序列求LCA要求的是两个结点在序列首次出现位置之间的节点中深度最小的。也就是比较关键词是深度,但是序列上是结点。
于是考虑欧拉序上深度有什么性质
欧拉序列是在DFS过程中搜索和回溯到一个结点时都将其加入序列末尾来构造的。
于是可以发现,序列上一个结点搜索到其子结点或者回溯到其父结点而得到序列的下一个结点,那么两者深度必定相差1。
每个结点会被记录多次但是按照渐进式和极限的思维我们的复杂度还是线性所以不要慌哈哈哈哈哈
此时之前的Four Russian又来了,这个算法的复杂度瓶颈在于分块后处理块内部分,再加上相差1的约束条件,通常这个算法被称作:
\(\pm 1\)RMQ
最麻烦的来了。
先分块。
令块长度为\(b = \lceil \frac{\log n}{2} \rceil\)
于是在整块间用st表查询,初始化复杂度\(\Theta(n)\)复杂度非常优秀丝毫不慌俗话说无论叠了几层O(n)还是线性
于是每块内有\(\frac{\log n}{2}\)个数,考虑到差值为1的特性,直接固定一个左端点,对于所有块内右端点暴力建表,查询时查表即可,实在是常数大到爆炸太方便进行暴力预处理了。
于是这个题就解了。
代码甚至可以直接参考今年初赛。
总结
整个RMQ的过程就是:
通过笛卡尔树把随机的RMQ转化为LCA
通过欧拉序列把LCA转化为有\(\pm 1\)约束的RMQ
通过分块和Four Russian解决欧拉序的RMQ
求得LCA,就是求得原序列的区间最值
还没结束
你可以发现这个标准RMQ占用的空间非常的,放到之前的例题就会MLE,于是该如何切掉 由乃救爷爷 这道题呢?
依然考虑笛卡尔树。
这棵树是一颗二叉树,于是考虑暴力从根节点向下跳,跳到覆盖的区间恰好在查询区间内,再加上数据随机,深度不超过\(\log n\),直接乱搞就能过。
代码
namespace RMQ {
constexpr int N = 100005;
constexpr int MT = N << 1;
constexpr int L = 20,B = 9,C = MT / B;
struct Node {
int val;
int dep,dfn,end;
Node *son[2];
}T[N];
Node *min(Node *x, Node *y) {
return x -> dep < y -> dep ? x : y;
}
int n,dfs_clock,b,c;
int pos[(1 << (B - 1)) + 5], dif[C + 5];
Node *r,*A[MT],*Min[L][C];
void build() {
static Node *S[N];
int top = 0;
repl(i,0,n) {
Node *p = &T[i];
while(top && S[top] -> val < p -> val)
p -> son[0] = S[top--];
if(top) S[top] -> son[1] = p;
S[++top] = p;
}
r = S[1];
}
void dfs(Node *u) {
A[u -> dfn = dfs_clock++] = u;
rep(i,0,1) if(u -> son[i]) {
u -> son[i] -> dep = u -> dep + 1;
dfs(u -> son[i]);
A[dfs_clock++] = u;
}
u -> end = dfs_clock - 1;
}
void init_st() {
b = (int)(ceil(log2(dfs_clock) / 2));
c = dfs_clock / b;
repl(i,0,c) {
Min[0][i] = A[b * i];
repl(j,1,b) Min[0][i] = min(Min[0][i],A[i * b + j]);
}
for (register int i = 1,l = 2;l <= c;++i,l <<= 1)
for (register int j = 0; j + l <= c;++j)
Min[i][j] = min(Min[i - 1][j], Min[i - 1][j + (l >> 1)]);
}
void init_block() {
rep(i,0,c)
for(register int j = 1;j < b && i * b + j < dfs_clock;++j)
if(A[i * b + j] -> dep < A[i * b + j - 1] -> dep)
dif[i] |= 1 << (j - 1);
repl(i,0,(1 << (b - 1))) {
int mx = 0,v = 0;
ff(j,1,b - 1) {
v += (i >> (j - 1) & 1) ? -1 : 1;
if(v < mx) {
mx = v;
pos[i] = j;
}
}
}
}
void init(int len) {
n = len;
build();
dfs(r);
init_st();
init_block();
}
#define l2(x) (31 - __builtin_clz(x))
Node *query_ST(int L,int R) {
int k = l2(R - L + 1);
return std::min(Min[k][L], Min[k][R - (1 << k) + 1]);
}
Node *query_block(int L,int R) {
int p = L / b;
int s = (dif[p] >> (L - p * b)) & ((1 << (R - L)) - 1);
return A[L + pos[s]];
}
Node *query(int L,int R) {
L = T[L].dfn,R = T[R].dfn;
if(L > R) std::swap(L,R);
int pl = L / b,pr = R / b;
if(pl == pr) return query_block(L,R);
Node *s = std::min(query_block(L, pl * b + b - 1),query_block(pr * b, R));
if(pl + 1 <= pr - 1) s = std::min(s,query_ST(pl + 1,pr - 1));
return s;
}
}