关于RMQ的一些拓展

引入

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


静态区间最值

也就是对于一个序列AAA,我们每次要查询一个区间l∼rl\sim rlr中的min/max{Ai}min/max\{A_i\}min/max{Ai}

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

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

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

  • 类似用法

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


树上LCALCALCA(最近公共祖先)

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

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

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

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

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,11,2,3,2,4,2,1,5,6,5,7,5,1

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

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

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

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

转欧拉序后长度是n+mn+mn+m,所以复杂度最后为O((n+m)log(n+m)+Q)O((n+m)log(n+m)+Q)O((n+m)log(n+m)+Q)的,其中m=n−1m=n-1m=n1,所以就是O(nlogn+Q)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)O(n+Q),其实是可以的。

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


最终拓展

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

End

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

转载于:https://www.cnblogs.com/VictoryCzt/p/10053400.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值