一、ST表

1.预处理

f[i][j]f[i][j]表示从i开始的长度为2j的区间即区间[i,i+2j1][i,i+2j−1])

递推公式j(j在外层递增):):

f[i][j]=maxf[i][j1],f[i+2j1][j1]f[i][j]=max{f[i][j−1],f[i+2j−1][j−1]}

即将区间[l,r][l,r]分为两个区间合并

2.查询

分为两段,第一段为区间[l,2k][l,2k],第二段为区间[r2k+1,r][r−2k+1,r],其中kk为满足2krl+12k≤r−l+1的所有数中最大的那个数

即区间[l,r][l,r]的最大值为maxf[l][k],f[r2k+1][k]max{f[l][k],f[r−2k+1][k]}

3.例子

忠诚 洛谷 P1816

多次查询区间最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int m,n;
#define MAXN 100001
#define LOG 18
int st[MAXN][LOG];
int lg2[MAXN];
int main(){
scanf("%d %d", &n, &m);
memset(st, 0x3f, sizeof st);
for(int i=1;i<=n;++i) scanf("%d", &st[i][0]);
for(int len=1;len<LOG;++len)
for(int i=1;i+(1<<len)-1<=n;++i)
st[i][len]=min(st[i][len-1], st[i+(1<<(len-1))][len-1]);
for(int i=2;i<=n;++i) lg2[i]=lg2[i>>1]+1;
while(m--){
int l,r;scanf("%d %d", &l, &r);
int sz=lg2[r-l+1];
printf("%d ", min(st[l][sz], st[r+1-(1<<sz)][sz]));
}
return 0;
}


二、树上倍增

1.求解LCA

(1) 预处理

首先结合dfs预处理出f[i][j]f[i][j]f[i][j],f[i][j]表示节点i向上跳2j层的节点

递推公式:

f[i][j]=f[f[i][j1]][j1]f[i][j]=f[f[i][j−1]][j−1]

即节点i分两次向上跳,每次跳2j12^{j−1}层跳到的节点就是节点ii向上跳2j2^j层的节点2j1×2=2j(2^{j−1}×2=2^j)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void load(int x, int fa){
f[x][0]=fa;
dep[x]=dep[fa]+1;
for(int i=1;i<20;++i)
f[x][i]=f[f[x][i-1]][i-1];
}
void dfs(int u, int fa){
load(u, fa);
for(int i=head[u];i;i=nxt[i]){
int v=vv[i];
if(v==fa) continue;
dfs(v, u);
}
}

同时也可以像下面预处理出log2nlog^n_2的所有值以优化常数

1
2
for(int i=2;i<=tot;++i)
lg2[i]=lg2[i>>1]+1; // 预处理log2n

(2) 查询

首先使两个查询节点跳至同一高度后(因为它们的最近公共祖先不可能低于这两点,跳跃方法同下),当前层记为xx,然后从log2xlog^x_200枚举(递减能保证可以完全分解成二进制)jj,如果上跳2j2^j层后不重合,那么就继续跳,重合则不跳,使两点层数一直逼近最近公共祖先,最后跳完202^0层后,两点必定会停在最近公共祖先的下一层,所以最后直接取当前层iif[i][0]f[i][0]就好了。

其中,可以直接从最大可能的ii开始枚举,因为反正ii也很小。

1
2
3
4
5
6
7
8
9
10
11
inline int lca(int a, int b){
if(dep[a]<dep[b]) swap(a, b);
for(int i=20;i>=0;--i)
if(dep[f[a][i]]>=dep[b])
a=f[a][i];
if(a==b) return a;
for(int i=20;i>=0;--i)
if(f[a][i]!=f[b][i])
a=f[a][i], b=f[b][i];
return f[a][0];
}

这是一种在线求LCA的算法,其实还有Tarjan这种效率高的离线算法。

2.O(1)O(1)求LCA

另外还有一种nlognnlogn预处理,每次O(1)O(1)查询的方法。

即用欧拉序+RMQ+RMQ可实现O(1)O(1)求得LCALCA。先dfsdfs全树,记录欧拉序(就是记下dfsdfs走过的节点),然后在欧拉序上以节点深度作为权值建STST表,每次查欧拉序上深度最小的点即为LCALCA。此时将问题转换为区间求最小值RMQRMQ问题。

(1)预处理

1
2
3
4
5
6
7
8
9
10
11
void dfs(int u, int fa){
dfn[u]=++tot;
f[tot][0]=u;
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=nxt[i]){
int v=vv[i];
if(v==fa) continue;
dfs(v, u);
f[++tot][0]=u; // 再记录
}
}
1
2
3
4
5
6
7
8
for(int i=1;i<20;++i)
for(int j=1;j+(1<<i)-1<=tot;++j)
if(dep[f[j][i-1]]<dep[f[j+(1<<(i-1))][i-1]])
f[j][i]=f[j][i-1];
else
f[j][i]=f[j+(1<<(i-1))][i-1];
for(int i=2;i<=tot;++i)
lg2[i]=lg2[i>>1]+1; // 预处理log2n

(2)查询

1
2
3
4
5
6
7
8
int lca(int a, int b){
int l=dfn[a],r=dfn[b];
if(l>r) swap(l,r);
int lg=lg2[r-l+1];
if(dep[f[l][lg]]<dep[f[r-(1<<lg)+1][lg]])
return f[l][lg];
else return f[r-(1<<lg)+1][lg];
}

3.求路径上最值

求树上两点a,ba,b路径上的最小权值

我们再设一个g[i][j]g[i][j]表示节点ii向上跳2j2^j内所经过的最小权值即可,转移方程:

g[i][j]=min(g[i][j1],g[f[i][j1]][j1])g[i][j]=min(g[i][j−1],g[f[i][j−1]][j−1])