关于RMQ的一些拓展

引入

关于RMQRMQ问题(静态区间最值查询),我们一般用的STST表,但是还有很多其他用法与用途。


静态区间最值

也就是对于一个序列AA,我们每次要查询一个区间lrl\sim r中的min/max{Ai}min/max\{A_i\}

其实一般用树状数组或者线段树可以做到nlogn+Qlognnlogn+Qlogn的复杂度QQ为询问数,但是因为是静态的,我们可以用STST表做到nlogn+Qnlogn+Q

其实思路是这样的,类似于倍增:
eg

这样其实也类似于线段树对于一个区间的管理,但是由于是静态的,不涉及修改,所以我们可以用数组代替记录下来,然后直接查询。(查询每次访问两个数组的值是O(1)O(1)的)

代码实现大概这样:

Luogu模板
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M=2e5+10,Log=19;
int n,m;
int maxv[Log][M],lg[Log<<1],ref[M],cnt;
void init(){
	lg[0]=1;for(cnt=1;;cnt++)
	{lg[cnt]=(lg[cnt-1]<<1);if(lg[cnt]>n) break;}
	ref[2]=1;for(int i=3;i<=n;i++)ref[i]=ref[i>>1]+1;
	for(int i=1;i<=n;i++)scanf("%d",&maxv[0][i]);
	for(int i=1;i<=cnt;i++){
		for(int j=1,up=n-lg[i]+1;j<=up;j++){
			maxv[i][j]=max(maxv[i-1][j],maxv[i-1][j+lg[i-1]]);
		}
	}
}
int query(int a,int b){
	if(a>b)swap(a,b);
	int k=0,len=b-a+1;
	k=ref[len-1];//预处理这个后才是真正的O(1)
	return max(maxv[k][a],maxv[k][b-lg[k]+1]);//查询最新只需max换成min即可
}
int L,R;
int main(){
	scanf("%d%d",&n,&m);
	init();
	for(int i=1;i<=m;i++){
		scanf("%d%d",&L,&R);
		printf("%d\n",query(L,R));
	}
	return 0;
}

  • 类似用法

那么RMQRMQ还可以用来求取静态区间gcdgcd,合并方式只不过将max/minmax/min改成了gcdgcd


树上LCALCA(最近公共祖先)

我们可以用静态树的在线算法:倍增O(nlogn+Qlogn)O(nlogn+Qlogn),树链剖分O(n+Qlogn)O(n+Qlogn)
也可以用动态树的在线算法:LCT维护O(nlogn+Qlogn+大常数)O(nlogn+Qlogn+\text{大常数})
还可以使用静态树的离线算法:TrajanO(n+m+Q+并查集)O(n+m+Q+\text{并查集})

其实,如果询问量较多,可以使用RMQRMQ来实现查询LCALCA

我们如果求出一棵树的欧拉序,我们来看看,如下图:

欧拉序:就是在深搜的过程中进入时加一次退出时也加一次,简单点就是每次访问时都加一次

eg

我们对其求出的欧拉序为:

1,2,3,2,4,2,1,5,6,5,7,5,11,2,3,2,4,2,1,5,6,5,7,5,1

每个点的深度为:
dep[1]=1dep[1]=1
dep[2]=2dep[2]=2
dep[3]=3dep[3]=3
dep[4]=3dep[4]=3
dep[5]=2dep[5]=2
dep[6]=3dep[6]=3
dep[7]=3dep[7]=3

然后我们来看,先令st[i]st[i]ii号点最开始出现的位置,对于lca(a,b)lca(a,b),我们就只需查询欧拉序中的st[a]st[b](st[a]st[b])st[a]\sim st[b](st[a]\leq st[b])深度最小的那个点的编号即可。

我们模拟一下:
对于上述图中的lca(3,5)lca(3,5),我们相当于查询st[3]st[5]st[3]\sim st[5],那么这里面最小的深度的点就是3,2,4,3,1,53,2,4,3,1,5中的11,而11也确实是它们的lcalca

其实正确性是这样的,对于欧拉序中的两个开始位置直之间的点,肯定包含完了这个两个点的路径上的所有点,而lcalca肯定在路径上,并且深度是最小的,所以这样就可以求出。

转欧拉序后长度是n+mn+m,所以复杂度最后为O((n+m)log(n+m)+Q)O((n+m)log(n+m)+Q)的,其中m=n1m=n-1,所以就是O(nlogn+Q)O(nlogn+Q)的。

代码:

#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int M=6e5+10,Log=22;
int n,m,lg[M<<1],s; 

struct node{
	int p,dep;
	node(){}
	node(int a,int b):p(a),dep(b){}
	bool operator <(const node &a)const{return dep<a.dep;}
}maxv[Log][M<<1];

