[算法] The Method of Four Russians
dqstz csq-2021 完善程序 T2 出题人石锤了(
经过初赛的吊锤之后, 我们都知道,四毛子算法(The Method of Four Russians)是一种 \(O(n)-O(1)\) 的优秀 RMQ 在线算法,具体是怎样的呢?
四毛子算法主要用到以下知识点:
-
笛卡尔树;
-
dfs 环游序(也叫 Euler 序);
-
基于分块的 RMQ。
先看笛卡尔树。
亮出模板题
笛卡尔树:
结点标号为 \((x_i, y_i)\),对于 \(x_i\),它满足二叉搜索树性质,对于 \(y_i\) 则满足堆性质。
考虑如何构造,先使其满足第一个性质,不难发现,我们只要按 \(x\) 从大到小 的顺序插入每一个结点,每次将当前节点作为之前一个结点的右儿子,必然满足。
但是这样做不能保证堆性质,如何解决?
若我当前的父亲与我不满足堆性质,那么我们将父亲与父亲的左链部分作为我的左儿子,自己到原来父亲的位置去(不难证明父亲原来的位置一定是某个节点的右儿子),若无法满足就继续进行次操作。
由于我们每进行一个操作,一个结点的左儿子都被填上了,相当于不能再用这个点,并且我们每次都是向上爬,不难想到用栈维护这个操作。
去掉对 \(x\) 的排序后复杂度为 \(O(n)\),一般这种构造方法在针对 \(x\) 已经有序的情况下,如模板题里面 \(x=id\) 有序。
Code:
struct node{
int x, y, son[2];//son[0],son[1]分别表示左右儿子
bool operator < (const node &A) const{return x<A.x;}
}T[N], S[N];
void build(){//建立Cartesian树
int top=0;
for(int i=1; i<=n; i++){
while(top&&T[S[top]].y>T[i].y)
T[i].son[0]=S[top--];
if(top) T[S[top]].son[1]=i;
//找到最终的父亲再接,省掉了修改
S[++top]=i;
}
}
int main(){
init()
sort(T);
build();
}
不难发现,如果我们要求 \([l, r]\) 区间的最 大/小 值,建出笛卡尔树后,问题转化为求 \(x=l\) 和 \(x=r\) 的两个结点的 LCA 的 \(y\) 值。
对于 LCA 问题,我们珂以使用 Tarjan + 离线 做到 \(O(n+m)\) 的复杂度,但是只能离线,还不够优秀。
dfs 环游序:
不容易直接说,见伪代码:
dfs(pos):
id[++cnt]=pos;
for( each E(pos, to) )
dfs(to),
id[++cnt]=pos;
为什么要这么做呢?

(这张是我随便从图床里翻出来的,不一定是笛卡尔树,它的编号为 \(/\) 左边的数字)
将 id 拍出来是:
1 2 4 2 5 2 1 3 6 3 7 3 1
若我们记 \(dfn_i\) 为点 \(i\) 第一次在环游序中出现的位置,则有:
我们又重新把 LCA 问题转化为了 RMQ !!1
开始套娃
你可能会说:这并没有什么用啊。
但是这里仍然有个新的性质:\(\color{red} {环游序中每相邻两个结点的深度差恒为 1}\),记住这个,我们将在后面用到它。
分块 RMQ
设块长为 \(B\),则整个序列被分成 \(t=\frac{n}{B}\) 块。
对于整块的询问,我们直接使用 ST 表,复杂度 \(O(t \log t)\)。
对于散块,我们有两种方法:
-
直接暴力预处理每两个区间内的最大值,\(O(tB^2)=O(nB)\)
-
再套一个 ST 表,\(O(tB \log B)=O(n \log B)\)。
你发现无论怎么搞还是带个 \(\log\),没错,你消不掉(
只需要明白这是怎么做的就好,接下来,我们来整理一下该做什么(以区间最大值为例):
对于原序列 \(a\),以 \(id\) 为 \(x\),\(a_i\) 为 \(y\) 建一颗笛卡尔树,然后,构造出其 dfs 序,现在,我们的任务就是找到 dfs 序 \([l, r]\) 中 \(dep\) 最小的位置。
考虑分块,块长为 \(\frac{\log n}{2}\),一共 \(\frac{2n}{\log n}\) 个块,整块询问依然 ST 表,复杂度为:
现在考虑散块怎么做,想起上面那个性质,转化为解决下面这个问题:
在一个 相邻两数差为 ±1 的序列里面,找最大值的位置
知道了位置就能知道值,所以求最大值和求最大值的位置的询问本质上是一样的,但是转化后我们只需要知道序列的相对大小!于是想到队员序列差分,一共有 \(2^B\)个不同的序列,我们只需要考虑每一种情况的最大值位置,珂以 \(O(2^BB)\) 暴力预处理出来。
这样做看起来很劣,但是 \(B\) 取 \(\log\) 级别的数就很优秀,这里 \(B=\frac{\log n}{2}\),所以复杂度:
十分优秀,这样子,我们就在预处理复杂度不超过 \(O(n)\)、单点询问 \(O(1)\) 的情况下解决了 RMQ 问题!!1
下面这份代码是使用 CCF csp-2021 S 组原题补空之后的,有详细注释,珂以通过 P3865,总耗时 1.8s。
Code:
#include <stdio.h>
#include <math.h>
#define MAXN 20000000
#define MAXT (MAXN<<1)
#define MAXL 18
#define MAXB 9
#define MAXC (MAXT/MAXB)
struct node{
int val, dep, dfn, end;
struct node *son[2];//son[0],son[1]分别表示左右儿子
}T[MAXN], *root, *id[MAXT], *Min[MAXL][MAXC];
struct node*min(struct node *x, struct node *y){return x->dep<y->dep?x:y;}
int n, t, b, c, Log2[MAXC+1];
int Pos[(1<<(MAXB-1))+5], Dif[MAXC+1];
void build(){//建立Cartesian树
static struct node*S[MAXN+1];//单调栈
int top=0;
for(int i=0; i<n; i++){
struct node *p=&T[i];
while(top&&S[top]->val<p->val)
p->son[0]=S[top--];
if(top) S[top]->son[1]=p;//右儿子在最后才连,这样就没必要把之前 pop 掉的儿子修改,
S[++top]=p;
}
root=S[1];
}
void DFS(struct node *p){//构建 Euler序列(dfs 环游序)
id[p->dfn=t++]=p;
for(int i=0; i<2; i++)
if(p->son[i]){
p->son[i]->dep=p->dep+1;
DFS(p->son[i]);
id[t++]=p;
}
p->end=t-1;
}
void ST_init(){//大块 ST 表预处理
b=(int)(ceil(log2(t)/2));//块长logn/2
c=t/b;//总共多少块
Log2[1]=0;
for(int i=2; i<=c; i++)
Log2[i]=Log2[i>>1]+1;
for(int i=0; i<c; i++){//先找出每一块内部最小值
Min[0][i]=id[i*b];
for(int j=1; j<b; j++)
Min[0][i]=min(Min[0][i], id[i*b+j]);
}
for(int i=1, l=2; l<=c; i++, l<<=1) //ST 表
for(int j=0; j+l<=c; j++)
Min[i][j]=min(Min[i-1][j], Min[i-1][j+(l>>1)]);
}
void small_init(){//块内预处理
for(int i=0; i<=c; i++)
for(int j=1; j<b&&i*b+j<t; j++)//记录差分数组 Dif
if(id[i*b+j]->dep<id[i*b+j-1]->dep) Dif[i]|=1<<(j-1);
for(int S=0; S<(1<<(b-1)); S++){
//计算状态 S 的最大值应该落在第几个位置
int mx=0,v=0;
for(int i=1; i<b; i++){
v+=(S>>(i-1)&1)?-1:1;
if(v<mx) mx=v, Pos[S]=i;
}
}
}
struct node*ST_query(int l,int r){//对于整块使用 ST 表
int g=Log2[r-l+1];
return min(Min[g][l],Min[g][r-(1<<g)+1]);
}
struct node*small_query(int l, int r){//块内查询
int p=l/b;//第几个块
int S=(Dif[p]>>(l-p*b))&((1<<(r-l))-1);
//将 [L[p], l-1], [r+1, R[p]] 多出来的清理掉
return id[l+Pos[S]];
}
struct node*query(int l, int r){
if(l>r) return query(r,l);
int pl=l/b, pr=r/b;
if(pl==pr) return small_query(l,r);//同一块内
else{
struct node *s=min(small_query(l, pl*b+b-1), small_query(pr*b,r));//询问散块
if(pl+1<=pr-1) s=min(s, ST_query(pl+1,pr-1));//中间的整块
return s;
}
}
int main(){
int m;
scanf("%d%d", &n, &m);
for(int i=0; i<n; i++)
scanf("%d", &T[i].val);
build();
DFS(root);
ST_init();
small_init();
while(m--){
int l,r;
scanf("%d%d", &l, &r);
printf("%d\n", query(T[l-1].dfn, T[r-1].dfn)->val);
}
return 0;
}

浙公网安备 33010602011771号