关于RMQ的一些拓展

引入

关于 R M Q RMQ RMQ问题(静态区间最值查询),我们一般用的 S T ST ST表,但是还有很多其他用法与用途。


静态区间最值

也就是对于一个序列 A A A,我们每次要查询一个区间 l ∼ r l\sim r lr中的 m i n / m a x { A i } min/max\{A_i\} min/max{Ai}

其实一般用树状数组或者线段树可以做到 n l o g n + Q l o g n nlogn+Qlogn nlogn+Qlogn的复杂度 Q Q Q为询问数,但是因为是静态的,我们可以用 S T ST ST表做到 n l o g n + Q nlogn+Q nlogn+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;
}

  • 类似用法

那么 R M Q RMQ RMQ还可以用来求取静态区间 g c d gcd gcd,合并方式只不过将 m a x / m i n max/min max/min改成了 g c d gcd gcd


树上 L C A LCA LCA(最近公共祖先)

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

其实,如果询问量较多,可以使用 R M Q RMQ RMQ来实现查询 L C A LCA LCA

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

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

eg

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

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

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

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

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

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

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

我们观察一个性质,就是欧拉序里面的相邻两点的 d e p dep dep差不超过 1 1 1,所以可以使用 ± 1 R M Q \pm 1RMQ ±1RMQ

其实这种 R M Q RMQ RMQ网上很少讲,虽然有,但是不清楚,所以博主自己 y y yy yy了几种方法。


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

所以我们考虑分块,对于每一块我们做一次 R M Q RMQ RMQ,对于分出来的所有块我们再做一次 R M Q RMQ RMQ,块的大小大概是 l o g n logn logn的大小,总共分成 ⌈ n l o g n ⌉ \lceil\frac{n}{logn}\rceil lognn块。

对于每一块,先内部求 R M Q RMQ RMQ,那么复杂度为 ⌈ n l o g n ⌉ × l o g n × l o g ( l o g n ) \lceil\frac{n}{logn}\rceil\times logn\times log(logn) lognn×logn×log(logn),所以复杂度为 n l o g l o g n nloglogn nloglogn

然后知道每一块的最值,我们再对 ⌈ n l o g n ⌉ \lceil\frac{n}{logn}\rceil lognn块求一个 R M Q RMQ RMQ,那么复杂度为 ⌈ n l o g n ⌉ l o g ⌈ n l o g n ⌉ \lceil\frac{n}{logn}\rceil log\lceil\frac{n}{logn}\rceil lognnloglognn,算下来不到 O ( n ) O(n) O(n)

所以总的复杂度为 O ( n l o g l o g n ) O(nloglogn) O(nloglogn)

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


但是这个根本没用到相邻的相差 1 1 1的性质。
所以我们再来看,同样分块,将 + 1 , − 1 +1,-1 +1,1的变化看作 0 , 1 0,1 0,1,我们将,然后对于一块只有 2 l o g n 2 = 2 l o g n = n 2^{\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();

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

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

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

那么复杂度为 O ( n l o g n 2 l o g l o g n 2 + ⌈ n l o g n 2 ⌉ ) O(\sqrt{n}\frac{logn}{2}log\frac{logn }{2}+\lceil{\frac{n}{\frac{logn}{2}}}\rceil) O(n 2lognlog2logn+2lognn)

块间的处理还是用原来的 R M Q RMQ RMQ的方式,复杂度为 O ( ⌈ n l o g n 2 ⌉ l o g ⌈ n l o g n 2 ⌉ ) 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 = 1 e 8 n=1e8 n=1e8的时候,复杂度才只有不到 6 e 8 6e8 6e8
其实计算来就是 1 e 4 × 13 × 4 + 1 e 8 × 4 + 7692308 × 23 = 577443084 1e4\times13\times4+1e8\times 4+7692308\times 23=577443084 1e4×13×4+1e8×4+7692308×23=577443084
而在 n = 1 e 7 n=1e7 n=1e7的时候就只有:
3162 × 12 × 4 + 1 e 7 × 4 + 16666667 = 56818443 3162\times 12\times 4+1e7\times 4+16666667=56818443 3162×12×4+1e7×4+16666667=56818443
n = 1 e 6 n=1e6 n=1e6的时候只有:
1000 × 10 × 4 + 1 e 6 × 4 + 1700000 = 5740000 1000\times 10\times 4+1e6\times 4+1700000=5740000 1000×10×4+1e6×4+1700000=5740000
所以常数大概是在 5 ∼ 6 5\sim 6 56之间,比 n l o g n nlogn nlogn l o g n logn logn小的多了,况且询问是 O ( 1 ) O(1) O(1)

那么对于边界块的特殊处理:
对于不满长度 l o g n 2 \frac{logn}{2} 2logn的块,暴力 R M Q RMQ RMQ即可,复杂度为常数。

代码实现,目前不太好写,博主就没有写,而且没找到卡 n l o g n nlogn nlogn L C A LCA LCA的题,QWQ。


最终拓展

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

End

最近发现了一篇有代码实现的+1-1RMQ(约束RMQ)的博客 - 【Orz%%%

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VictoryCzt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值