struct ss{
	int to,last;
	ss(){}
	ss(int a,int b):to(a),last(b){}
}g[M<<1];
int head[M],cnt;
void add(int a,int b){
	g[++cnt]=ss(b,head[a]);head[a]=cnt;
	g[++cnt]=ss(a,head[b]);head[b]=cnt;
}
int dep[M],pos[M],tot;
void dfs(int a,int b){
	dep[a]=dep[b]+1;maxv[0][pos[a]=++tot]=node(a,dep[a]);
	for(int i=head[a];i;i=g[i].last){
		if(g[i].to==b) continue;
		dfs(g[i].to,a);
		maxv[0][++tot]=node(a,dep[a]);
	}
}
void init(){
	lg[2]=lg[3]=1;
	for(int i=4;i<=tot;i++)lg[i]=lg[i>>1]+1;
	for(int i=1;(1ll<<i)<=tot;i++){
		for(int j=1;j<=tot;j++){
			maxv[i][j]=min(maxv[i-1][j],maxv[i-1][j+(1<<(i-1))]);
		}
	}
}
int getlca(int a,int b){
	if(a>b)swap(a,b);
	int k=lg[b-a+1];
	return min(maxv[k][a],maxv[k][b-(1<<k)+1]).p;
}
int a,b;
int main(){
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<n;i++){
		scanf("%d%d",&a,&b);
		add(a,b);
	}
	dfs(s,0);
	init();
	for(int i=1;i<=m;i++){
		scanf("%d%d",&a,&b);
		printf("%d\n",getlca(pos[a],pos[b]));
	}
	return 0;
}

拓展

我们能不能做到和离线的Tarjan同样优秀的复杂度呢?O(n+Q)O(n+Q),其实是可以的。

我们观察一个性质,就是欧拉序里面的相邻两点的depdep差不超过11,所以可以使用±1RMQ\pm 1RMQ

其实这种RMQRMQ网上很少讲,虽然有,但是不清楚,所以博主自己yyyy了几种方法。


对于O(nlogn)O(nlogn)的预处理,这是主要要解决的问题,查询O(1)O(1)已经非常优秀了。

所以我们考虑分块,对于每一块我们做一次RMQRMQ,对于分出来的所有块我们再做一次RMQRMQ,块的大小大概是lognlogn的大小,总共分成nlogn\lceil\frac{n}{logn}\rceil块。

对于每一块,先内部求RMQRMQ,那么复杂度为nlogn×logn×log(logn)\lceil\frac{n}{logn}\rceil\times logn\times log(logn),所以复杂度为nloglognnloglogn

然后知道每一块的最值,我们再对nlogn\lceil\frac{n}{logn}\rceil块求一个RMQRMQ,那么复杂度为nlognlognlogn\lceil\frac{n}{logn}\rceil log\lceil\frac{n}{logn}\rceil,算下来不到O(n)O(n)

所以总的复杂度为O(nloglogn)O(nloglogn)

每次查询则分为三部分,两个块内和一个块间,所以复杂度还是O(1)O(1)的。


但是这个根本没用到相邻的相差11的性质。
所以我们再来看,同样分块,将+1,1+1,-1的变化看作0,10,1,我们将,然后对于一块只有2logn2=2logn=n2^{\frac{logn}{2}}=\sqrt{2^{logn}}=\sqrt{n}种不同的情况。

所以我们枚举这些不同情况(用二进制枚举的方式)
类似于这种:

int S=(1<<int(log2(n)+1))>>1;
for(int i=0;i<=S;i++)work();

然后处理这些情况下,从左往右的前缀最小(大),(应该是处理区间和的最值,也就是偏移量,但是这里实际上的实现似乎有点小问题)。
如:01010101
则表示的是1,+1,1,+1-1,+1,-1,+1
然后我们维护的其实是01010101的前缀和的RMQRMQ

那么对应到实际上的序列,我们只需知道左端点的值就能快速算出真正最小的值。

那么将每种区间的情况对应上去,每次查询只需加上偏移值即可(也就是左端点值,如果你开始设置的最左边的一个差为11的话你要减去11,否则加上11)。

那么复杂度为O(nlogn2loglogn2+nlogn2)O(\sqrt{n}\frac{logn}{2}log\frac{logn }{2}+\lceil{\frac{n}{\frac{logn}{2}}}\rceil)

块间的处理还是用原来的RMQRMQ的方式,复杂度为O(nlogn2lognlogn2)O(\lceil\frac{n}{\frac{logn}{2}}\rceil log\lceil\frac{n}{\frac{logn}{2}}\rceil),所以最后还是O(n)O(n)的。
具体来说,在n=1e8n=1e8的时候,复杂度才只有不到6e86e8
其实计算来就是1e4×13×4+1e8×4+7692308×23=5774430841e4\times13\times4+1e8\times 4+7692308\times 23=577443084
而在n=1e7n=1e7的时候就只有:
3162×12×4+1e7×4+16666667=568184433162\times 12\times 4+1e7\times 4+16666667=56818443
n=1e6n=1e6的时候只有:
1000×10×4+1e6×4+1700000=57400001000\times 10\times 4+1e6\times 4+1700000=5740000
所以常数大概是在565\sim 6之间,比nlognnlognlognlogn小的多了,况且询问是O(1)O(1)

那么对于边界块的特殊处理:
对于不满长度logn2\frac{logn}{2}的块,暴力RMQRMQ即可,复杂度为常数。

代码实现,目前不太好写,博主就没有写,而且没找到卡nlognnlognLCALCA的题,QWQ。


最终拓展

其实对于所有的一般的序列,不满足±1\pm1的性质的,我们可以将其转化为笛卡尔树,然后区间最值问题就转化为了树上求LCALCA的问题,就可以用±1RMQ\pm1RMQ了,于是就可以做到O(n)O(n)

End

讲解中也许有很多问题,如果有会±1RMQ\pm 1RMQ的大佬觉得有问题的话,提出并联系博主。

posted @ 2018-10-24 18:35  VictoryCzt  阅读(196)  评论(0编辑  收藏  举报