W
e
l
c
o
m
e
: )

[算法] The Method of Four Russians

dqstz csq-2021 完善程序 T2 出题人石锤了(

经过初赛的吊锤之后, 我们都知道,四毛子算法(The Method of Four Russians)是一种 \(O(n)-O(1)\) 的优秀 RMQ 在线算法,具体是怎样的呢?

四毛子算法主要用到以下知识点:

  1. 笛卡尔树;

  2. dfs 环游序(也叫 Euler 序);

  3. 基于分块的 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\) 第一次在环游序中出现的位置,则有:

\[dep_{\operatorname{LCA}(a, b)}=\min_{dfn_a\le i \le dfn_b} dep_i \]

我们又重新把 LCA 问题转化为了 RMQ !!1

开始套娃

你可能会说:这并没有什么用啊。

但是这里仍然有个新的性质:\(\color{red} {环游序中每相邻两个结点的深度差恒为 1}\),记住这个,我们将在后面用到它。

分块 RMQ

设块长为 \(B\),则整个序列被分成 \(t=\frac{n}{B}\) 块。

对于整块的询问,我们直接使用 ST 表,复杂度 \(O(t \log t)\)

对于散块,我们有两种方法:

  1. 直接暴力预处理每两个区间内的最大值,\(O(tB^2)=O(nB)\)

  2. 再套一个 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 表,复杂度为:

\[O(\frac{2n}{\log n} \log \frac{2n}{\log n})=O(\frac{2n}{\log n} (\log n+\log 2-\log \log n))=O(\frac{2n}{\log n} \log n)=O(n) \]

现在考虑散块怎么做,想起上面那个性质,转化为解决下面这个问题:

在一个 相邻两数差为 ±1 的序列里面,找最大值的位置

知道了位置就能知道值,所以求最大值和求最大值的位置的询问本质上是一样的,但是转化后我们只需要知道序列的相对大小!于是想到队员序列差分,一共有 \(2^B\)个不同的序列,我们只需要考虑每一种情况的最大值位置,珂以 \(O(2^BB)\) 暴力预处理出来。

这样做看起来很劣,但是 \(B\)\(\log\) 级别的数就很优秀,这里 \(B=\frac{\log n}{2}\),所以复杂度:

\[O(2^BB)=2^{\frac{\log n}{2}}\frac{\log n}{2}=(2^{\log n})^{\frac{1}{2}}\frac{\log n}{2}=\sqrt{n}\log n \le O(n) \]

十分优秀,这样子,我们就在预处理复杂度不超过 \(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;
}
posted @ 2022-01-18 21:42  127_127_127  阅读(787)  评论(0)    收藏  举报