ST表
- 即用f[i][j]表示从i开始向后$2^j-1$个的范围
- 应用倍增的思想,显然任意一段长度不小于2的st表对应区间都能由左右两段得到
f[i][j] = max ( f[i][j - 1] , f[i + 2^{j-1}][j - 1] )
- 注意要做到超出数组长度的部分不会对答案造成影响,且不会越界
- 1.在递推是对右半区间特判
- 2.扩展数组至原数组3-4倍大,并填充不会造成影响的值
- 递归更新顺序为先枚举j,再枚举i,且边界为f[i][0],即长度为1的区间
- 对于区间最大值查询[l,r],令t = \lfloor log_2(r-l+1) \rfloor
- 则$ans$ = max(f[l][t] , f[r-2^t+1][t])
void pre()
{
for(int j = 1;j <= 30;j++)
{
for(int i = 1;i + (1 << j) -1 <= N;j++)
{
f[i][j] = max(f[i][j] , f[i + (1 << (j - 1))][j - 1])
}
}
}
int log2[MAX];
for(int i = 2;i <= N;i++)
{
if(i == (1 << (log2[i - 1] + 1))
log2[i] = log2[i - 1] + 1;
else
log2[i] = log2[i - 1];
//等同于 log2[i] = log2[i/2]+1;
}
cin>> N >> M;
for(int i = 1;i <= N;i++)
{
cin>>num[i];
f[i][0]=num[i];
}
pre();
for(int i = 1;i <= M;i++)
{
int x,y;
cin >> x >> y;
int len = log2[y - x + 1];
cout << max(f[x][len] , f[y - (1 << len) + 1)][len])
}
- RMQ解决的是静态区间最值查询问题,要求查询的区间始终不变
eg1:降雨量
即求区间max,分类讨论
-
必假情况 \begin{cases} val[x]>val[y](x/y已知) \\ val[x]<=max(val[z])(x和至少一个z已知) \\ val[y]<=val[z](y和至少一个z已知) \end{cases}
-
必真情况 val[y] >= val > max(val[z]) (x,y,以及区间内的所有z已知)
-
剩余为可能
-
预处理st表,O(1)求区间max
倍增求LCA(最近公共祖先)
-
常见做法有倍增/树剖/RMQ/tarjan离线
-
考虑暴力求lca,预处理得到两个点的深度,每次选择深度较大的点向上跳一步直到两点重合,重合的位置即为LCA
-
但复杂度为O(n)
-
由于跳过头依然是公共祖先,考虑倍增
-
令fa[i][j]代表i向上跳$2^j$条边到达的节点
-
则fa[i][j] = fa[fa[i][j-1]][j-1]
-
dfs一遍求出fa[i][0],然后nlogn预处理出所有fa[i][j],这样向上跳的任意距离都可以被拆分为log段距离之和
-
查询是的上跳部分分为两段:
-
第一段跳到深度相同
-
第二段两点同时上跳到lca
-
两段路程分别用倍增数组拆分为至多log段完成
代码实现
#include <bits/stdc++.h>
using namespace std;
const int MAX = 500007;
struct edge
{
int next;
int to;
} e[MAX << 1];
int eid = 0;
int head[MAX];
int lg[MAX];
int father[MAX][22];
int dep[MAX];
void adde(int x, int y)
{
e[++eid].to = y;
e[eid].next = head[x];
head[x] = eid;
}
void LCA_prework(int u, int lst)
{
dep[u] = dep[lst] + 1;
father[u][0] = lst;
for (int i = 1; (1 << i) <= dep[u]; i++)
{
father[u][i] = father[father[u][i - 1]][i - 1];
}
for (int i = head[u]; i; i = e[i].next)
{
if (e[i].to == lst)
{
continue;
}
LCA_prework(e[i].to, u);
}
}
int LCA(int x, int y)
{
if (dep[x] < dep[y])
{
swap(x, y);
}
while (dep[x] > dep[y])
{
x = father[x][lg[dep[x] - dep[y]] - 1];
}
if (x == y)
{
return x;
}
for (int j = lg[dep[x]] - 1; j >= 0; j--)
{
if (father[x][j] != father[y][j])
{
x = father[x][j];
y = father[y][j];
}
}
return father[x][0];
}
int N, M, R;
int read()
{
int num = 0, bj = 0;
char ch = getchar();
while (!isdigit(ch))
{
if (ch == '-')
{
bj = 1;
}
ch = getchar();
}
while (isdigit(ch))
{
num = num * 10 + ch - '0';
ch = getchar();
}
return bj ? -num : num;
}
int main()
{
N = read();
M = read();
R = read();
for (int i = 1; i <= N; i++)
{
lg[i] = lg[i - 1] + (i == (1 << lg[i - 1]));
}
for (int i = 1; i <= N - 1; i++)
{
int fr, to;
fr = read();
to = read();
adde(fr, to);
adde(to, fr);
}
LCA_prework(R, 0);
int x, y;
for (int i = 1; i <= M; i++)
{
x = read();
y = read();
printf("%d\n", LCA(x, y));
}
}
eg2:货车运输
- kruskal重构树模板
eg3:求和
- 树剖/LCA
- 先考虑k恒定时的做法
- 预处理每个点到根路径上所有点的深度的k次方和
- 查询时用类似前缀和的方法提取出路径上的点权值和
- 令sum为点到根的所有点的深度的k次方和
- sum+sum[y]-2 * sum[lca(x,y)] + val[lca(x,y)]
- 发现k的范围极小,对于所有可能的k分别预处理sum即可
eg3:运输计划
- 二分总耗时t,若有航道的时间大于t,其路径上一定要选择一条修改,使得时间变短,让它小于t
- 所以改造的点一定在要修改的路径交上,可将每条路径上的每条边的计数+1,最后计数==路径数的即为路径交
- 用树上查分优化,c+=1,c[y]+=1,c[lca(x,y)]-=2
- 判断将路径交变成虫洞能否满足所有要求
eg4:仓鼠找sugar
- 结论题:两条路径相交,等价于存在某一条路径的lca在另一条路径上