专栏文章
一种新的LCA算法
算法·理论参与者 42已保存评论 59
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 59 条
- 当前快照
- 1 份
- 快照标识符
- @mkqg8zxn
- 此快照首次捕获于
- 2026/01/23 13:37 4 周前
- 此快照最后确认于
- 2026/02/19 01:15 11 小时前
本文将介绍一种 LCA 算法,具有实现简洁、常数小的优势。
过程
预处理
需要维护以下信息:
- : 的子树大小。
- : 的父亲。
- : 在 dfs 序中所处的位置。
- : 的祖先中最深的节点 满足 。
我们通过一遍 dfs 算出所有信息。
、、 很容易维护。
但是 这东西怎么搞?
看上去很逆天,来证明一下:
-
如果一个点没有 ,显然,它的父亲也不可能有 。
-
由此可知,子树中没有 的点组成的点集一定时若干条从某个点到子树根节点的路径所经过的点组成点集的并。
-
有了前两条结论,我们已经可以很明显地发现子树中所有没有 地点可以被若干条从某个点到根的路径完整地覆盖,现在来证明它可以只用 条:假设存在至少两条,取其中任意两条,设其中一条为 到根,另一条为 到根,这里假设 ,很明显,从子树根节点到 和 的路径上不存在有 的点。
- 如果 是 的祖先,那你还留着 干什么?
- 否则,令 ,可以发现 ,又由于 ,有 ,则 可以是 ,这与“从子树根节点到 的路径上不存在有 的点”矛盾,证明到此完成。
这样,我们就可以 dfs 了。
废话少说,直接看代码:
CPP vector<int> sz(n + 1),to(n + 1),at(n + 1),f(n + 1);
int dftop = 0;
auto dfs = [&](auto &&self,int x,int y) -> int {
f[x] = y;
sz[x] = 1;
at[x] = ++dftop;//更新f、sz、at
int mxsz = 0,vv = x;
//不得不扯一下树链剖分LCA的内容了,在树剖中,查询复杂度为O(logn)的关键就在于每跳一条轻边子树大小至少*2,这里也用到了类似的原理,把儿子分为轻重儿子,由于sz[轻儿子]<=sz[重儿子],轻儿子的子树中所有还没找到to[v]的点v,to[v]都被确认为x。
for (auto &i:a[x])
if (i != y){
int v = self(self,i,x);
sz[x] += sz[i];//更新sz
if (sz[i] > mxsz)
mxsz = sz[i],swap(vv,v);//重儿子是最后被更新的,如果重儿子由之前确认的点u变成i,则把u按照轻儿子的方式处理并把重儿子设置为i,否则把i按照轻儿子的方式处理。
for (;v != x;v = f[v])
to[v] = x;
}
for (;sz[vv] * 2 <= sz[x];vv = f[vv]) to[vv] = x;//处理重儿子
return vv;
};
查询
如果要查询点 和 的 LCA,则重复执行以下步骤:
- 如果,交换 和 。
- 如果 且 ,即 是 的祖先,结束。
- 否则,。
结束后 就是 LCA。
但是你凭什么说这样是对的?难道不会在第三步的时候跳多了跳到 LCA 的祖先上面去?
还真不会,证明:
- 令 为 和 真实的 LCA。
- 。
- 。
- ,那 一定不比 浅,直接跳过去肯定是对的。
复杂度:由于每跳一次 至少会乘 ,复杂度为 。
这个算法做P3379完整的代码:
CPP#include <bits/stdc++.h>
using namespace std;
int main (){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int n,m,s;
cin >> n >> m >> s;
vector<vector<int>> a(n + 1);
for (int i = n;--i;){
int x,y;
cin >> x >> y;
a[x].push_back(y);
a[y].push_back(x);
}
vector<int> sz(n + 1),to(n + 1),at(n + 1),f(n + 1);
int dftop = 0;
auto dfs = [&](auto &&self,int x,int y) -> int {
f[x] = y;
sz[x] = 1;
at[x] = ++dftop;
int mxsz = 0,vv = x;
for (auto &i:a[x])
if (i != y){
int v = self(self,i,x);
sz[x] += sz[i];
if (sz[i] > mxsz)
mxsz = sz[i],swap(vv,v);
for (;v != x;v = f[v])
to[v] = x;
}
for (;sz[vv] * 2 <= sz[x];vv = f[vv]) to[vv] = x;
return vv;
};
dfs(dfs,s,0);
for (;m--;){
int x,y;
cin >> x >> y;
for (;;){
if (sz[x] > sz[y]) swap(x,y);
if (at[y] <= at[x]&&at[x] < at[y] + sz[y]) break;
x = to[x];
}
cout << y << '\n';
}
}
优劣势分析
优势
-
实现简洁、常数小、需要维护的信息少,实测中,在节点数和查询量相近时具有和树链剖分相近的速度。
-
相较于树链剖分,这个算法少一遍 dfs,在节点多查询少时理论上具有更优的性能。
劣势
-
逆天正确性不明显,需要复杂证明支撑。 -
树链剖分能够轻松支持各种扩展功能,这个算法不行。
相关推荐
评论
共 59 条评论,欢迎与作者交流。
正在加载评论...