线性倍增(RMQ)
空间换时间
设状态 minn[i][j]minn[i][j] 表示区间 [i,i+2j−1][i,i+2j−1] 的最小值。
如下图,易得 minn[i][j]=min(minn[i][j−1],minn[i+2j−1][j−1])minn[i][j]=min(minn[i][j−1],minn[i+2j−1][j−1]) 。
注意边界为 minn[i][0]=a[i]minn[i][0]=a[i] , aa 为原数组。
这样,若我们要查询区间 的最小值,所需时间只需 O(1)O(1) 。
而 i+2j−1⩽ni+2j−1⩽n ,故 jj 的上界为 ,该数组占用的空间为 O(nlogn)O(nlogn) (有额外开销),这即是传统意义上的“空间换时间”。
如何求值
给定区间 [l,r][l,r] ,用上文的方法我们可以求出区间 [l,l+2j][l,l+2j] 与 [r−2j+1,r][r−2j+1,r] 的最小值。
那么要求 [l,r][l,r] 的最小值,只需让 [l,l+2j][l,l+2j] 与 [r−2j+1,r][r−2j+1,r] 的并为 [l,r][l,r] 即可。
列不等式
解得
(注意这里的 loglog 不是整数)取整时,为了保险,一般取 j⩾log(r−l)j⩾log(r−l) 。
复杂度
查询区间最值,如上文所说,单次查询可以在 O(1)O(1) 的时间内出解。
空间复杂度为 O(nlogn)O(nlogn)
代码实现
#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(logn) 时间内查询。
基本思想
实质上与RMQ的查询非常类似,这里以查询两点的LCA举例。
若用 fa[u][i]fa[u][i] 表示 uu 的第 个祖先,那么易得递推式
在dfs时预处理即可。
进军LCA
假设对于点对 (u,v)(u,v) , dep[u]⩽dep[v]dep[u]⩽dep[v] ,它们的LCA为 aa 。
若 与 vv 深度相同,考虑 的祖先的性质。由于 lca(u,v)=alca(u,v)=a ,那么显然 aa 的所有祖先都是 与 vv 的公共祖先,反之亦然。因此若 ,那么 fa[u][i]fa[u][i] 要么等于 aa 要么等于 的祖先。故这种情况下,可以考虑枚举所有 ii ,若 ,则将 uu 与 更新为 fa[u][i]fa[u][i] 和 fa[v][i]fa[v][i] 。假设 fa[u][i]=fa[v][i]fa[u][i]=fa[v][i] ,那么对于 j>ij>i , fa[u][j]=fa[v][j]fa[u][j]=fa[v][j] ,故要从高位(即 log2hlog2h , hh 为树高)往低位枚举 ,每次找到合法的 ii 就更新 和 vv 。
对于深度不同的情况,其实非常简单,前面假设了 ,又因为它们深度不同,那么 dep[u]<dep[v]dep[u]<dep[v] ,这时若将 vv 更改为 的第 dep[v]−dep[u]dep[v]−dep[u] 个祖先, aa 显然不变,于是将 拆为二进制,用 fafa 数组将 vv 的深度置为与 相同。
扩展:树上最短路径最大/最小点权/边权
模仿求LCA,设 minn[u][i]minn[u][i] 为 uu 到 路径上的最小边权,那么有状态转移方程:
边界: minn[u][0]minn[u][0] 的值为 uu 到 的边权。
其余均可同理实现。
代码实现
#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));
}
}
本文详细介绍了线性倍增(RMQ)和树上倍增算法。线性倍增采用空间换时间策略,可在 O(1) 时间查询区间最值,空间复杂度为 O(nlogn)。树上倍增应用广泛,能在静态树中维护信息,以查询两点 LCA 为例介绍其基本思想,还提及树上最短路径最大/最小点权/边权的扩展应用。
2519

被折叠的 条评论
为什么被折叠?



