线性倍增(RMQ)
空间换时间
设状态
minn[i][j]
m
i
n
n
[
i
]
[
j
]
表示区间
[i,i+2j−1]
[
i
,
i
+
2
j
−
1
]
的最小值。
如下图,易得
minn[i][j]=min(minn[i][j−1],minn[i+2j−1][j−1])
m
i
n
n
[
i
]
[
j
]
=
min
(
m
i
n
n
[
i
]
[
j
−
1
]
,
m
i
n
n
[
i
+
2
j
−
1
]
[
j
−
1
]
)
。
注意边界为
minn[i][0]=a[i]
m
i
n
n
[
i
]
[
0
]
=
a
[
i
]
,
a
a
为原数组。
这样,若我们要查询区间 的最小值,所需时间只需
O(1)
O
(
1
)
。
而
i+2j−1⩽n
i
+
2
j
−
1
⩽
n
,故
j
j
的上界为 ,该数组占用的空间为
O(nlogn)
O
(
n
log
n
)
(有额外开销),这即是传统意义上的“空间换时间”。
如何求值
给定区间
[l,r]
[
l
,
r
]
,用上文的方法我们可以求出区间
[l,l+2j]
[
l
,
l
+
2
j
]
与
[r−2j+1,r]
[
r
−
2
j
+
1
,
r
]
的最小值。
那么要求
[l,r]
[
l
,
r
]
的最小值,只需让
[l,l+2j]
[
l
,
l
+
2
j
]
与
[r−2j+1,r]
[
r
−
2
j
+
1
,
r
]
的并为
[l,r]
[
l
,
r
]
即可。
列不等式
解得
(注意这里的 log log 不是整数)取整时,为了保险,一般取 j⩾log(r−l) j ⩾ log ( r − l ) 。
复杂度
查询区间最值,如上文所说,单次查询可以在
O(1)
O
(
1
)
的时间内出解。
空间复杂度为
O(nlogn)
O
(
n
log
n
)
代码实现
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int N,Q,a[200001],l,r;
namespace RMQ{
int minn[200001][18],maxn[200001][18];
void build_ST(int *a,int n){
for(int i=1;i<=n;++i)minn[i][0]=maxn[i][0]=a[i];
int lg=(int)log2(n);
for(int j=1;j<=lg;++j)for(int i=1;i+(1<<j)-1<=n;++i){
minn[i][j]=min(minn[i][j-1],minn[i+(1<<j-1)][j-1]);
maxn[i][j]=max(maxn[i][j-1],maxn[i+(1<<j-1)][j-1]);
}
}
int query_min(int l,int r){
int lg=(int)log2(r-l+1);
return min(minn[l][lg],minn[r-(1<<lg)+1][lg]);
}
int query_max(int l,int r){
int lg=(int)log2(r-l+1)-1;
return max(maxn[l][lg],maxn[r-(1<<lg)+1][lg]);
}
}
int main(){
scanf("%d%d",&N,&Q);
for(int i=1;i<=N;++i)scanf("%d",a+i);
RMQ::build_ST(a,N);
for(int i=1;i<=Q;++i){
scanf("%d%d",&l,&r);
printf("Minimum of interval [%d,%d]: %d\n",l,r,RMQ::query_min(l,r));
printf("Maximum of interval [%d,%d]: %d\n",l,r,RMQ::query_max(l,r));
}
}
树上倍增
树上倍增的应用非常广泛,可以在静态树中维护各种信息,并可以在 O(logn) O ( log n ) 时间内查询。
基本思想
实质上与RMQ的查询非常类似,这里以查询两点的LCA举例。
若用
fa[u][i]
f
a
[
u
]
[
i
]
表示
u
u
的第 个祖先,那么易得递推式
在dfs时预处理即可。
进军LCA
假设对于点对
(u,v)
(
u
,
v
)
,
dep[u]⩽dep[v]
d
e
p
[
u
]
⩽
d
e
p
[
v
]
,它们的LCA为
a
a
。
若 与
v
v
深度相同,考虑 的祖先的性质。由于
lca(u,v)=a
l
c
a
(
u
,
v
)
=
a
,那么显然
a
a
的所有祖先都是 与
v
v
的公共祖先,反之亦然。因此若 ,那么
fa[u][i]
f
a
[
u
]
[
i
]
要么等于
a
a
要么等于 的祖先。故这种情况下,可以考虑枚举所有
i
i
,若 ,则将
u
u
与 更新为
fa[u][i]
f
a
[
u
]
[
i
]
和
fa[v][i]
f
a
[
v
]
[
i
]
。假设
fa[u][i]=fa[v][i]
f
a
[
u
]
[
i
]
=
f
a
[
v
]
[
i
]
,那么对于
j>i
j
>
i
,
fa[u][j]=fa[v][j]
f
a
[
u
]
[
j
]
=
f
a
[
v
]
[
j
]
,故要从高位(即
log2h
l
o
g
2
h
,
h
h
为树高)往低位枚举 ,每次找到合法的
i
i
就更新 和
v
v
。
对于深度不同的情况,其实非常简单,前面假设了 ,又因为它们深度不同,那么
dep[u]<dep[v]
d
e
p
[
u
]
<
d
e
p
[
v
]
,这时若将
v
v
更改为 的第
dep[v]−dep[u]
d
e
p
[
v
]
−
d
e
p
[
u
]
个祖先,
a
a
显然不变,于是将 拆为二进制,用
fa
f
a
数组将
v
v
的深度置为与 相同。
扩展:树上最短路径最大/最小点权/边权
模仿求LCA,设
minn[u][i]
m
i
n
n
[
u
]
[
i
]
为
u
u
到 路径上的最小边权,那么有状态转移方程:
边界:
minn[u][0]
m
i
n
n
[
u
]
[
0
]
的值为
u
u
到 的边权。
其余均可同理实现。
代码实现
#include<iostream>
#include<cstdio>
#include<vector>
#include<cmath>
using namespace std;
const int SIZE=500000,LOG2_SIZE=20;
struct tree{
vector<int> point[SIZE+1];
int fa[SIZE+1][LOG2_SIZE],dep[SIZE+1],log_n;
void dfs_dp(int u){
for(int i=1;i<=log_n;++i)
if(fa[u][i-1])
fa[u][i]=fa[fa[u][i-1]][i-1];
else
break;
for(int v:point[u])
if(v!=fa[u][0]){
fa[v][0]=u;
dep[v]=dep[u]+1;
dfs_dp(v);
}
}
void jump(int &v,int d){
int log_d=(int)log2(d);
for(int i=0;i<=log_d;++i)
if(d&(1<<i))
v=fa[v][i];
}
int query(int u,int v){
if(dep[u]>dep[v])
swap(u,v);
jump(v,dep[v]-dep[u]);
if(u==v)
return u;
for(int i=log_n;~i;--i)
if(fa[u][i]!=fa[v][i]){
u=fa[u][i];
v=fa[v][i];
}
return fa[u][0];
}
};
tree T;
int N,Q,R,u,v;
int main(){
scanf("%d%d%d",&N,&Q,&R);
for(int i=1;i<N;++i){
scanf("%d%d",&u,&v);
T.point[u].push_back(v);
T.point[v].push_back(u);
}
T.log_n=(int)log2(N);
T.dfs_dp(R);
for(int i=1;i<=Q;++i){
scanf("%d%d",&u,&v);
printf("The lowest common ancestor of %d and %d is: %d\n",T.query(u,v));
}
}