算法描述
倍增算法在ST表中的应用是直接在第二维的dp中直接以二进制的形式展现;而对于树上的问题则可以这样设计:设fa[i][j]表示i结点的第2^j个祖先,显然的是fa[i][0]等于i的父节点,假设父节点的祖先结点都已经更新完毕,我们就可以根据父亲的祖先来更新本结点的祖先,于是状态转移方程就成了fa[i][j]=fa[fa[i][j-1]][j-1],这是根据二进制中可以拆分成两个相同小一次的二进制和的特性来构造的。于是,用 d f s dfs dfs把每个结点的fa处理好后求LCA就可以在 l o g n logn logn的复杂度求得。逻辑如下:
- 将两个点提到一个深度(所以在dfs中还要维护每个结点的深度)
- 两个结点一起向上跳,要从最多的步数然后越来越少地跳,如果两个祖先相等了可能是跳过头了,考虑不跳,然后不相等肯定没到达就选择往上跳,最后一定能到达LCA的儿子结点中,返回fa[u][0]即可。这里是根据二进制数-1可以由多个小于它的数组合而成的原理来构造的。
例子
以洛谷P3379板子题来检验代码正确性
#include <bits/stdc++.h>
using namespace std;
struct Edge
{
int to;
int next;
};
Edge E[1000005];
int head[500005], cnt;
int n, m, rt;
int fa[500005][19]; //i结点的第2^j个祖先
int deep[500005]; //结点深度
int Log[500005];
int Mi[20];
void edge_add(int u, int v)
{
E[cnt].to = v;
E[cnt].next = head[u];
head[u] = cnt++;
}
void init()
{
memset(head, -1, sizeof(head));
memset(fa, 0, sizeof(fa));
cnt = 0;
int u, v;
for (int i = 1; i < n; i++)
{
scanf("%d%d", &u, &v);
edge_add(u, v);
edge_add(v, u);
}
}
void dfs(int u, int parent) {
fa[u][0] = parent; //直接双亲更新
deep[rt]=deep[parent]+1;
for (int cur = 1; Mi[cur] <= deep[u]; cur++)
fa[u][cur]=fa[fa[u][cur-1]][cur-1]; //走到这个点说明前面的都已经更新完毕 利用前面的更新当前的
for (int cur = head[u]; ~cur; cur=E[cur].next)
if (E[cur].to^parent) //非父即子
dfs(E[cur].to, u);
}
int LCA(int u, int v) {
if (deep[u]<deep[v]) swap(u, v);
while (deep[u]>deep[v]) //爬树
u=fa[u][Log[deep[u]-deep[v]]]; //拆分深度差的二进制,尽量往上跳
if (u == v) return v;
for (int len=Log[deep[u]]; len>=0; len--)
if (fa[u][len]^fa[v][len]) { //相等考虑不跳,可能跳过头了,显然假设这个步长是解的话剩余的二进制加起来等于这个长度的二进制-1,所以底下返回父亲结点成立
u = fa[u][len];
v=fa[v][len];
}
return fa[u][0];
}
int main()
{
Log[0] = -1;
for (int i = 1; i <= 500000; i++)
Log[i] = Log[i >> 1] + 1;
Mi[0] = 1;
for (int i = 1; i < 20; i++)
Mi[i] = Mi[i - 1] << 1;
int v, u;
scanf("%d%d%d", &n, &m, &rt);
init();
dfs(rt, 0); //预处理fa表和深度表方便之后计算
for (int i = 0; i < m; i++)
{
scanf("%d%d", &u, &v);
printf("%d\n", LCA(u, v));
}
return 0;
}
优化常数的写法
#include <bits/stdc++.h>
using namespace std;
/**
* 常数最小
*/
int n, m, s;
int _to[1000005], _next[1000005], head[500005], cnt;
int deep[500005], fa[500005][19];
int Log[500005], Mi[20];
void edge_add(int u, int v) {
_to[cnt] = v;
_next[cnt] = head[u];
head[u] = cnt++;
}
void init() {
memset(head, -1, sizeof(head));
cnt=0;
int u, v;
for (int i = 1; i < n; i++) {
scanf("%d%d", &u, &v);
edge_add(u, v);
edge_add(v, u);
}
deep[0]=0; fa[0][0]=0;//根的虚拟祖先
}
void dfs(int rt, int parent) {
fa[rt][0] = parent;
deep[rt]=deep[parent]+1;
for (int i = 1; Mi[i] <= deep[rt]; i++) //注意这里是Mi而不是i
fa[rt][i]=fa[fa[rt][i-1]][i-1];
for(int cur=head[rt]; ~cur; cur=_next[cur])
if (_to[cur]^parent)
dfs(_to[cur], rt);
}
int LCA(int u, int v) {
if (deep[u] < deep[v]) swap(u, v);
while (deep[u] > deep[v])
u=fa[u][Log[deep[u]-deep[v]]];
if (u == v) return v;
for (int i=Log[deep[u]]; i >= 0; i--)
if (fa[u][i]^fa[v][i]) {
u=fa[u][i];
v=fa[v][i];
}
return fa[u][0];
}
int main() {
Log[0]=-1;
for (int i = 1; i < 500005; i++)
Log[i] = Log[i>>1]+1;
Mi[0]=1;
for (int i = 1; i < 20; i++)
Mi[i] = Mi[i-1]<<1;
scanf("%d%d%d", &n, &m, &s);
init();
dfs(s, 0);
int u, v;
for (int i = 0; i < m; i++) {
scanf("%d%d", &u, &v);
printf("%d\n", LCA(u, v));
}
return 0;
}