引入
关于 R M Q RMQ RMQ问题(静态区间最值查询),我们一般用的 S T ST ST表,但是还有很多其他用法与用途。
静态区间最值
也就是对于一个序列 A A A,我们每次要查询一个区间 l ∼ r l\sim r l∼r中的 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
其实思路是这样的,类似于倍增:
这样其实也类似于线段树对于一个区间的管理,但是由于是静态的,不涉及修改,所以我们可以用数组代替记录下来,然后直接查询。(查询每次访问两个数组的值是 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。
我们如果求出一棵树的欧拉序,我们来看看,如下图:
欧拉序:就是在深搜的过程中进入时加一次退出时也加一次,简单点就是每次访问时都加一次
我们对其求出的欧拉序为:
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=n−1,所以就是 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 ⌈lognn⌉log⌈lognn⌉,算下来不到 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(n2lognlog2logn+⌈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(⌈2lognn⌉log⌈2lognn⌉),所以最后还是
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
5∼6之间,比
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的大佬觉得有问题的话,提出并联系博主。